@deepseek-ai/dsh-client-ui-message-feedback 0.0.1-rc.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 ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/client/ui-message-feedback/README.md
5
+ README.md: 461e87589567eb95b075839d5893cc551e67a035
6
+ README.zh.md: 31f722021ff65f476a6ba1ab0211fd4e091671d2
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @deepseek-ai/dsh-client-ui-message-feedback
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Per-message feedback plugin, browser half: a Like/Dislike pair plus an optional note, contributed as the `feedback` entry (order 10) of the `conversation.chat.assistant-actions` strip. The strip is declared by `ui-conversation` and rendered inside the finalized assistant message's IconActions row, between copy and branch, so the controls inherit that row's chrome and hover behavior. Only finalized messages reach the slot — an interruption-frozen partial carries no `messageId` and therefore no feedback controls. The strip renders once per turn, on the closing assistant message that owns the turn's IconActions row: earlier steps of a multi-step turn produce tool rows rather than a rateable body, so they present no controls even though the Host would accept them as targets.
6
+
7
+ One `MessageFeedbackController` per Session backs every message control in that Session, so a single `messageFeedback.list` read seeds the whole transcript. The read is deferred to the first hover or focus rather than fired on mount, because the controls mount once per settled message in the visible history.
8
+
9
+ Mutations go through `ctx.remote.messageFeedback`; the Host owns per-item compare-and-set. Every `put` and `delete` carries the `version` this controller last observed, and a `version-conflict` reply carries the authoritative item, so a lost race reconciles from the reply itself instead of refetching the Session. Mutations serialize per Session, so a queued operation always compares against the committed version. Re-clicking the recorded rating retracts the feedback; switching sides carries the existing note forward.
10
+
11
+ The `/client` exports are the plugin body (`apply`/`inject`), the `MessageFeedbackActions` component, the `MessageFeedbackController` class, and the injected face types.
12
+
13
+ ## Model Experience
14
+
15
+ None, as feedback is a sidecar that never enters the append-only Session log, the model context, or telemetry; no rating or note is ever visible to the model.
16
+
17
+ #### KV Cache effect
18
+
19
+ None; no feedback mutation touches the history tail.
20
+
21
+ ## Known Limitations and Deferred Work
22
+
23
+ - **Note size is a Host policy** — the deployment configures `maxNoteBytes` (8192 in the Web bundle) and the Host rejects an oversized note with `note-too-large`. The editor does not pre-check the limit, so an oversized note fails on save rather than while typing.
24
+ - **No cross-tab push** — a second tab's rating becomes visible on reconnect or on the next conflict reply, not immediately; the sidecar publishes no live frames.
25
+ - **Chat view only** — the trajectory and waterfall views render no feedback controls even though their assistant nodes now carry the same `messageId`.
package/README.zh.md ADDED
@@ -0,0 +1,25 @@
1
+ # @deepseek-ai/dsh-client-ui-message-feedback
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 单条消息反馈插件的浏览器侧:一对 Like/Dislike 按钮加一个可选备注,作为 `conversation.chat.assistant-actions` 条带的 `feedback` 条目(order 10)贡献。该条带由 `ui-conversation` 声明,渲染在已定稿助手消息的 IconActions 行内、复制与分支之间,因此控件沿用该行的样式与 hover 行为。只有已定稿的消息能到达这个 slot——被中断冻结的部分输出不带 `messageId`,因此也没有反馈控件。该操作栏每个 Turn 渲染一次,位于持有该 Turn IconActions 行的收尾助手消息上:多步骤 Turn 中较早的步骤产出的是工具行而非可评分正文,因此即使 Host 会接受它们作为目标,界面上也不出现控件。
6
+
7
+ 每个 Session 一个 `MessageFeedbackController`,支撑该 Session 内所有消息的控件,因此一次 `messageFeedback.list` 读取即可填充整段对话。该读取延迟到首次 hover 或 focus 才发起,而不是在挂载时触发,因为可见历史中每条已结束的消息都会挂载一次控件。
8
+
9
+ 变更通过 `ctx.remote.messageFeedback` 提交,按条目的 compare-and-set 由 Host 负责。每次 `put` 和 `delete` 都携带本 controller 最后观察到的 `version`;`version-conflict` 响应会带回权威条目,因此竞争失败时直接用该响应对账,无需重新拉取整个 Session。变更按 Session 串行,排队中的操作总是与已提交的版本比较。再次点击已记录的评分会撤回反馈;切换到另一侧会保留已有备注。
10
+
11
+ `/client` 导出插件本体(`apply`/`inject`)、`MessageFeedbackActions` 组件、`MessageFeedbackController` 类以及注入面类型。
12
+
13
+ ## 模型体验
14
+
15
+ 无。反馈是 sidecar,不进入 append-only 的 Session 日志、模型上下文或遥测;任何评分与备注对模型都不可见。
16
+
17
+ #### KV Cache 影响
18
+
19
+ 无;任何反馈变更都不触碰历史尾部。
20
+
21
+ ## 已知限制与暂缓事项
22
+
23
+ - **备注大小是 Host 策略** —— 部署方配置 `maxNoteBytes`(Web bundle 中为 8192),超长备注由 Host 以 `note-too-large` 拒绝。编辑器不预先校验该上限,因此超长备注在保存时才失败,而不是在输入过程中。
24
+ - **无跨标签页推送** —— 另一个标签页的评分要等到重连或下一次冲突响应才可见,不会立即出现;该 sidecar 不发布实时帧。
25
+ - **仅限对话视图** —— trajectory 与 waterfall 视图不渲染反馈控件,尽管它们的助手节点现在也带有相同的 `messageId`。
package/lib/client.js ADDED
@@ -0,0 +1,611 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@deepseek-ai/dsh-client-ui-message-feedback",
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 _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region lib/types/client/controller.js
11
+ const INITIAL_VIEW = Object.freeze({
12
+ status: "cold",
13
+ items: /* @__PURE__ */ new Map(),
14
+ error: null
15
+ });
16
+ const OK = Object.freeze({ ok: true });
17
+ const DISPOSED = Object.freeze({
18
+ ok: false,
19
+ error: Object.freeze({
20
+ code: "disposed",
21
+ message: "feedback controller is disposed"
22
+ })
23
+ });
24
+ /** Human-readable text for one business failure code. */
25
+ function describe(code) {
26
+ switch (code) {
27
+ case "session-not-found": return "this session is no longer persisted";
28
+ case "target-not-found": return "this message is not a persisted assistant message";
29
+ case "version-conflict": return "feedback changed elsewhere";
30
+ case "note-blank": return "a note must contain a non-whitespace character";
31
+ case "note-too-large": return "the note is too long";
32
+ default: return code;
33
+ }
34
+ }
35
+ /** Build the rejected branch for one business failure code. */
36
+ function fail(code) {
37
+ return {
38
+ ok: false,
39
+ error: {
40
+ code,
41
+ message: describe(code)
42
+ }
43
+ };
44
+ }
45
+ /** Carrier failure rendered with the Host-supplied code and message. */
46
+ function carrierFailure(error) {
47
+ return {
48
+ ok: false,
49
+ error: {
50
+ code: error.code,
51
+ message: error.message
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * Per-session feedback object layer. One instance backs every per-message
57
+ * control in that Session, so a single list read seeds them all.
58
+ */
59
+ var MessageFeedbackController = class {
60
+ remote;
61
+ sessionId;
62
+ view = INITIAL_VIEW;
63
+ listeners = /* @__PURE__ */ new Set();
64
+ loadPromise = null;
65
+ operationTail = Promise.resolve();
66
+ disposed = false;
67
+ /**
68
+ * @param remote - the messageFeedback Remote namespace.
69
+ * @param sessionId - Session owning every addressed assistant message.
70
+ */
71
+ constructor(remote, sessionId) {
72
+ this.remote = remote;
73
+ this.sessionId = sessionId;
74
+ }
75
+ /** Return the cached immutable view. */
76
+ getSnapshot = () => this.view;
77
+ /** Subscribe to view replacement. */
78
+ subscribe = (listener) => {
79
+ this.listeners.add(listener);
80
+ return () => {
81
+ this.listeners.delete(listener);
82
+ };
83
+ };
84
+ /**
85
+ * Load once; a failed load stays retryable.
86
+ * @returns the settled load result, shared by concurrent callers.
87
+ */
88
+ ensure() {
89
+ if (this.view.status === "ready") return Promise.resolve(OK);
90
+ return this.refresh();
91
+ }
92
+ /**
93
+ * Re-read the authoritative list, collapsing concurrent callers onto one
94
+ * in-flight read.
95
+ *
96
+ * This is the unserialized read used to seed a cold controller, where no
97
+ * mutation can be in flight yet. A reconnect must use {@link resync} instead:
98
+ * an unserialized list response can otherwise arrive after a newer mutation's
99
+ * reply and overwrite the version that mutation just committed.
100
+ * @returns the settled reload result.
101
+ */
102
+ refresh() {
103
+ if (this.loadPromise !== null) return this.loadPromise;
104
+ this.publish({
105
+ status: "loading",
106
+ items: this.view.items,
107
+ error: null
108
+ });
109
+ const pending = this.load();
110
+ this.loadPromise = pending;
111
+ return pending.finally(() => {
112
+ this.loadPromise = null;
113
+ });
114
+ }
115
+ /**
116
+ * Re-read the list behind this Session's queued mutations, so a reconnect
117
+ * cannot resurrect a version an in-flight mutation already replaced.
118
+ * @returns the settled reload result.
119
+ */
120
+ resync() {
121
+ return this.mutate(() => this.refresh(), { seed: false });
122
+ }
123
+ /**
124
+ * Create or replace feedback for one message, comparing against the version
125
+ * this controller last observed.
126
+ *
127
+ * The note is resolved here rather than by the caller: `mutate` awaits the
128
+ * one list read first, so this body always sees the committed item, while a
129
+ * control that rendered before that read completed would still be holding
130
+ * `undefined`. Omitting `note` therefore keeps whatever is stored; only
131
+ * {@link clearNote} removes one.
132
+ * @param messageId - target assistant message.
133
+ * @param rating - desired judgment.
134
+ * @param note - replacement explanation; omitted keeps the stored note.
135
+ * @returns the settled mutation result.
136
+ */
137
+ rate(messageId, rating, note) {
138
+ return this.mutate(async () => {
139
+ const observed = this.view.items.get(messageId);
140
+ return await this.putCommitted(messageId, rating, note ?? observed?.note, observed);
141
+ });
142
+ }
143
+ /**
144
+ * Replace one message's rating with the opposite judgment, or retract it when
145
+ * the committed rating already matches. The decision reads the committed item
146
+ * inside the serialized mutation, so a click that lands before the first list
147
+ * read still toggles against the stored value rather than the empty view a
148
+ * cold control rendered.
149
+ * @param messageId - target assistant message.
150
+ * @param rating - the judgment the human asked for.
151
+ * @returns the settled mutation result.
152
+ */
153
+ toggle(messageId, rating) {
154
+ return this.mutate(async () => {
155
+ const observed = this.view.items.get(messageId);
156
+ if (observed?.rating === rating) return await this.deleteCommitted(messageId, observed);
157
+ return await this.putCommitted(messageId, rating, observed?.note, observed);
158
+ });
159
+ }
160
+ /**
161
+ * Drop the note while keeping the rating. Absent feedback needs no call.
162
+ * @param messageId - target assistant message.
163
+ * @returns the settled mutation result.
164
+ */
165
+ clearNote(messageId) {
166
+ return this.mutate(async () => {
167
+ const observed = this.view.items.get(messageId);
168
+ if (observed === void 0 || observed.note === void 0) return OK;
169
+ return await this.putCommitted(messageId, observed.rating, void 0, observed);
170
+ });
171
+ }
172
+ /**
173
+ * Remove feedback for one message. A message with no known item is already
174
+ * in the requested state, so no call is made.
175
+ * @param messageId - target assistant message.
176
+ * @returns the settled mutation result.
177
+ */
178
+ clear(messageId) {
179
+ return this.mutate(async () => {
180
+ const observed = this.view.items.get(messageId);
181
+ if (observed === void 0) return OK;
182
+ return await this.deleteCommitted(messageId, observed);
183
+ });
184
+ }
185
+ /** Commit one put against the observed version and reconcile a conflict. */
186
+ async putCommitted(messageId, rating, note, observed) {
187
+ const carried = await this.remote.put({
188
+ sessionId: this.sessionId,
189
+ messageId,
190
+ rating,
191
+ ...note === void 0 ? {} : { note },
192
+ ifVersion: observed?.version ?? null
193
+ });
194
+ if (!carried.ok) return carrierFailure(carried.error);
195
+ const result = carried.value;
196
+ if (result.ok) {
197
+ this.commit(messageId, result.value);
198
+ return OK;
199
+ }
200
+ if (result.error.code === "version-conflict") this.commit(messageId, result.error.current);
201
+ return fail(result.error.code);
202
+ }
203
+ /** Commit one delete against the observed version and reconcile a conflict. */
204
+ async deleteCommitted(messageId, observed) {
205
+ const carried = await this.remote.delete({
206
+ sessionId: this.sessionId,
207
+ messageId,
208
+ ifVersion: observed.version
209
+ });
210
+ if (!carried.ok) return carrierFailure(carried.error);
211
+ const result = carried.value;
212
+ if (result.ok) {
213
+ this.commit(messageId, null);
214
+ return OK;
215
+ }
216
+ if (result.error.code === "version-conflict") this.commit(messageId, result.error.current);
217
+ return fail(result.error.code);
218
+ }
219
+ /** Drop subscribers and refuse further work when the owning fiber unloads. */
220
+ dispose() {
221
+ this.disposed = true;
222
+ this.listeners.clear();
223
+ }
224
+ /** Fetch the whole sidecar and publish it as the seeded view. */
225
+ async load() {
226
+ try {
227
+ const carried = await this.remote.list({ sessionId: this.sessionId });
228
+ if (this.disposed) return OK;
229
+ if (!carried.ok) {
230
+ this.publish({
231
+ status: "error",
232
+ items: this.view.items,
233
+ error: carried.error.message
234
+ });
235
+ return carrierFailure(carried.error);
236
+ }
237
+ const result = carried.value;
238
+ if (!result.ok) {
239
+ this.publish({
240
+ status: "error",
241
+ items: this.view.items,
242
+ error: describe(result.error.code)
243
+ });
244
+ return fail(result.error.code);
245
+ }
246
+ const items = /* @__PURE__ */ new Map();
247
+ for (const item of result.value.items) items.set(item.messageId, item);
248
+ this.publish({
249
+ status: "ready",
250
+ items,
251
+ error: null
252
+ });
253
+ return OK;
254
+ } catch (error) {
255
+ if (this.disposed) return OK;
256
+ const message = error instanceof Error ? error.message : "message feedback list failed";
257
+ this.publish({
258
+ status: "error",
259
+ items: this.view.items,
260
+ error: message
261
+ });
262
+ return {
263
+ ok: false,
264
+ error: {
265
+ code: "transport",
266
+ message
267
+ }
268
+ };
269
+ }
270
+ }
271
+ /**
272
+ * Serialize one mutation behind this Session's prior mutation so queued
273
+ * operations always compare against the committed version, and translate a
274
+ * transport throw into the same settled shape the controls already render.
275
+ */
276
+ mutate(operation, options = {}) {
277
+ const guarded = async () => {
278
+ if (this.disposed) return DISPOSED;
279
+ if (options.seed !== false) {
280
+ const loaded = await this.ensure();
281
+ if (!loaded.ok) return loaded;
282
+ if (this.disposed) return DISPOSED;
283
+ }
284
+ try {
285
+ return await operation();
286
+ } catch (error) {
287
+ return {
288
+ ok: false,
289
+ error: {
290
+ code: "transport",
291
+ message: error instanceof Error ? error.message : "message feedback mutation failed"
292
+ }
293
+ };
294
+ }
295
+ };
296
+ const result = this.operationTail.then(guarded, guarded);
297
+ this.operationTail = result.then(() => void 0);
298
+ return result;
299
+ }
300
+ /**
301
+ * Replace one message's entry, keeping every other entry's identity. Only a
302
+ * `mutate` operation reaches this, and `mutate` refuses admission once the
303
+ * controller is disposed, so no disposal guard belongs here; `publish` is
304
+ * the single place that stops notifying after listeners are dropped.
305
+ */
306
+ commit(messageId, item) {
307
+ const items = new Map(this.view.items);
308
+ if (item === null) items.delete(messageId);
309
+ else items.set(messageId, item);
310
+ this.publish({
311
+ status: "ready",
312
+ items,
313
+ error: null
314
+ });
315
+ }
316
+ /** Replace the view and contain subscriber failures at the observable boundary. */
317
+ publish(view) {
318
+ this.view = Object.freeze(view);
319
+ for (const listener of this.listeners) try {
320
+ listener();
321
+ } catch (error) {
322
+ console.error("[ui-message-feedback] subscriber threw:", error);
323
+ }
324
+ }
325
+ };
326
+ //#endregion
327
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css.mjs
328
+ const css = "._8_XoUG_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}._8_XoUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_action:disabled{cursor:default;opacity:.4}._8_XoUG_action[data-active]{color:var(--dsw-alias-label-primary)}._8_XoUG_noteOpen{max-width:220px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;background:0 0;border:none;border-radius:14px;padding:0 8px;font-size:13px;line-height:28px;overflow:hidden}._8_XoUG_noteOpen:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_noteEditor{align-items:flex-start;gap:6px;display:inline-flex}._8_XoUG_noteInput{border:1px solid var(--dsw-alias-border-secondary);background:var(--dsw-alias-bg-primary);width:260px;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;border-radius:8px;padding:6px 8px;font-size:13px}._8_XoUG_noteSave,._8_XoUG_noteCancel{cursor:pointer;border:none;border-radius:14px;height:28px;padding:0 10px;font-size:13px}._8_XoUG_noteSave{background:var(--dsw-alias-interactive-bg-primary);color:var(--dsw-alias-label-inverse)}._8_XoUG_noteSave:disabled{cursor:default;opacity:.4}._8_XoUG_noteCancel{color:var(--dsw-alias-label-tertiary);background:0 0}._8_XoUG_noteCancel:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_failure{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:28px}";
329
+ const tagId = "@deepseek-ai/dsh-client-ui-message-feedback/MessageFeedbackActions.module.css";
330
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
331
+ const tag = document.createElement("style");
332
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-message-feedback";
333
+ tag.dataset.pluginCss = tagId;
334
+ tag.textContent = css;
335
+ document.head.appendChild(tag);
336
+ }
337
+ var MessageFeedbackActions_module_css_default = {
338
+ "action": "_8_XoUG_action",
339
+ "noteOpen": "_8_XoUG_noteOpen",
340
+ "noteEditor": "_8_XoUG_noteEditor",
341
+ "noteCancel": "_8_XoUG_noteCancel",
342
+ "failure": "_8_XoUG_failure",
343
+ "noteInput": "_8_XoUG_noteInput",
344
+ "noteSave": "_8_XoUG_noteSave"
345
+ };
346
+ //#endregion
347
+ //#region lib/types/client/MessageFeedbackActions.js
348
+ /**
349
+ * Per-message feedback controls: a Like/Dislike pair plus an optional note.
350
+ * Rendered inside the assistant message's IconActions row, so the buttons
351
+ * reuse that row's chrome and sit between copy and branch.
352
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/MessageFeedbackActions
353
+ */
354
+ /**
355
+ * One message's feedback controls.
356
+ * @param props - the owner's message identity, the injected verbs, and the
357
+ * shared feedback hook.
358
+ * @returns the rating buttons, plus the note editor while it is open.
359
+ */
360
+ function MessageFeedbackActions({ messageId, ensure, rate, toggle, clearNote, useFeedback, t }) {
361
+ const item = useFeedback((view) => view.items.get(messageId));
362
+ const loadFailed = useFeedback((view) => view.status === "error");
363
+ const rating = item?.rating;
364
+ const [noteOpen, setNoteOpen] = (0, react.useState)(false);
365
+ const [draft, setDraft] = (0, react.useState)("");
366
+ const [pending, setPending] = (0, react.useState)(false);
367
+ const [failure, setFailure] = (0, react.useState)(null);
368
+ const seeded = (0, react.useRef)(false);
369
+ const seed = (0, react.useCallback)(() => {
370
+ if (seeded.current) return;
371
+ seeded.current = true;
372
+ ensure();
373
+ }, [ensure]);
374
+ const alive = (0, react.useRef)(true);
375
+ (0, react.useEffect)(() => () => {
376
+ alive.current = false;
377
+ }, []);
378
+ const settle = (0, react.useCallback)((result) => {
379
+ if (!alive.current) return;
380
+ setPending(false);
381
+ if (result.ok) {
382
+ setFailure(null);
383
+ return;
384
+ }
385
+ setFailure(result.error?.code === "version-conflict" ? t("error.conflict") : t("error.generic"));
386
+ }, [t]);
387
+ const onRate = (0, react.useCallback)((next) => {
388
+ setPending(true);
389
+ setFailure(null);
390
+ setNoteOpen(false);
391
+ toggle(messageId, next).then(settle);
392
+ }, [
393
+ messageId,
394
+ settle,
395
+ toggle
396
+ ]);
397
+ const onSaveNote = (0, react.useCallback)((current) => {
398
+ const trimmed = draft.trim();
399
+ setPending(true);
400
+ setFailure(null);
401
+ (trimmed.length === 0 ? clearNote(messageId) : rate(messageId, current, trimmed)).then((result) => {
402
+ settle(result);
403
+ if (result.ok && alive.current) setNoteOpen(false);
404
+ });
405
+ }, [
406
+ clearNote,
407
+ draft,
408
+ messageId,
409
+ rate,
410
+ settle
411
+ ]);
412
+ const openNote = (0, react.useCallback)(() => {
413
+ setDraft(item?.note ?? "");
414
+ setNoteOpen(true);
415
+ }, [item?.note]);
416
+ const likeLabel = rating === "positive" ? t("action.likeActive") : t("action.like");
417
+ const dislikeLabel = rating === "negative" ? t("action.dislikeActive") : t("action.dislike");
418
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
419
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
420
+ label: likeLabel,
421
+ side: "bottom",
422
+ children: (0, react_jsx_runtime.jsx)("button", {
423
+ type: "button",
424
+ className: MessageFeedbackActions_module_css_default.action,
425
+ "aria-label": likeLabel,
426
+ "aria-pressed": rating === "positive",
427
+ "data-active": rating === "positive" || void 0,
428
+ disabled: pending,
429
+ onFocus: seed,
430
+ onPointerEnter: seed,
431
+ onClick: () => {
432
+ onRate("positive");
433
+ },
434
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLikeOutline16, {})
435
+ })
436
+ }),
437
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
438
+ label: dislikeLabel,
439
+ side: "bottom",
440
+ children: (0, react_jsx_runtime.jsx)("button", {
441
+ type: "button",
442
+ className: MessageFeedbackActions_module_css_default.action,
443
+ "aria-label": dislikeLabel,
444
+ "aria-pressed": rating === "negative",
445
+ "data-active": rating === "negative" || void 0,
446
+ disabled: pending,
447
+ onFocus: seed,
448
+ onPointerEnter: seed,
449
+ onClick: () => {
450
+ onRate("negative");
451
+ },
452
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDislikeOutline16, {})
453
+ })
454
+ }),
455
+ rating !== void 0 && !noteOpen && (0, react_jsx_runtime.jsx)("button", {
456
+ type: "button",
457
+ className: MessageFeedbackActions_module_css_default.noteOpen,
458
+ onClick: openNote,
459
+ children: item?.note === void 0 ? t("note.open") : item.note
460
+ }),
461
+ rating !== void 0 && noteOpen && (0, react_jsx_runtime.jsxs)("span", {
462
+ className: MessageFeedbackActions_module_css_default.noteEditor,
463
+ children: [
464
+ (0, react_jsx_runtime.jsx)("textarea", {
465
+ className: MessageFeedbackActions_module_css_default.noteInput,
466
+ "aria-label": t("note.aria"),
467
+ placeholder: t("note.placeholder"),
468
+ value: draft,
469
+ rows: 2,
470
+ onChange: (event) => {
471
+ setDraft(event.target.value);
472
+ }
473
+ }),
474
+ (0, react_jsx_runtime.jsx)("button", {
475
+ type: "button",
476
+ className: MessageFeedbackActions_module_css_default.noteSave,
477
+ disabled: pending,
478
+ onClick: () => {
479
+ onSaveNote(rating);
480
+ },
481
+ children: t("note.save")
482
+ }),
483
+ (0, react_jsx_runtime.jsx)("button", {
484
+ type: "button",
485
+ className: MessageFeedbackActions_module_css_default.noteCancel,
486
+ onClick: () => {
487
+ setNoteOpen(false);
488
+ },
489
+ children: t("note.cancel")
490
+ })
491
+ ]
492
+ }),
493
+ failure === null && loadFailed && (0, react_jsx_runtime.jsx)("span", {
494
+ className: MessageFeedbackActions_module_css_default.failure,
495
+ role: "status",
496
+ children: t("error.load")
497
+ }),
498
+ failure !== null && (0, react_jsx_runtime.jsx)("span", {
499
+ className: MessageFeedbackActions_module_css_default.failure,
500
+ role: "status",
501
+ children: failure
502
+ })
503
+ ] });
504
+ }
505
+ //#endregion
506
+ //#region lib/types/client/locales.js
507
+ /** `feedback` namespace dictionaries. */
508
+ /** Simplified Chinese dictionary (the key-set source of truth). */
509
+ const zh = {
510
+ "action.like": "好的回答",
511
+ "action.likeActive": "取消标记",
512
+ "action.dislike": "有问题的回答",
513
+ "action.dislikeActive": "取消标记",
514
+ "note.open": "补充说明",
515
+ "note.placeholder": "这条回答哪里好,或哪里有问题?(可选)",
516
+ "note.save": "保存",
517
+ "note.cancel": "取消",
518
+ "note.aria": "反馈说明",
519
+ "error.conflict": "这条反馈已在别处改动,已显示最新状态",
520
+ "error.load": "反馈状态加载失败",
521
+ "error.generic": "反馈保存失败"
522
+ };
523
+ /** English dictionary, checked complete against the zh key set. */
524
+ const en = {
525
+ "action.like": "Good response",
526
+ "action.likeActive": "Remove rating",
527
+ "action.dislike": "Bad response",
528
+ "action.dislikeActive": "Remove rating",
529
+ "note.open": "Add a note",
530
+ "note.placeholder": "What was good, or what went wrong? (optional)",
531
+ "note.save": "Save",
532
+ "note.cancel": "Cancel",
533
+ "note.aria": "Feedback note",
534
+ "error.conflict": "This feedback changed elsewhere; the latest state is shown",
535
+ "error.load": "Could not load feedback",
536
+ "error.generic": "Could not save feedback"
537
+ };
538
+ //#endregion
539
+ //#region lib/types/client/index.js
540
+ /**
541
+ * Message feedback plugin, browser half: the Like/Dislike entry in the
542
+ * conversation.chat.assistant-actions strip. One MessageFeedbackController per
543
+ * Session backs every message control in that Session, so a single list read
544
+ * seeds the whole transcript. Mutations go through the generated
545
+ * messageFeedback Remote; the Host owns per-item compare-and-set.
546
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client
547
+ */
548
+ /** Dictionary namespace owned by this plugin. */
549
+ const NS = "feedback";
550
+ /** Required services: the slot registry, the Remote namespace, and the copy. */
551
+ const inject = [
552
+ "slots",
553
+ "remote",
554
+ "remote.messageFeedback",
555
+ "locale"
556
+ ];
557
+ /**
558
+ * Client plugin body: the per-message feedback entry and its per-session
559
+ * object layer.
560
+ * @param ctx - client root context.
561
+ */
562
+ function apply(ctx) {
563
+ ctx.effect(() => ctx.locale.register(NS, {
564
+ zh,
565
+ en
566
+ }), "ui-message-feedback: dictionaries");
567
+ const controllers = /* @__PURE__ */ new Map();
568
+ const controllerFor = (sessionId) => {
569
+ let controller = controllers.get(sessionId);
570
+ if (controller === void 0) {
571
+ controller = new MessageFeedbackController(ctx.remote.messageFeedback, sessionId);
572
+ controllers.set(sessionId, controller);
573
+ }
574
+ return controller;
575
+ };
576
+ ctx.on("connection/reset", () => {
577
+ for (const controller of controllers.values()) if (controller.getSnapshot().status !== "cold") controller.resync();
578
+ });
579
+ ctx.slots.inject("conversation.chat.assistant-actions", () => {
580
+ const dispose = ctx.slots.register({
581
+ name: "conversation.chat.assistant-actions",
582
+ id: "feedback",
583
+ order: 10,
584
+ locale: NS,
585
+ inject: (sessionId) => {
586
+ const controller = controllerFor(sessionId);
587
+ return {
588
+ hooks: { feedback: controller },
589
+ ensure: () => controller.ensure(),
590
+ rate: (messageId, rating, note) => controller.rate(messageId, rating, note),
591
+ toggle: (messageId, rating) => controller.toggle(messageId, rating),
592
+ clearNote: (messageId) => controller.clearNote(messageId),
593
+ clear: (messageId) => controller.clear(messageId)
594
+ };
595
+ }
596
+ }, MessageFeedbackActions);
597
+ return () => {
598
+ dispose();
599
+ for (const controller of controllers.values()) controller.dispose();
600
+ controllers.clear();
601
+ };
602
+ });
603
+ }
604
+ //#endregion
605
+ exports.apply = apply;
606
+ exports.inject = inject;
607
+ return module.exports;
608
+ }
609
+ });
610
+
611
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Message feedback surface plugin, node half. Pure UI plugin: the empty apply
4
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
5
+ * half ships via exports["./client"], discovered through the package.json
6
+ * dsh.client declaration.
7
+ */
8
+ /** Host plugin body — no host-side behavior for this surface plugin. */
9
+ function apply() {}
10
+ //#endregion
11
+ export { apply };
@@ -0,0 +1,26 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-message-feedback`.
4
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-client-ui-message-feedback";
7
+ /** Cordis companion plugin name. */
8
+ const name = "client-ui-feedback-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: the plugin owns one slot registration and one
13
+ * per-session controller map, both released by the same effect disposer. The
14
+ * lifecycle spec proves the registration is withdrawn and every controller is
15
+ * dropped when the owning fiber is disposed, so no second authority exists to
16
+ * check at runtime.
17
+ */
18
+ const install = () => {};
19
+ /**
20
+ * Register this package's invariant companion.
21
+ * @param ctx - Cordis context carrying the invariant service.
22
+ * @returns the installed registration's disposer after setup succeeds.
23
+ */
24
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
25
+ //#endregion
26
+ export { apply, inject, name };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Per-message feedback controls: a Like/Dislike pair plus an optional note.
3
+ * Rendered inside the assistant message's IconActions row, so the buttons
4
+ * reuse that row's chrome and sit between copy and branch.
5
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/MessageFeedbackActions
6
+ */
7
+ import type { MessageFeedbackActionProps } from './slots.ts';
8
+ /**
9
+ * One message's feedback controls.
10
+ * @param props - the owner's message identity, the injected verbs, and the
11
+ * shared feedback hook.
12
+ * @returns the rating buttons, plus the note editor while it is open.
13
+ */
14
+ export declare function MessageFeedbackActions({ messageId, ensure, rate, toggle, clearNote, useFeedback, t }: MessageFeedbackActionProps): import("react").JSX.Element;
15
+ //# sourceMappingURL=MessageFeedbackActions.d.ts.map
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Browser-local object layer over one Session's durable message-feedback
3
+ * sidecar. The Host owns per-item compare-and-set: every mutation carries the
4
+ * version this controller last observed, and a `version-conflict` reply carries
5
+ * the authoritative item, so a lost race reconciles from the reply itself
6
+ * instead of refetching the whole Session.
7
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/controller
8
+ */
9
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
10
+ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
11
+ import type { MessageId, SessionId } from '@deepseek-ai/dsh-client-connection/client';
12
+ import type { MessageFeedbackDeleteResult, MessageFeedbackItem, MessageFeedbackListResult, MessageFeedbackPutResult, MessageFeedbackRating } from '@deepseek-ai/dsh-message-feedback/types';
13
+ /**
14
+ * The three Remote calls this controller needs. The generated face wraps every
15
+ * business result in {@link RemoteResult}: a carrier failure arrives as the
16
+ * `ok: false` branch rather than a rejection, so this controller reads one
17
+ * envelope and never wraps a call to recover a transport error.
18
+ */
19
+ export interface MessageFeedbackRemote {
20
+ list: (request: {
21
+ sessionId: SessionId;
22
+ }) => Promise<RemoteResult<MessageFeedbackListResult>>;
23
+ put: (request: {
24
+ sessionId: SessionId;
25
+ messageId: MessageId;
26
+ rating: MessageFeedbackRating;
27
+ note?: string;
28
+ ifVersion: MessageFeedbackItem['version'] | null;
29
+ }) => Promise<RemoteResult<MessageFeedbackPutResult>>;
30
+ delete: (request: {
31
+ sessionId: SessionId;
32
+ messageId: MessageId;
33
+ ifVersion: MessageFeedbackItem['version'];
34
+ }) => Promise<RemoteResult<MessageFeedbackDeleteResult>>;
35
+ }
36
+ /** Load state of the one list read that seeds every per-message control. */
37
+ export type MessageFeedbackStatus = 'cold' | 'loading' | 'ready' | 'error';
38
+ /** Immutable view published to every per-message control in one Session. */
39
+ export interface MessageFeedbackView {
40
+ status: MessageFeedbackStatus;
41
+ /** Current item per message, keyed by the addressed message id. */
42
+ items: ReadonlyMap<MessageId, MessageFeedbackItem>;
43
+ /** Reason the last load failed, cleared by the next successful load. */
44
+ error: string | null;
45
+ }
46
+ /** Settled action shape rendered by the message-level controls. */
47
+ export type MessageFeedbackActionResult = {
48
+ ok: true;
49
+ } | {
50
+ ok: false;
51
+ error: {
52
+ code: string;
53
+ message: string;
54
+ };
55
+ };
56
+ /**
57
+ * Per-session feedback object layer. One instance backs every per-message
58
+ * control in that Session, so a single list read seeds them all.
59
+ */
60
+ export declare class MessageFeedbackController implements HostObservable<MessageFeedbackView> {
61
+ private readonly remote;
62
+ private readonly sessionId;
63
+ private view;
64
+ private readonly listeners;
65
+ private loadPromise;
66
+ private operationTail;
67
+ private disposed;
68
+ /**
69
+ * @param remote - the messageFeedback Remote namespace.
70
+ * @param sessionId - Session owning every addressed assistant message.
71
+ */
72
+ constructor(remote: MessageFeedbackRemote, sessionId: SessionId);
73
+ /** Return the cached immutable view. */
74
+ getSnapshot: () => MessageFeedbackView;
75
+ /** Subscribe to view replacement. */
76
+ subscribe: (listener: () => void) => (() => void);
77
+ /**
78
+ * Load once; a failed load stays retryable.
79
+ * @returns the settled load result, shared by concurrent callers.
80
+ */
81
+ ensure(): Promise<MessageFeedbackActionResult>;
82
+ /**
83
+ * Re-read the authoritative list, collapsing concurrent callers onto one
84
+ * in-flight read.
85
+ *
86
+ * This is the unserialized read used to seed a cold controller, where no
87
+ * mutation can be in flight yet. A reconnect must use {@link resync} instead:
88
+ * an unserialized list response can otherwise arrive after a newer mutation's
89
+ * reply and overwrite the version that mutation just committed.
90
+ * @returns the settled reload result.
91
+ */
92
+ refresh(): Promise<MessageFeedbackActionResult>;
93
+ /**
94
+ * Re-read the list behind this Session's queued mutations, so a reconnect
95
+ * cannot resurrect a version an in-flight mutation already replaced.
96
+ * @returns the settled reload result.
97
+ */
98
+ resync(): Promise<MessageFeedbackActionResult>;
99
+ /**
100
+ * Create or replace feedback for one message, comparing against the version
101
+ * this controller last observed.
102
+ *
103
+ * The note is resolved here rather than by the caller: `mutate` awaits the
104
+ * one list read first, so this body always sees the committed item, while a
105
+ * control that rendered before that read completed would still be holding
106
+ * `undefined`. Omitting `note` therefore keeps whatever is stored; only
107
+ * {@link clearNote} removes one.
108
+ * @param messageId - target assistant message.
109
+ * @param rating - desired judgment.
110
+ * @param note - replacement explanation; omitted keeps the stored note.
111
+ * @returns the settled mutation result.
112
+ */
113
+ rate(messageId: MessageId, rating: MessageFeedbackRating, note?: string): Promise<MessageFeedbackActionResult>;
114
+ /**
115
+ * Replace one message's rating with the opposite judgment, or retract it when
116
+ * the committed rating already matches. The decision reads the committed item
117
+ * inside the serialized mutation, so a click that lands before the first list
118
+ * read still toggles against the stored value rather than the empty view a
119
+ * cold control rendered.
120
+ * @param messageId - target assistant message.
121
+ * @param rating - the judgment the human asked for.
122
+ * @returns the settled mutation result.
123
+ */
124
+ toggle(messageId: MessageId, rating: MessageFeedbackRating): Promise<MessageFeedbackActionResult>;
125
+ /**
126
+ * Drop the note while keeping the rating. Absent feedback needs no call.
127
+ * @param messageId - target assistant message.
128
+ * @returns the settled mutation result.
129
+ */
130
+ clearNote(messageId: MessageId): Promise<MessageFeedbackActionResult>;
131
+ /**
132
+ * Remove feedback for one message. A message with no known item is already
133
+ * in the requested state, so no call is made.
134
+ * @param messageId - target assistant message.
135
+ * @returns the settled mutation result.
136
+ */
137
+ clear(messageId: MessageId): Promise<MessageFeedbackActionResult>;
138
+ /** Commit one put against the observed version and reconcile a conflict. */
139
+ private putCommitted;
140
+ /** Commit one delete against the observed version and reconcile a conflict. */
141
+ private deleteCommitted;
142
+ /** Drop subscribers and refuse further work when the owning fiber unloads. */
143
+ dispose(): void;
144
+ /** Fetch the whole sidecar and publish it as the seeded view. */
145
+ private load;
146
+ /**
147
+ * Serialize one mutation behind this Session's prior mutation so queued
148
+ * operations always compare against the committed version, and translate a
149
+ * transport throw into the same settled shape the controls already render.
150
+ */
151
+ private mutate;
152
+ /**
153
+ * Replace one message's entry, keeping every other entry's identity. Only a
154
+ * `mutate` operation reaches this, and `mutate` refuses admission once the
155
+ * controller is disposed, so no disposal guard belongs here; `publish` is
156
+ * the single place that stops notifying after listeners are dropped.
157
+ */
158
+ private commit;
159
+ /** Replace the view and contain subscriber failures at the observable boundary. */
160
+ private publish;
161
+ }
162
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Message feedback plugin, browser half: the Like/Dislike entry in the
3
+ * conversation.chat.assistant-actions strip. One MessageFeedbackController per
4
+ * Session backs every message control in that Session, so a single list read
5
+ * seeds the whole transcript. Mutations go through the generated
6
+ * messageFeedback Remote; the Host owns per-item compare-and-set.
7
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client
8
+ */
9
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
10
+ export type { MessageFeedbackActionResult, MessageFeedbackStatus, MessageFeedbackView, MessageFeedbackRemote, } from './controller.ts';
11
+ export type { MessageFeedbackActionProps, MessageFeedbackInjected } from './slots.ts';
12
+ export type { MessageFeedbackKey } from './locales.ts';
13
+ /** Required services: the slot registry, the Remote namespace, and the copy. */
14
+ export declare const inject: string[];
15
+ /**
16
+ * Client plugin body: the per-message feedback entry and its per-session
17
+ * object layer.
18
+ * @param ctx - client root context.
19
+ */
20
+ export declare function apply(ctx: ClientContext): void;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,40 @@
1
+ /** `feedback` namespace dictionaries. */
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ export declare const zh: {
4
+ 'action.like': string;
5
+ 'action.likeActive': string;
6
+ 'action.dislike': string;
7
+ 'action.dislikeActive': string;
8
+ 'note.open': string;
9
+ 'note.placeholder': string;
10
+ 'note.save': string;
11
+ 'note.cancel': string;
12
+ 'note.aria': string;
13
+ 'error.conflict': string;
14
+ 'error.load': string;
15
+ 'error.generic': string;
16
+ };
17
+ /** The feedback namespace key union. */
18
+ export type MessageFeedbackKey = keyof typeof zh;
19
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
20
+ interface LocaleNamespaceMap {
21
+ /** The per-message feedback controls' copy. */
22
+ feedback: MessageFeedbackKey;
23
+ }
24
+ }
25
+ /** English dictionary, checked complete against the zh key set. */
26
+ export declare const en: {
27
+ 'action.like': string;
28
+ 'action.likeActive': string;
29
+ 'action.dislike': string;
30
+ 'action.dislikeActive': string;
31
+ 'note.open': string;
32
+ 'note.placeholder': string;
33
+ 'note.save': string;
34
+ 'note.cancel': string;
35
+ 'note.aria': string;
36
+ 'error.conflict': string;
37
+ 'error.load': string;
38
+ 'error.generic': string;
39
+ };
40
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The feedback entry's injected face. The target
3
+ * 'conversation.chat.assistant-actions' slot is declared and typed by
4
+ * ui-conversation; this package only contributes the entry, so no SlotMap
5
+ * merge lives here. Live per-message state arrives through the `feedback`
6
+ * hook (the framework standard kit binds it into `useFeedback`); inject
7
+ * carries the two mutation verbs plus the lazy loader.
8
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/slots
9
+ */
10
+ import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
11
+ import type { MessageId } from '@deepseek-ai/dsh-client-connection/client';
12
+ import type { MessageFeedbackRating } from '@deepseek-ai/dsh-message-feedback/types';
13
+ import type { MessageFeedbackActionResult, MessageFeedbackView } from './controller.ts';
14
+ /** Injected business face of one assistant-message feedback entry. */
15
+ export interface MessageFeedbackInjected {
16
+ hooks: {
17
+ /** The owning Session's feedback view, shared by every message control. */
18
+ feedback: HostObservable<MessageFeedbackView>;
19
+ };
20
+ /** Load the Session's feedback once, on first interaction. */
21
+ ensure: () => Promise<MessageFeedbackActionResult>;
22
+ /**
23
+ * Create or replace this Session's feedback for one message.
24
+ * @param messageId - target assistant message.
25
+ * @param rating - desired judgment.
26
+ * @param note - optional explanation.
27
+ */
28
+ rate: (messageId: MessageId, rating: MessageFeedbackRating, note?: string) => Promise<MessageFeedbackActionResult>;
29
+ /**
30
+ * Apply the requested judgment, retracting instead when the committed rating
31
+ * already matches. The controller decides from the committed item, so a click
32
+ * before the first list read still toggles the stored value.
33
+ * @param messageId - target assistant message.
34
+ * @param rating - the judgment the human asked for.
35
+ */
36
+ toggle: (messageId: MessageId, rating: MessageFeedbackRating) => Promise<MessageFeedbackActionResult>;
37
+ /**
38
+ * Drop the note while keeping the rating.
39
+ * @param messageId - target assistant message.
40
+ */
41
+ clearNote: (messageId: MessageId) => Promise<MessageFeedbackActionResult>;
42
+ /**
43
+ * Remove this Session's feedback for one message.
44
+ * @param messageId - target assistant message.
45
+ */
46
+ clear: (messageId: MessageId) => Promise<MessageFeedbackActionResult>;
47
+ }
48
+ /** Full props of one assistant-message feedback entry. */
49
+ export type MessageFeedbackActionProps = PropsRuntime<'conversation.chat.assistant-actions'> & InjectFace<MessageFeedbackInjected> & PropsLocale<'feedback'>;
50
+ //# sourceMappingURL=slots.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Message feedback surface plugin, node half. Pure UI plugin: the empty apply
3
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
4
+ * half ships via exports["./client"], discovered through the package.json
5
+ * dsh.client declaration.
6
+ */
7
+ /** Host plugin body — no host-side behavior for this surface plugin. */
8
+ export declare function apply(): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-message-feedback`.
3
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "client-ui-feedback-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-client-ui-message-feedback",
3
+ "description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote",
4
+ "version": "0.0.1-rc.3",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/client/ui-message-feedback"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./client": {
26
+ "types": "./lib/types/client/index.d.ts",
27
+ "default": "./lib/client.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "dsh": {
33
+ "client": {
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-runtime",
36
+ "@deepseek-ai/dsh-api-remotes",
37
+ "@deepseek-ai/dsh-client-locale",
38
+ "@deepseek-ai/dsh-client-ui-conversation"
39
+ ],
40
+ "platform": "web"
41
+ }
42
+ },
43
+ "license": "BSD-3-Clause",
44
+ "peerDependencies": {
45
+ "react": "^18.2.0",
46
+ "@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.3",
47
+ "@deepseek-ai/dsh-client-connection": "^0.0.1-rc.3",
48
+ "@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
49
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.0.1-rc.3",
50
+ "@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
51
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
52
+ "@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
53
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
54
+ "@deepseek-ai/dsh-typert-protocol": "^0.0.1-rc.3",
55
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
56
+ "@deepseek-ai/dsh-message-feedback": "^0.0.1-rc.3"
57
+ },
58
+ "devDependencies": {
59
+ "@testing-library/react": "^16.1.0",
60
+ "@types/react": "~18.3.1",
61
+ "react": "^18.2.0",
62
+ "react-dom": "^18.2.0",
63
+ "@deepseek-ai/dsh-client-connection": "^0.0.1-rc.3",
64
+ "@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.3",
65
+ "@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
66
+ "@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
67
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.0.1-rc.3",
68
+ "@deepseek-ai/dsh-client-test-runtime": "^0.0.1-rc.3",
69
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
70
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
71
+ "@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
72
+ "@deepseek-ai/dsh-message-feedback": "^0.0.1-rc.3",
73
+ "@deepseek-ai/dsh-typert-protocol": "^0.0.1-rc.3",
74
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
75
+ },
76
+ "files": [
77
+ "lib/index.js",
78
+ "lib/invariant.js",
79
+ "lib/client.js",
80
+ "lib/types/**/*.d.ts"
81
+ ],
82
+ "scripts": {
83
+ "bundle": "tsdown",
84
+ "watch": "tsdown --watch"
85
+ }
86
+ }