@deepseek-ai/dsh-client-ui-user-questions 0.1.1-rc.2 → 0.1.2-alpha.2
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.i18n.yaml +2 -2
- package/README.md +83 -6
- package/README.zh.md +84 -7
- package/lib/client.js +247 -93
- package/lib/types/client/QuestionComposer.d.ts +6 -5
- package/lib/types/client/contract/slots.d.ts +49 -34
- package/lib/types/client/draft-store.d.ts +37 -0
- package/lib/types/client/index.d.ts +3 -4
- package/package.json +26 -22
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-user-questions/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 2462a3d25644cf073b4652d564a9f7a213e92250
|
|
6
|
+
README.zh.md: 296974703d77791393811b89fd046078969584b5
|
package/README.md
CHANGED
|
@@ -1,20 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Web ask_user_question feature for the dsh web client: the composer-takeover question UI and the plan-review approval card."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-client-ui-user-questions
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-client-ui-user-questions` is the web question feature plugin: its browser half registers the `question` entry in the conversation-owned `conversation.composer` chain, so when the agent asks the user a question the composer is taken over by the question UI. The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges, and custom answers, and submits one structured answer batch for the whole request. A request whose single question declares a presentation intent renders as that intent's own surface instead — notably the `plan-review` waiting-approval card with `Chat about it` / `Refuse` / `Approve`. Its host half is empty on purpose: mounting `dsh-tool-ask-user` there would put the tool in the registry's global layer and merge it into every agent regardless of the preset that composed it.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
When the agent asks a question, the composer becomes the question surface: answer each question, navigate with the pager, or skip it. Single-select choices advance immediately; Enter continues the flow and submits once every question is answered or skipped, while Shift+Enter breaks a line instead (during IME composition Enter only confirms the input candidate without advancing).
|
|
29
|
+
|
|
30
|
+
### Answering
|
|
31
|
+
|
|
32
|
+
A multi-select draft keeps its selected labels while the user opens or edits the custom answer, so its submitted item may carry both `selected` and `custom`; a single-select custom answer remains exclusive. Question detail reuses the assistant-output `MarkdownText` primitive, including its GFM rendering and untrusted-content policy. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. "Skip this question" retains other drafts and emits the existing blank `{ selected: [] }` result for that item, while close rejects the whole wait as `ASK_CANCELLED`.
|
|
33
|
+
|
|
34
|
+
### The plan-review card
|
|
35
|
+
|
|
36
|
+
A `plan-review` intent — set by `dsh-plan-mode` on the `exit_plan_mode` review — renders the waiting-approval card layout: a `Plan review` strip, the plan as the scrolling markdown body, and one decision row of `Chat about it` / `Refuse` / `Approve`. Approve and Refuse answer with the asker's own option labels; `Chat about it` rejects the wait as `ASK_CANCELLED`, returning the composer so the user can say what they want instead.
|
|
37
|
+
|
|
38
|
+
### Failure and recovery
|
|
39
|
+
|
|
40
|
+
The generic question flow keeps its current page, selected labels, custom text, and explicit skips in a non-persisted Slot store scoped to the owning Session and keyed by the pending request's local render identity. Switching from Session A to B remounts the strict composer entry, but returning to A reuses A's store and restores the unfinished draft. A different request identity reads an empty draft and replaces the previous value on its first edit; a successful answer or cancellation clears the matching value. The host remains authoritative for whether the request is pending.
|
|
41
|
+
|
|
42
|
+
-----
|
|
43
|
+
|
|
44
|
+
<a id="understand-the-implementation"></a>
|
|
45
|
+
## Understand the implementation
|
|
6
46
|
|
|
7
|
-
|
|
47
|
+
<details>
|
|
48
|
+
<summary>Implementation internals — click to expand</summary>
|
|
8
49
|
|
|
9
|
-
|
|
50
|
+
The package is one ownership rule: rendering a question is a host UI capability, having the tool is an agent capability, so the `tool-ask-user` row belongs to the presets that want it (and to the TUI composition, which has no presets).
|
|
10
51
|
|
|
11
|
-
|
|
52
|
+
### Intent surface election
|
|
53
|
+
|
|
54
|
+
The card claims a request only when it can send every answer that request allows: one question, the intent declared, the plan present as `detail`, the named approve label offered, and a binary single choice (at most one option besides approve, not multi-select). Anything else stays on the generic flow, which can express it. An intent changes the layout, never which answers are reachable.
|
|
55
|
+
|
|
56
|
+
### Copy and locale
|
|
12
57
|
|
|
13
58
|
Composer chrome copy (pager, buttons, placeholders, validation feedback) is bilingual: the plugin registers zh/en dictionaries under the `question` namespace of `dsh-client-locale` and hands the entry its bound translator plus the locale snapshot source through the inject face, so a locale switch re-renders a mounted composer. Question and option text arrives from the model and renders verbatim; carrier failure messages also display untranslated.
|
|
14
59
|
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
-----
|
|
63
|
+
|
|
64
|
+
<a id="further-exploration"></a>
|
|
65
|
+
## Further Exploration
|
|
66
|
+
|
|
67
|
+
These pages cover the composer host, the tool seam, and the plan-mode consumer.
|
|
68
|
+
|
|
69
|
+
- [ui-conversation](../ui-conversation/README.md) — the chat surface owning the `conversation.composer` chain.
|
|
70
|
+
- [tool-ask-user](../../interaction/tool-ask-user/README.md) — the model-facing tool whose schema and answers this UI renders.
|
|
71
|
+
- [ui-plan](../ui-plan/README.md) — the plan-mode surface that sets the `plan-review` intent.
|
|
72
|
+
- [user-questions](../../interaction/user-questions/README.md) — the Host-side question seam and its answerer waterfall.
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
<a id="model-experience"></a>
|
|
15
77
|
## Model Experience
|
|
16
78
|
|
|
17
|
-
Indirectly, through `dsh-tool-ask-user
|
|
79
|
+
Indirectly, through `dsh-tool-ask-user`, whose model-visible schema and answer rendering this package presents in the Web client.
|
|
18
80
|
|
|
19
81
|
#### KV Cache effect
|
|
20
82
|
|
|
@@ -22,5 +84,20 @@ No direct invalidation; `dsh-tool-ask-user` owns the model-visible tool call and
|
|
|
22
84
|
|
|
23
85
|
## Known Limitations and Deferred Work
|
|
24
86
|
|
|
25
|
-
|
|
87
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
These limits define draft durability and composer ownership; they are current package constraints.
|
|
91
|
+
|
|
92
|
+
- **Unsubmitted drafts have page-and-Session lifetime** — Session navigation preserves them while that Session scope remains in the page, but a full page reload, Session pruning, or a newly delivered pending-request identity starts with an empty draft. The store never writes them to the Host, `localStorage`, or disk.
|
|
26
93
|
- **One request owns the composer at a time** — later pending requests remain in the session snapshot and become visible after the earlier request resolves.
|
|
94
|
+
|
|
95
|
+
<a id="dev-note"></a>
|
|
96
|
+
### Dev Note
|
|
97
|
+
|
|
98
|
+
<details>
|
|
99
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
100
|
+
|
|
101
|
+
None.
|
|
102
|
+
|
|
103
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,26 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "dsh Web 客户端的 ask_user_question 功能:接管编辑器的提问 UI 与 plan-review 审批卡片。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-client-ui-user-questions
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-client-ui-user-questions` 是 Web 提问功能插件:其浏览器侧把 `question` 条目注册到会话拥有的 `conversation.composer` chain 中,因此当 agent 向用户提问时,编辑器会被提问 UI 接管。组件每次渲染一个问题,提供进度导航、单选与多选选项、推荐徽标与自定义答案,并为整个请求提交一批结构化答案。若某个请求的唯一问题声明了呈现意图,则改为渲染该意图自己的界面——最典型的是 `plan-review` 等待审批卡片,带 `Chat about it` / `Refuse` / `Approve`。其主机侧刻意为空:在那里挂载 `dsh-tool-ask-user` 会把工具放进注册表的全局层,并把它并入每一个 agent,无论它由哪个 preset 组装。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
当 agent 提问时,编辑器变成提问界面:回答每个问题、用翻页器导航,或跳过它。选择单选选项后会立即前进;Enter 继续流程,并在所有问题均已回答或跳过后提交,而 Shift+Enter 改为换行(IME 组合输入期间按 Enter 只会确认输入候选,不会前进)。
|
|
29
|
+
|
|
30
|
+
### 作答
|
|
31
|
+
|
|
32
|
+
用户打开或编辑自定义答案时,多选题草稿会保留已选中的标签,因此提交项可以同时携带 `selected` 与 `custom`;单选题的自定义答案仍保持互斥。问题详情复用助手输出的 `MarkdownText` 原语,包括其 GFM 渲染与不受信任内容策略。限高卡片保持标题、导航与提交动作固定,超长的详情与选项共享内部滚动区。「跳过此问题」会保留其他草稿,并为该项发出既有的空 `{ selected: [] }` 结果;关闭则以 `ASK_CANCELLED` 拒绝整个等待。
|
|
33
|
+
|
|
34
|
+
### plan-review 卡片
|
|
35
|
+
|
|
36
|
+
`plan-review` 意图——由 `dsh-plan-mode` 在 `exit_plan_mode` 审阅上设置——渲染等待审批卡片的布局:一条 `Plan review` 条带、计划作为可滚动的 markdown 主体,以及一行 `Chat about it` / `Refuse` / `Approve` 的决定操作。Approve 与 Refuse 用提问方自己的选项标签回答;`Chat about it` 以 `ASK_CANCELLED` 拒绝该等待,让编辑器归位,用户可以直接说出他想说的话。
|
|
37
|
+
|
|
38
|
+
### 失败与恢复
|
|
39
|
+
|
|
40
|
+
通用提问流程把当前题号、已选标签、自定义文本和显式跳过状态保存在非持久化 Slot store 中;该 store 归属对应 Session,并以待处理请求的本地渲染标识为 key。从 Session A 切换到 B 会重新挂载严格 Session 级编辑器条目,但返回 A 时会复用 A 的 store 并恢复未完成草稿。不同的请求标识读取空草稿,并在首次编辑时替换旧值;成功回答或取消会清除相符的值。请求是否仍在等待由主机保持权威。
|
|
41
|
+
|
|
42
|
+
-----
|
|
43
|
+
|
|
44
|
+
<a id="understand-the-implementation"></a>
|
|
45
|
+
## 理解实现
|
|
6
46
|
|
|
7
|
-
|
|
47
|
+
<details>
|
|
48
|
+
<summary>实现细节——点击展开</summary>
|
|
8
49
|
|
|
9
|
-
|
|
50
|
+
本包是一条归属规则:渲染提问是宿主的 UI 能力,拥有该工具则是 agent 的能力,因此 `tool-ask-user` 行属于需要它的各个 preset(以及没有 preset 的 TUI 组装)。
|
|
10
51
|
|
|
11
|
-
|
|
52
|
+
### 意图表面选举
|
|
53
|
+
|
|
54
|
+
卡片只在能够发出该请求允许的每一个答案时才接管:只有一个问题、声明了意图、计划以 `detail` 存在、提供了被指名的批准标签,且是二元单选(除批准外最多一个选项,且非多选)。其他任何情形都留在能够表达它的通用流程上。意图改变的只是布局,从不改变可达的答案。
|
|
55
|
+
|
|
56
|
+
### 文案与 locale
|
|
12
57
|
|
|
13
58
|
编辑器外框文案(翻页器、按钮、占位符、校验提示)是双语的:插件在 `dsh-client-locale` 的 `question` 命名空间下注册 zh/en 词典,并通过 inject face 把绑定的翻译函数和 locale 快照源交给该条目,因此切换语言会重新渲染已挂载的编辑器。问题与选项文本来自模型并原样渲染;载体失败消息也不经翻译直接显示。
|
|
14
59
|
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
-----
|
|
63
|
+
|
|
64
|
+
<a id="further-exploration"></a>
|
|
65
|
+
## 进一步探索
|
|
66
|
+
|
|
67
|
+
以下页面覆盖编辑器宿主、工具 seam 与 plan-mode 消费方。
|
|
68
|
+
|
|
69
|
+
- [ui-conversation](../ui-conversation/README.zh.md)——拥有 `conversation.composer` 链的聊天界面。
|
|
70
|
+
- [tool-ask-user](../../interaction/tool-ask-user/README.zh.md)——本 UI 所渲染其 schema 与答案的面向模型工具。
|
|
71
|
+
- [ui-plan](../ui-plan/README.zh.md)——设置 `plan-review` 意图的 plan-mode 界面。
|
|
72
|
+
- [user-questions](../../interaction/user-questions/README.zh.md)——Host 侧提问 seam 及其 answerer waterfall。
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
<a id="model-experience"></a>
|
|
15
77
|
## 模型体验
|
|
16
78
|
|
|
17
|
-
|
|
79
|
+
间接影响模型体验:本包在 Web 客户端呈现 `dsh-tool-ask-user` 所拥有的模型可见 schema 与答案渲染。
|
|
18
80
|
|
|
19
81
|
#### KV Cache 影响
|
|
20
82
|
|
|
21
83
|
不会直接失效;模型可见的工具调用与结果由 `dsh-tool-ask-user` 拥有。
|
|
22
84
|
|
|
23
|
-
##
|
|
85
|
+
## 已知限制与延期工作
|
|
24
86
|
|
|
25
|
-
-
|
|
87
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
这些限制定义草稿持久性与编辑器归属;它们是当前包约束。
|
|
91
|
+
|
|
92
|
+
- **未提交草稿的生命周期限于当前页面与 Session**:只要该 Session scope 仍留在页面内,Session 导航就会保留草稿;完整刷新页面、Session 被裁剪,或待处理请求以新的本地标识重新交付时,则从空草稿开始。store 从不把草稿写入主机、`localStorage` 或磁盘。
|
|
26
93
|
- **每次只有一个请求拥有编辑器**:后续待处理请求仍留在会话快照中,并在较早请求落定后显示。
|
|
94
|
+
|
|
95
|
+
<a id="dev-note"></a>
|
|
96
|
+
### 开发备注
|
|
97
|
+
|
|
98
|
+
<details>
|
|
99
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
100
|
+
|
|
101
|
+
无。
|
|
102
|
+
|
|
103
|
+
</details>
|
package/lib/client.js
CHANGED
|
@@ -4,25 +4,19 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
7
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
9
|
let react = require("react");
|
|
9
10
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
-
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
11
|
-
function r(e) {
|
|
12
|
-
var t, f, n = "";
|
|
13
|
-
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
14
|
-
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
15
|
-
var o = e.length;
|
|
16
|
-
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
17
|
-
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
18
|
-
return n;
|
|
19
|
-
}
|
|
20
|
-
function clsx() {
|
|
21
|
-
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
22
|
-
return n;
|
|
23
|
-
}
|
|
24
|
-
//#endregion
|
|
25
11
|
//#region lib/types/client/contract/slots.js
|
|
12
|
+
function settlePendingComposer(settle, failureMessage) {
|
|
13
|
+
try {
|
|
14
|
+
settle();
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return Promise.reject(error instanceof Error ? error : new Error(failureMessage, { cause: error }));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
26
20
|
/**
|
|
27
21
|
* Narrow a request to a renderable plan review, or return undefined to leave it
|
|
28
22
|
* to the generic question flow.
|
|
@@ -60,57 +54,156 @@ window.__ModuleLoader__.load({
|
|
|
60
54
|
...decline === void 0 ? {} : { decline }
|
|
61
55
|
};
|
|
62
56
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
let nextQuestionKey = 0;
|
|
58
|
+
/** Create a wire-preserved user-question rejection. */
|
|
59
|
+
function questionError(message, code) {
|
|
60
|
+
const error = new Error(message);
|
|
61
|
+
error.name = "UserQuestionError";
|
|
62
|
+
error.code = code;
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
/** One answerable Client presentation of a pending Host waterfall. */
|
|
70
66
|
var PendingQuestion = class {
|
|
71
|
-
|
|
67
|
+
sessionId;
|
|
68
|
+
/** Presentation discriminator used by Session pending-interaction consumers. */
|
|
69
|
+
kind;
|
|
70
|
+
/** Opaque render identity and request key for the Session-scoped draft store. */
|
|
71
|
+
key;
|
|
72
|
+
/** The request's question list. */
|
|
73
|
+
questions;
|
|
74
|
+
/** Result returned by the Remote Event listener to the Host waterfall. */
|
|
75
|
+
result;
|
|
76
|
+
#resolve;
|
|
77
|
+
#reject;
|
|
78
|
+
#signal;
|
|
79
|
+
#onAbort;
|
|
80
|
+
#delegated = Symbol("pending question delegated");
|
|
81
|
+
#settled = false;
|
|
72
82
|
/**
|
|
73
|
-
* @param
|
|
83
|
+
* @param sessionId - Agent/Session identity owning the scoped request.
|
|
84
|
+
* @param questions - complete question batch.
|
|
85
|
+
* @param signal - Host request and delivery lifetime.
|
|
74
86
|
*/
|
|
75
|
-
constructor(
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
constructor(sessionId, questions, signal) {
|
|
88
|
+
this.sessionId = sessionId;
|
|
89
|
+
nextQuestionKey += 1;
|
|
90
|
+
this.key = `question:${String(nextQuestionKey)}`;
|
|
91
|
+
this.questions = questions;
|
|
92
|
+
this.kind = planReviewOf(questions) === void 0 ? "question" : "plan-review";
|
|
93
|
+
const completion = Promise.withResolvers();
|
|
94
|
+
this.result = completion.promise;
|
|
95
|
+
this.#resolve = completion.resolve;
|
|
96
|
+
this.#reject = completion.reject;
|
|
97
|
+
this.#signal = signal;
|
|
98
|
+
if (signal === void 0) {
|
|
99
|
+
this.#onAbort = void 0;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const onAbort = () => {
|
|
103
|
+
this.abort(questionError("ask_user_question was aborted before the user answered", "ASK_ABORTED"));
|
|
104
|
+
};
|
|
105
|
+
this.#onAbort = onAbort;
|
|
106
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
107
|
+
if (signal.aborted) onAbort();
|
|
85
108
|
}
|
|
86
109
|
/**
|
|
87
|
-
*
|
|
110
|
+
* Resolve the Host waterfall with the whole answer batch.
|
|
88
111
|
* @param answer - complete structured answer batch.
|
|
89
112
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
answer(answer) {
|
|
114
|
+
return settlePendingComposer(() => {
|
|
115
|
+
this.finish(() => {
|
|
116
|
+
this.#resolve(answer);
|
|
117
|
+
});
|
|
118
|
+
}, "pending question settlement failed");
|
|
119
|
+
}
|
|
120
|
+
/** Delegate an unanswered request to the next waterfall listener. */
|
|
121
|
+
delegate() {
|
|
122
|
+
if (this.#settled) return;
|
|
123
|
+
this.finish(() => {
|
|
124
|
+
this.#reject(this.#delegated);
|
|
97
125
|
});
|
|
98
|
-
if (!receipt.accepted) throw new Error(`question response rejected: ${receipt.reason}`);
|
|
99
126
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Test whether a rejection requests waterfall delegation.
|
|
129
|
+
* @param reason - rejection received from {@link PendingQuestion.result}.
|
|
130
|
+
* @returns whether {@link PendingQuestion.delegate} produced it.
|
|
131
|
+
*/
|
|
132
|
+
isDelegation(reason) {
|
|
133
|
+
return reason === this.#delegated;
|
|
134
|
+
}
|
|
135
|
+
/** Reject the Host waterfall because the user closed the question. */
|
|
136
|
+
cancel() {
|
|
137
|
+
return settlePendingComposer(() => {
|
|
138
|
+
this.finish(() => {
|
|
139
|
+
this.#reject(questionError("the user cancelled ask_user_question", "ASK_CANCELLED"));
|
|
140
|
+
});
|
|
141
|
+
}, "pending question cancellation failed");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* End an unanswered presentation when its transport, scope, or plugin lifetime ends.
|
|
145
|
+
* @param reason - rejection exposed to the waiting Remote Event listener.
|
|
146
|
+
*/
|
|
147
|
+
abort(reason) {
|
|
148
|
+
if (this.#settled) return;
|
|
149
|
+
this.finish(() => {
|
|
150
|
+
this.#reject(reason);
|
|
109
151
|
});
|
|
110
|
-
|
|
152
|
+
}
|
|
153
|
+
finish(settle) {
|
|
154
|
+
if (this.#settled) throw new Error(`pending question ${this.key} is already settled`);
|
|
155
|
+
this.#settled = true;
|
|
156
|
+
if (this.#signal !== void 0 && this.#onAbort !== void 0) this.#signal.removeEventListener("abort", this.#onAbort);
|
|
157
|
+
settle();
|
|
111
158
|
}
|
|
112
159
|
};
|
|
113
160
|
//#endregion
|
|
161
|
+
//#region lib/types/client/draft-store.js
|
|
162
|
+
/**
|
|
163
|
+
* Session-scoped draft state for the generic question composer. The Slot
|
|
164
|
+
* registry owns store instances; this module exports only the factory so a
|
|
165
|
+
* plugin reload cannot reuse a module-global handle.
|
|
166
|
+
*/
|
|
167
|
+
const emptyProgress = () => ({
|
|
168
|
+
index: 0,
|
|
169
|
+
drafts: []
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* Declare the question composer's transient Session store.
|
|
173
|
+
* @returns a non-persisted store handle whose instance is owned by the Slot registry.
|
|
174
|
+
*/
|
|
175
|
+
function createQuestionDraftStore() {
|
|
176
|
+
return (0, _deepseek_ai_dsh_client_store.defineStore)({
|
|
177
|
+
init: () => ({ progress: emptyProgress() }),
|
|
178
|
+
actions: {
|
|
179
|
+
replace: (draft, requestKey, progress) => {
|
|
180
|
+
draft.requestKey = requestKey;
|
|
181
|
+
draft.progress = progress;
|
|
182
|
+
},
|
|
183
|
+
clear: (draft, requestKey) => {
|
|
184
|
+
if (draft.requestKey !== requestKey) return;
|
|
185
|
+
delete draft.requestKey;
|
|
186
|
+
draft.progress = emptyProgress();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
193
|
+
function r(e) {
|
|
194
|
+
var t, f, n = "";
|
|
195
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
196
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
197
|
+
var o = e.length;
|
|
198
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
199
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
200
|
+
return n;
|
|
201
|
+
}
|
|
202
|
+
function clsx() {
|
|
203
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
204
|
+
return n;
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
114
207
|
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css.mjs
|
|
115
208
|
const css$1 = ".LVzXQa_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.LVzXQa_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-state-warn-secondary);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.LVzXQa_card,.LVzXQa_card *{box-sizing:border-box}.LVzXQa_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);flex-shrink:0;align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.LVzXQa_dot{background:var(--dsw-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.LVzXQa_body{overscroll-behavior:contain;flex:auto;min-height:0;padding:12px 16px 4px;font-size:14px;line-height:22px;overflow-y:auto}.LVzXQa_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 12px;display:flex}.LVzXQa_feedback{min-height:16px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}.LVzXQa_actions{flex-shrink:0;align-items:center;gap:8px;display:flex}.LVzXQa_discuss{color:var(--dsw-alias-label-secondary);gap:6px}.LVzXQa_discuss:hover:not(:disabled){color:var(--dsw-alias-label-primary)}@media (width<=720px){.LVzXQa_card{border-radius:16px}.LVzXQa_body{padding:10px 12px 4px}.LVzXQa_footer{align-items:flex-end;padding:8px 12px 10px}}";
|
|
116
209
|
const tagId$1 = "@deepseek-ai/dsh-client-ui-user-questions/PlanReviewPanel.module.css";
|
|
@@ -151,6 +244,13 @@ window.__ModuleLoader__.load({
|
|
|
151
244
|
* @returns The plan-review takeover for this request.
|
|
152
245
|
*/
|
|
153
246
|
function PlanReviewPanel({ pending, review, t }) {
|
|
247
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
248
|
+
code: {
|
|
249
|
+
copyLabel: t("copy"),
|
|
250
|
+
copiedLabel: t("copied")
|
|
251
|
+
},
|
|
252
|
+
footnotes: t("markdown.footnotes")
|
|
253
|
+
}), [t]);
|
|
154
254
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
155
255
|
const [error, setError] = (0, react.useState)(null);
|
|
156
256
|
const settle = (send) => {
|
|
@@ -182,7 +282,10 @@ window.__ModuleLoader__.load({
|
|
|
182
282
|
(0, react_jsx_runtime.jsx)("div", {
|
|
183
283
|
className: PlanReviewPanel_module_css_default.body,
|
|
184
284
|
"data-plan-review-scroll": true,
|
|
185
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
285
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
286
|
+
text: review.plan,
|
|
287
|
+
labels: markdownLabels
|
|
288
|
+
})
|
|
186
289
|
}),
|
|
187
290
|
(0, react_jsx_runtime.jsxs)("div", {
|
|
188
291
|
className: PlanReviewPanel_module_css_default.footer,
|
|
@@ -309,7 +412,7 @@ window.__ModuleLoader__.load({
|
|
|
309
412
|
* Mirror and textarea MUST share font, line-height, padding and wrapping rules
|
|
310
413
|
* or the two heights diverge.
|
|
311
414
|
*
|
|
312
|
-
* @param props -
|
|
415
|
+
* @param props - visual variant, draft text, and the field's event handlers.
|
|
313
416
|
* @returns The mirrored auto-growing field.
|
|
314
417
|
*/
|
|
315
418
|
function AnswerField(props) {
|
|
@@ -333,38 +436,51 @@ window.__ModuleLoader__.load({
|
|
|
333
436
|
});
|
|
334
437
|
}
|
|
335
438
|
/**
|
|
336
|
-
* Composer takeover
|
|
337
|
-
*
|
|
439
|
+
* Composer takeover router. Generic-question drafts live in this entry's
|
|
440
|
+
* Session-scoped Slot store, keyed by the pending carrier, so a strict Session
|
|
441
|
+
* entry remount restores the same request without exposing it to another one.
|
|
338
442
|
*
|
|
339
|
-
* One takeover, two
|
|
340
|
-
* package renders
|
|
443
|
+
* One takeover, two presentations: a request that declares a presentation intent this
|
|
444
|
+
* package renders uses that presentation (a plan review is one decision over one
|
|
341
445
|
* plan, not a question set), and every other request takes the generic flow.
|
|
342
446
|
* The routing lives here, at the one entry that owns the composer seat, so
|
|
343
|
-
* neither
|
|
447
|
+
* neither presentation can claim a request the other is already rendering.
|
|
344
448
|
*
|
|
345
449
|
* @param props - the selector-matched pending question carrier plus the framework standard kit.
|
|
346
450
|
* @returns The question flow, or the intent's own surface, for this request.
|
|
347
451
|
*/
|
|
348
452
|
function QuestionComposer(props) {
|
|
349
|
-
const question =
|
|
453
|
+
const question = props.matched;
|
|
350
454
|
const review = (0, react.useMemo)(() => planReviewOf(question.questions), [question]);
|
|
351
455
|
return review === void 0 ? (0, react_jsx_runtime.jsx)(QuestionFlow, {
|
|
352
456
|
pending: question,
|
|
353
|
-
t: props.t
|
|
457
|
+
t: props.t,
|
|
458
|
+
useStore: props.useStore,
|
|
459
|
+
actions: props.actions
|
|
354
460
|
}, question.key) : (0, react_jsx_runtime.jsx)(PlanReviewPanel, {
|
|
355
461
|
pending: question,
|
|
356
462
|
review,
|
|
357
463
|
t: props.t
|
|
358
464
|
}, question.key);
|
|
359
465
|
}
|
|
360
|
-
function QuestionFlow({ pending, t }) {
|
|
466
|
+
function QuestionFlow({ pending, t, useStore, actions }) {
|
|
361
467
|
const questions = pending.questions;
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
468
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
469
|
+
code: {
|
|
470
|
+
copyLabel: t("copy"),
|
|
471
|
+
copiedLabel: t("copied")
|
|
472
|
+
},
|
|
473
|
+
footnotes: t("markdown.footnotes")
|
|
474
|
+
}), [t]);
|
|
475
|
+
const initialProgress = (0, react.useMemo)(() => ({
|
|
476
|
+
index: 0,
|
|
477
|
+
drafts: questions.map(() => ({
|
|
478
|
+
selected: [],
|
|
479
|
+
custom: "",
|
|
480
|
+
skipped: false
|
|
481
|
+
}))
|
|
482
|
+
}), [questions]);
|
|
483
|
+
const { index, drafts } = useStore((state) => state.requestKey === pending.key && state.progress.drafts.length === questions.length ? state.progress : void 0) ?? initialProgress;
|
|
368
484
|
const [busy, setBusy] = (0, react.useState)(null);
|
|
369
485
|
const [error, setError] = (0, react.useState)(null);
|
|
370
486
|
const [minimized, setMinimized] = (0, react.useState)(false);
|
|
@@ -372,16 +488,24 @@ window.__ModuleLoader__.load({
|
|
|
372
488
|
const question = questions[index];
|
|
373
489
|
const draft = drafts[index];
|
|
374
490
|
const hasOptions = (question.options?.length ?? 0) > 0;
|
|
491
|
+
const replaceProgress = (nextIndex, nextDrafts) => {
|
|
492
|
+
actions.replace(pending.key, {
|
|
493
|
+
index: nextIndex,
|
|
494
|
+
drafts: nextDrafts
|
|
495
|
+
});
|
|
496
|
+
};
|
|
375
497
|
const cancelFlow = () => {
|
|
376
498
|
setBusy("cancel");
|
|
377
499
|
setError(null);
|
|
378
|
-
pending.cancel().
|
|
500
|
+
pending.cancel().then(() => {
|
|
501
|
+
actions.clear(pending.key);
|
|
502
|
+
}).catch((cause) => {
|
|
379
503
|
setBusy(null);
|
|
380
504
|
setError({ text: cause instanceof Error ? cause.message : String(cause) });
|
|
381
505
|
});
|
|
382
506
|
};
|
|
383
|
-
const updateDraft = (update) => {
|
|
384
|
-
|
|
507
|
+
const updateDraft = (update, nextIndex = index) => {
|
|
508
|
+
replaceProgress(nextIndex, drafts.map((item, itemIndex) => itemIndex === index ? update(item) : item));
|
|
385
509
|
setError(null);
|
|
386
510
|
};
|
|
387
511
|
const choose = (label) => {
|
|
@@ -399,15 +523,14 @@ window.__ModuleLoader__.load({
|
|
|
399
523
|
custom: "",
|
|
400
524
|
skipped: false
|
|
401
525
|
};
|
|
402
|
-
});
|
|
403
|
-
if (question.multiSelect !== true && index < questions.length - 1) setIndex((current) => current + 1);
|
|
526
|
+
}, question.multiSelect !== true && index < questions.length - 1 ? index + 1 : index);
|
|
404
527
|
};
|
|
405
528
|
const answered = (item) => item.selected.length > 0 || item.custom.trim() !== "";
|
|
406
529
|
const completed = (item) => answered(item) || item.skipped;
|
|
407
530
|
const submitDrafts = (values) => {
|
|
408
531
|
const missing = values.findIndex((item) => !completed(item));
|
|
409
532
|
if (missing >= 0) {
|
|
410
|
-
|
|
533
|
+
replaceProgress(missing, values);
|
|
411
534
|
setError({ key: "error.incomplete" });
|
|
412
535
|
return;
|
|
413
536
|
}
|
|
@@ -426,7 +549,9 @@ window.__ModuleLoader__.load({
|
|
|
426
549
|
}) };
|
|
427
550
|
setBusy("answer");
|
|
428
551
|
setError(null);
|
|
429
|
-
pending.answer(answer).
|
|
552
|
+
pending.answer(answer).then(() => {
|
|
553
|
+
actions.clear(pending.key);
|
|
554
|
+
}).catch((cause) => {
|
|
430
555
|
setBusy(null);
|
|
431
556
|
setError({ text: cause instanceof Error ? cause.message : String(cause) });
|
|
432
557
|
});
|
|
@@ -437,7 +562,7 @@ window.__ModuleLoader__.load({
|
|
|
437
562
|
return;
|
|
438
563
|
}
|
|
439
564
|
if (index < questions.length - 1) {
|
|
440
|
-
|
|
565
|
+
replaceProgress(index + 1, drafts);
|
|
441
566
|
setError(null);
|
|
442
567
|
return;
|
|
443
568
|
}
|
|
@@ -463,12 +588,9 @@ window.__ModuleLoader__.load({
|
|
|
463
588
|
custom: "",
|
|
464
589
|
skipped: true
|
|
465
590
|
} : item);
|
|
466
|
-
|
|
591
|
+
replaceProgress(index < questions.length - 1 ? index + 1 : index, nextDrafts);
|
|
467
592
|
setError(null);
|
|
468
|
-
if (index < questions.length - 1)
|
|
469
|
-
setIndex((current) => current + 1);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
593
|
+
if (index < questions.length - 1) return;
|
|
472
594
|
submitDrafts(nextDrafts);
|
|
473
595
|
};
|
|
474
596
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -517,7 +639,10 @@ window.__ModuleLoader__.load({
|
|
|
517
639
|
"data-question-scroll": true,
|
|
518
640
|
children: [question.detail !== void 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
519
641
|
className: QuestionComposer_module_css_default.detail,
|
|
520
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
642
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
643
|
+
text: question.detail,
|
|
644
|
+
labels: markdownLabels
|
|
645
|
+
})
|
|
521
646
|
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
522
647
|
className: QuestionComposer_module_css_default.options,
|
|
523
648
|
role: question.multiSelect === true ? "group" : "radiogroup",
|
|
@@ -610,7 +735,7 @@ window.__ModuleLoader__.load({
|
|
|
610
735
|
"aria-label": t("nav.prev"),
|
|
611
736
|
disabled: index === 0 || busy !== null,
|
|
612
737
|
onClick: () => {
|
|
613
|
-
|
|
738
|
+
replaceProgress(index - 1, drafts);
|
|
614
739
|
setError(null);
|
|
615
740
|
},
|
|
616
741
|
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
|
|
@@ -629,7 +754,7 @@ window.__ModuleLoader__.load({
|
|
|
629
754
|
"aria-label": t("nav.next"),
|
|
630
755
|
disabled: index === questions.length - 1 || busy !== null,
|
|
631
756
|
onClick: () => {
|
|
632
|
-
|
|
757
|
+
replaceProgress(index + 1, drafts);
|
|
633
758
|
setError(null);
|
|
634
759
|
},
|
|
635
760
|
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
|
|
@@ -703,11 +828,35 @@ window.__ModuleLoader__.load({
|
|
|
703
828
|
//#region lib/types/client/index.js
|
|
704
829
|
/** Dictionary namespace owned by this plugin. */
|
|
705
830
|
const NS = "question";
|
|
706
|
-
/** Required services:
|
|
707
|
-
const inject = [
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
831
|
+
/** Required services: Agent scopes, Remote Events, Session UI, Slot registry, and copy. */
|
|
832
|
+
const inject = [
|
|
833
|
+
"sessions",
|
|
834
|
+
"remote",
|
|
835
|
+
"uiSession",
|
|
836
|
+
"slots",
|
|
837
|
+
"locale"
|
|
838
|
+
];
|
|
839
|
+
/** Present one request until the user answers, cancels, or its lifetime ends. */
|
|
840
|
+
async function answerQuestion(ctx, owner, request, next, registerPendingInteraction) {
|
|
841
|
+
const sessionId = ctx.sessions.scopeOf(owner);
|
|
842
|
+
if (sessionId === void 0) return next();
|
|
843
|
+
const pending = new PendingQuestion(sessionId, request.questions, request.signal);
|
|
844
|
+
const completed = Promise.withResolvers();
|
|
845
|
+
const remove = registerPendingInteraction(pending, async () => {
|
|
846
|
+
pending.delegate();
|
|
847
|
+
await completed.promise;
|
|
848
|
+
});
|
|
849
|
+
try {
|
|
850
|
+
try {
|
|
851
|
+
return await pending.result;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (pending.isDelegation(error)) return await next();
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
} finally {
|
|
857
|
+
remove();
|
|
858
|
+
completed.resolve();
|
|
859
|
+
}
|
|
711
860
|
}
|
|
712
861
|
/**
|
|
713
862
|
* Client plugin body: register the `question` dictionaries and the question
|
|
@@ -720,14 +869,19 @@ window.__ModuleLoader__.load({
|
|
|
720
869
|
zh,
|
|
721
870
|
en
|
|
722
871
|
}), "ui-user-questions: dictionaries");
|
|
872
|
+
const questionDraftStore = createQuestionDraftStore();
|
|
873
|
+
const registerPendingInteraction = ctx.uiSession.registerPendingInteraction((pending) => pending.kind === "plan-review" ? 2 : 1);
|
|
723
874
|
ctx.slots.inject("conversation.composer", () => ctx.slots.register({
|
|
724
875
|
name: "conversation.composer",
|
|
725
|
-
select:
|
|
726
|
-
locale: NS
|
|
876
|
+
select: ({ pendingInteraction }) => pendingInteraction instanceof PendingQuestion ? pendingInteraction : null,
|
|
877
|
+
locale: NS,
|
|
878
|
+
store: questionDraftStore
|
|
727
879
|
}, QuestionComposer));
|
|
880
|
+
ctx.remote.$on("user-questions/request", function(request, next) {
|
|
881
|
+
return answerQuestion(ctx, this, request, next, registerPendingInteraction);
|
|
882
|
+
});
|
|
728
883
|
}
|
|
729
884
|
//#endregion
|
|
730
|
-
exports.PendingQuestion = PendingQuestion;
|
|
731
885
|
exports.apply = apply;
|
|
732
886
|
exports.inject = inject;
|
|
733
887
|
return module.exports;
|
|
@@ -9,14 +9,15 @@ export declare function parseRecommendedLabel(label: string): {
|
|
|
9
9
|
recommended: boolean;
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
|
-
* Composer takeover
|
|
13
|
-
*
|
|
12
|
+
* Composer takeover router. Generic-question drafts live in this entry's
|
|
13
|
+
* Session-scoped Slot store, keyed by the pending carrier, so a strict Session
|
|
14
|
+
* entry remount restores the same request without exposing it to another one.
|
|
14
15
|
*
|
|
15
|
-
* One takeover, two
|
|
16
|
-
* package renders
|
|
16
|
+
* One takeover, two presentations: a request that declares a presentation intent this
|
|
17
|
+
* package renders uses that presentation (a plan review is one decision over one
|
|
17
18
|
* plan, not a question set), and every other request takes the generic flow.
|
|
18
19
|
* The routing lives here, at the one entry that owns the composer seat, so
|
|
19
|
-
* neither
|
|
20
|
+
* neither presentation can claim a request the other is already rendering.
|
|
20
21
|
*
|
|
21
22
|
* @param props - the selector-matched pending question carrier plus the framework standard kit.
|
|
22
23
|
* @returns The question flow, or the intent's own surface, for this request.
|
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/** The pending question carrier the owner dispatches into the composer slot. */
|
|
13
|
-
export type QuestionWait = PendingWait<'question'>;
|
|
1
|
+
/** Question composer props and one pending Remote waterfall response. */
|
|
2
|
+
import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
4
|
+
import type { AskUserQuestionAnswer, AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions';
|
|
5
|
+
import type { createQuestionDraftStore } from '../draft-store.ts';
|
|
6
|
+
declare module '@deepseek-ai/dsh-client-ui-session/client' {
|
|
7
|
+
interface SessionPendingInteractionMap {
|
|
8
|
+
/** Pending question or plan-review request. */
|
|
9
|
+
question: PendingQuestion;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
14
12
|
/** One structured answer batch covering every question of the request. */
|
|
15
|
-
export type QuestionAnswer =
|
|
16
|
-
/** One question of the request
|
|
17
|
-
type QuestionItem =
|
|
13
|
+
export type QuestionAnswer = AskUserQuestionAnswer;
|
|
14
|
+
/** One question of the request. */
|
|
15
|
+
type QuestionItem = AskUserQuestionItem;
|
|
18
16
|
/** One option the asker offered on a question. */
|
|
19
17
|
type QuestionOption = NonNullable<QuestionItem['options']>[number];
|
|
20
18
|
/**
|
|
21
19
|
* A request narrowed to the `plan-review` presentation intent: everything the
|
|
22
20
|
* decision card renders and answers with, so the panel never re-reads the
|
|
23
|
-
* request
|
|
21
|
+
* request fields. `approve` and `decline` are the asker's own options — an
|
|
24
22
|
* answer must carry one of those labels verbatim — and `plan` is the markdown
|
|
25
23
|
* body under review.
|
|
26
24
|
*/
|
|
@@ -55,31 +53,48 @@ export interface PlanReview {
|
|
|
55
53
|
* @returns The narrowed review, or undefined when the generic flow owns it.
|
|
56
54
|
*/
|
|
57
55
|
export declare function planReviewOf(questions: readonly QuestionItem[]): PlanReview | undefined;
|
|
58
|
-
/**
|
|
59
|
-
* Question domain face over the carrier: render identity and questions
|
|
60
|
-
* transparently forwarded; answer/cancel own the wire encoding (the success
|
|
61
|
-
* fields and the cancelled error) and turn a rejected carrier receipt into a
|
|
62
|
-
* thrown error. Components mint one per carrier via useMemo (never inside a
|
|
63
|
-
* select — a per-dispatch mint would churn identity and break memoization).
|
|
64
|
-
*/
|
|
56
|
+
/** One answerable Client presentation of a pending Host waterfall. */
|
|
65
57
|
export declare class PendingQuestion {
|
|
66
|
-
private
|
|
58
|
+
#private;
|
|
59
|
+
readonly sessionId: SessionId;
|
|
60
|
+
/** Presentation discriminator used by Session pending-interaction consumers. */
|
|
61
|
+
readonly kind: 'question' | 'plan-review';
|
|
62
|
+
/** Opaque render identity and request key for the Session-scoped draft store. */
|
|
63
|
+
readonly key: string;
|
|
64
|
+
/** The request's question list. */
|
|
65
|
+
readonly questions: readonly AskUserQuestionItem[];
|
|
66
|
+
/** Result returned by the Remote Event listener to the Host waterfall. */
|
|
67
|
+
readonly result: Promise<QuestionAnswer>;
|
|
67
68
|
/**
|
|
68
|
-
* @param
|
|
69
|
+
* @param sessionId - Agent/Session identity owning the scoped request.
|
|
70
|
+
* @param questions - complete question batch.
|
|
71
|
+
* @param signal - Host request and delivery lifetime.
|
|
69
72
|
*/
|
|
70
|
-
constructor(
|
|
71
|
-
/** Opaque render identity (React key / draft remount axis), forwarded from the carrier. */
|
|
72
|
-
get key(): string;
|
|
73
|
-
/** The request's question list, forwarded from the carrier payload. */
|
|
74
|
-
get questions(): QuestionWait['payload']['questions'];
|
|
73
|
+
constructor(sessionId: SessionId, questions: readonly AskUserQuestionItem[], signal?: AbortSignal);
|
|
75
74
|
/**
|
|
76
|
-
*
|
|
75
|
+
* Resolve the Host waterfall with the whole answer batch.
|
|
77
76
|
* @param answer - complete structured answer batch.
|
|
78
77
|
*/
|
|
79
78
|
answer(answer: QuestionAnswer): Promise<void>;
|
|
80
|
-
/**
|
|
79
|
+
/** Delegate an unanswered request to the next waterfall listener. */
|
|
80
|
+
delegate(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Test whether a rejection requests waterfall delegation.
|
|
83
|
+
* @param reason - rejection received from {@link PendingQuestion.result}.
|
|
84
|
+
* @returns whether {@link PendingQuestion.delegate} produced it.
|
|
85
|
+
*/
|
|
86
|
+
isDelegation(reason: unknown): boolean;
|
|
87
|
+
/** Reject the Host waterfall because the user closed the question. */
|
|
81
88
|
cancel(): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* End an unanswered presentation when its transport, scope, or plugin lifetime ends.
|
|
91
|
+
* @param reason - rejection exposed to the waiting Remote Event listener.
|
|
92
|
+
*/
|
|
93
|
+
abort(reason: unknown): void;
|
|
94
|
+
private finish;
|
|
82
95
|
}
|
|
96
|
+
/** Pending value returned by the composer-chain selector. */
|
|
97
|
+
export type QuestionWait = PendingQuestion;
|
|
83
98
|
/**
|
|
84
99
|
* Full component props: the framework runtime share (chain currency +
|
|
85
100
|
* session/global standard kit) plus the chain `matched` share — the entry's
|
|
@@ -87,7 +102,7 @@ export declare class PendingQuestion {
|
|
|
87
102
|
* standard locale seat; the carrier plus the domain face above carry the
|
|
88
103
|
* whole behavior surface.
|
|
89
104
|
*/
|
|
90
|
-
export type QuestionComposerProps = PropsRuntime<'conversation.composer'> & {
|
|
105
|
+
export type QuestionComposerProps = PropsRuntime<'conversation.composer'> & PropsStore<ReturnType<typeof createQuestionDraftStore>> & {
|
|
91
106
|
matched: QuestionWait;
|
|
92
107
|
} & PropsLocale<'question'>;
|
|
93
108
|
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped draft state for the generic question composer. The Slot
|
|
3
|
+
* registry owns store instances; this module exports only the factory so a
|
|
4
|
+
* plugin reload cannot reuse a module-global handle.
|
|
5
|
+
*/
|
|
6
|
+
import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
|
|
7
|
+
/** One in-progress answer, including an explicit skip. */
|
|
8
|
+
export interface QuestionDraftAnswer {
|
|
9
|
+
/** Offered labels currently selected. */
|
|
10
|
+
selected: string[];
|
|
11
|
+
/** Human-authored alternative or additional answer. */
|
|
12
|
+
custom: string;
|
|
13
|
+
/** Whether the user explicitly skipped this question. */
|
|
14
|
+
skipped: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Navigation and answer drafts for one pending request. */
|
|
17
|
+
export interface QuestionDraftProgress {
|
|
18
|
+
/** Current question index. */
|
|
19
|
+
index: number;
|
|
20
|
+
/** One draft per question, in request order. */
|
|
21
|
+
drafts: QuestionDraftAnswer[];
|
|
22
|
+
}
|
|
23
|
+
interface QuestionDraftState {
|
|
24
|
+
requestKey?: string;
|
|
25
|
+
progress: QuestionDraftProgress;
|
|
26
|
+
}
|
|
27
|
+
type QuestionDraftActions = {
|
|
28
|
+
replace: (draft: QuestionDraftState, requestKey: string, progress: QuestionDraftProgress) => void;
|
|
29
|
+
clear: (draft: QuestionDraftState, requestKey: string) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Declare the question composer's transient Session store.
|
|
33
|
+
* @returns a non-persisted store handle whose instance is owned by the Slot registry.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createQuestionDraftStore(): EngineStoreHandle<QuestionDraftState, QuestionDraftActions>;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=draft-store.d.ts.map
|
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
* separate chain entry per shape would race the same carrier, so the shape
|
|
13
13
|
* choice lives inside this entry — see QuestionComposer.
|
|
14
14
|
*/
|
|
15
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
15
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
16
16
|
import { type QuestionKey } from './locales.ts';
|
|
17
|
-
export { PendingQuestion } from './contract/slots.ts';
|
|
18
|
-
export type { PlanReview, QuestionAnswer, QuestionComposerProps, QuestionWait, } from './contract/slots.ts';
|
|
17
|
+
export type { PendingQuestion, PlanReview, QuestionAnswer, QuestionComposerProps, QuestionWait, } from './contract/slots.ts';
|
|
19
18
|
export type { QuestionKey } from './locales.ts';
|
|
20
19
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
21
20
|
interface LocaleNamespaceMap {
|
|
@@ -23,7 +22,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
23
22
|
question: QuestionKey;
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
|
-
/** Required services:
|
|
25
|
+
/** Required services: Agent scopes, Remote Events, Session UI, Slot registry, and copy. */
|
|
27
26
|
export declare const inject: string[];
|
|
28
27
|
/**
|
|
29
28
|
* Client plugin body: register the `question` dictionaries and the question
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-user-questions",
|
|
3
|
-
"description": "Web ask_user_question
|
|
4
|
-
"version": "0.1.
|
|
3
|
+
"description": "Web ask_user_question composer takeover and plan-review presentation UI",
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,8 +32,12 @@
|
|
|
32
32
|
"dsh": {
|
|
33
33
|
"client": {
|
|
34
34
|
"inject": [
|
|
35
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
36
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
35
37
|
"@deepseek-ai/dsh-client-locale",
|
|
36
|
-
"@deepseek-ai/dsh-client-ui-conversation"
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-session"
|
|
37
41
|
],
|
|
38
42
|
"platform": "web"
|
|
39
43
|
}
|
|
@@ -43,29 +47,29 @@
|
|
|
43
47
|
"clsx": "^2.0.0"
|
|
44
48
|
},
|
|
45
49
|
"peerDependencies": {
|
|
46
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
47
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
|
|
48
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
49
|
-
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
50
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2"
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
52
51
|
},
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@types/react": "~18.3.1",
|
|
55
54
|
"react": "^18.2.0",
|
|
56
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
57
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-client-
|
|
66
|
-
"@deepseek-ai/dsh-client-
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
56
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-user-questions": "^0.1.2-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.2-alpha.2",
|
|
68
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
|
|
70
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
|
|
71
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
72
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
|
|
69
73
|
},
|
|
70
74
|
"files": [
|
|
71
75
|
"lib/index.js",
|