@khorsheed/dsh-client-message-tools 0.3.4 → 0.3.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.3.5(2026-10-26)
4
+
5
+ - **长消息自动折叠**:你发出的消息超过 600 字或 12 行时,在所有会话里折叠成约十行、底部渐隐,点开看全文;会话里最新的一条保持展开,直到你发下一条。你自己点过的展开/收起按消息记在浏览器里。
6
+ - **折叠按钮放进气泡里**:展开/收起不再单独占一行,改成气泡右下角的小圆形箭头(折叠时压在渐隐上,展开后收进底部留白并翻转方向),深浅主题下都清楚。
7
+ - **会话引用显示成标签**:用 @ 菜单引用的其它会话,在你的消息气泡里显示为「@标题」标签,不再露出 `@[标题](dsh-session:…)` 原文;句子中间的标签留在句子里。
8
+ - **README 重写**:改为大白话说明;仓内目录随 npm 包名改为 `packages/client-message-tools`(包名不变,安装与设置无感)。
9
+
3
10
  ## 0.3.4(2026-10-22)
4
11
 
5
12
  适配宿主 0.2.1 线:verifiedHost 前移至 0.2.1-rc.1,`@deepseek-ai/dsh-*` peer 区间加宽覆盖 0.2.1 并声明 `engines.dsh`(`>=0.1.5-rc.1 <0.3.0-0`);退役 load 期 invariant 伴生探针(本包为空仪式,无行为变化)。
package/README.en.md CHANGED
@@ -1,86 +1,52 @@
1
- # dsh-client-message-tools
1
+ # @khorsheed/dsh-client-message-tools
2
2
 
3
3
  English | [中文](README.md)
4
4
 
5
- Sent messages can still be edited or withdrawn — and a withdrawal really deletes them from the model's memory, not just marks them.
5
+ Take back or rewrite what you already sent — and a withdrawal really makes the model forget, it doesn't just hide the message on screen.
6
6
 
7
- Said something too fast, or the conversation went off the rails? Until now all you could do was send another message to correct course. This plugin puts a row of small buttons on every message you send: copy, edit, withdraw. Editing rewrites in place and re-sends, and the model answers the new text; withdrawing takes that message and everything after it out of the model context, folding it into an expandable "N messages withdrawn" divider — and the original text lands back in your composer draft (never auto-sent). Changed your mind? The divider's "restore to end of conversation" puts it all back.
7
+ Said something too fast, or the conversation went off course? Until now you could only send another message to patch it up, and the detour stayed in the model's memory. With Message Tools, every message you send gets a small row of buttons: want to say it differently, hit edit, change it and resend, and the model answers the new text; want it unsaid, hit withdraw, and the model no longer sees that message or anything after it — while the original text goes back into your composer for you to fix and send again.
8
8
 
9
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions1.png" width="640" alt="copy, edit, and withdraw action buttons under a user message">
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions1.png" width="560" alt="copy, edit, and withdraw action buttons under a user message">
10
10
 
11
- ## Features
11
+ ## What it does
12
12
 
13
- - **Action row on every user message** — copy, edit, and withdraw on user bubbles and admitted steering messages.
14
- - **Edit in place** — inline editor with a working model chip; saving regenerates the turn from that point, and edited bubbles edit again.
15
- - **Real withdrawal, not a marker** — the message and the whole tail after it leave the model context, collapsing into an expandable 「已撤回 N 条消息」 divider; expanding it replays the withdrawn user text, the images sent with those messages, and the assistant text (image-only messages count toward N).
16
- - **Draft backfill** — a landed withdrawal puts the original text back into the composer draft, never auto-sent.
17
- - **Restore to tail** — 「恢复到对话末尾」 replays user messages verbatim and assistant text as a 「已恢复」 group; tool calls never replay.
13
+ - **Edit and get a fresh answer**: hit edit and the message turns into an input box in place — you can switch the model too; "Save and resend" and the model answers again from there. Edited messages can be edited again.
14
+ - **Withdraw means forgotten**: the message and everything after it leave the model's context; the conversation keeps only a "Withdrawn N messages" line you can expand to read the original.
15
+ - **Your words come back to the composer**: the withdrawn text lands back in the input box for you to rework — it is never sent for you.
16
+ - **Changed your mind? Put it back**: "Restore to the end of the conversation" brings the withdrawn content back, as it was, at the end.
17
+ - **Long messages fold up**: a pasted wall of text folds to about ten lines; "Show all" opens it. The newest message always stays open.
18
+ - **Mentioned sessions read at a glance**: when you @ another session in a message, the bubble shows a small tag with that session's title instead of a raw link.
18
19
 
19
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions2.png" width="640" alt="in-place editing: the message becomes an input with a model picker; saving re-sends it as a new message, and the edited original leaves the model context">
20
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions2.png" width="560" alt="in-place editing: the message becomes an input with a model picker; saving re-sends it as a new message, and the edited original leaves the model context">
20
21
 
21
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions3.png" width="640" alt="the confirmation dialog before withdrawing, explaining that the message and everything after it will be hidden from the model and the original text backfilled into the composer">
22
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions3.png" width="560" alt="the confirmation dialog before withdrawing, explaining that the message and everything after it will be hidden from the model and the original text backfilled into the composer">
22
23
 
23
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions4.png" width="640" alt="withdrawn content folds into an expandable divider with a restore-to-end-of-conversation button at the bottom">
24
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions4.png" width="560" alt="withdrawn content folds into an expandable divider with a restore-to-end-of-conversation button at the bottom">
24
25
 
25
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions5.png" width="640" alt="restored messages return to the tail of the conversation as they were, grouped under a Restored section">
26
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions5.png" width="560" alt="restored messages return to the tail of the conversation as they were, grouped under a Restored section">
26
27
 
27
28
  ## Install
28
29
 
29
- ```sh
30
- dsh plugin --profile web add @khorsheed/dsh-client-message-tools
31
- ```
32
-
33
- Restart the web instance to activate; uninstall restores the previous composition exactly.
30
+ In DSH, open Plugins → Add plugin, enter the package name below and click Install:
34
31
 
35
- ```sh
36
- dsh plugin --profile web remove @khorsheed/dsh-client-message-tools
32
+ ```
33
+ @khorsheed/dsh-basic
37
34
  ```
38
35
 
39
- ## Compatibility
40
-
41
- - npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`): ✅ full — adapted to the 0.1.5-rc.1 format v2/v3 (`assistant/attempt` replaces `assistant/chunk`; the surfaceOp replace fields `start`/`end` are renamed `startSeq`/`endSeq`), full build+test green; minHost moves up to 0.1.5-rc.1 — older hosts stay on the previous release line. The Session V4 producer-owned source (kind `message-tools`) also writes durably on a 0.1.5 host: 0.1.5's `user/message` admission only requires a non-empty kind string.
42
- - source line (deepseek-harness master): ✅ (verifiedHost: 0.2.1-rc.1) — host-021 wave: full build+test green against the 0.2.1-rc.1 tree: the image arm consumes the official `conversation.images.content` factory in place on 0.2.1 (the 0.1.5–0.2.0 lines take the legacy-prop retention path); the shadow renderer's font size follows the host axis (Khorsheed/dsh-plugins#1). Session V4 adaptation: the write side moved to the producer-owned source (kind `message-tools`; V4 native admission refuses the retired `kind: 'plugin'` wrapper), and the read side accepts the new kind, `plugin:message-tools` (the V3→V4 migrated form), and the released V3 wrapper (served verbatim on a 0.1.5 host); the `conversation.chat.node` rc.1 contract (the new hookContext/inject shapes, the `SessionEventLike` match-event union) re-verified; the ui-primitives icon rename (`Icon*Outline14/16` → `Icon*OutlineMedium`) followed. 0.1.7-alpha.1's conversation grouping folds a turn's process rows into a group container (the「已调用工具」fold header carries the group key on the same DOM attribute), so the DOM hider now also covers the container keys of groups whose members are all hidden, plus the composite groupPart row keys; pre-0.1.7 hosts render rows ungrouped and stay covered by the plain node-key rules, unchanged.
43
-
44
- **Version line mapping**: the first release after 0.2.0 supports host `0.1.5-rc.1` and later; hosts on `0.1.2-rc.1` stay on `0.2.0`, hosts on `0.1.0-rc.6` ~ `0.1.1-rc.2` stay on the 0.1.x release line (last release `0.1.0`).
45
-
46
- ## Known Limitations
47
-
48
- - **Full-span hiding relies on an undocumented DOM attribute** — if upstream drops it, hiding degrades to renderer-only (user messages still hidden) with one `console.warn`, never an error.
49
- - **Edit and withdrawal backfill are text-only** — image attachments of the original message do not enter the resend or the backfilled composer draft; they stay visible in the divider's expanded replay, so you can send them again yourself (the host has no public "set draft text + images" API — images can only be re-registered as a fresh upload).
50
- - **Restore is a tail replay, not an in-place repair** — assistant text replays as framed user-role messages, tool calls/results never; the span stays out of the model context, and the restore entry exists only on withdrawal dividers.
51
- - **Context and queued messages are out of scope** — context messages keep the official renderer (no action row); the official queue dock already edits/removes queued messages.
52
-
53
- ## How it works
54
-
55
- <details>
56
- <summary>Internals (click to expand)</summary>
57
-
58
- **Architecture.** The browser half shadows the official user-message renderer (`conversation.chat.node`, keys `user` and `steering`, priority -1) with a visual clone that adds the action row, and registers four `ConversationNodeDefinition`s: a withdrawal projects as an expandable 「已撤回 N 条消息」 divider (N = user-visible messages hidden by the span), an edit replacement as an in-place bubble with an 「已编辑」 badge, and a restore entry as a replayed row in the 「已恢复」 group. The host half is the `messageTools` Typert Remote service (`withdraw` / `edit` / `restore`); the client mounts its generated Remote contribution via `ctx.remote.$mount`, so no core-package edits are needed.
59
-
60
- **Edit** is an in-place replacement: the host appends one `user/message` replacement whose content IS the edited text (spanning the target and the surface tail — editing an old message discards what followed it), then starts regeneration via `agent.followup` with a minimal producer-sourced trigger. The edited text appears exactly once in the model context; the old content stays in the log as the audit trail. A running turn is cancelled first and awaited until fully settled — settle means the cancelled turn's teardown (tool results, `turn/end`) has fully landed in the log, not the `running` flip; the wait is bounded, a turn that never settles rejects with 「编辑失败」, and a failed cancel rejects without editing. Edit chains work: an edited bubble edits again, targeting the previous replacement's seq. The editor's model chip shares the per-session `ModelDirectory` (`ctx.get('modelDirectories')`, optional) with the composer and the /model popup — without ui-model-selection no chip renders — and the switch applies to the regenerated turn.
61
-
62
- **Withdrawal** is real, not a marker: the host appends a `user/message` surface replacement (the compaction mechanism) spanning the target and every surface node after it, so the span leaves `session.surface` and never reaches the model again; the same cancel-and-settle choreography as edit keeps streaming content from landing after the replacement. The event carries the producer-owned source (kind `message-tools`), cites every shadowed node in `sourceEventSeqs`, and is durable (`SessionStore.flush`) before the method returns — it is the audit trail. No new event type is introduced: an out-of-harness type cannot carry `ignorable: true`, and a persisted unknown type would make session-persistence refuse the log on reload. A landed withdrawal backfills the target's original text into the composer draft (blank fills, non-empty appends a new line, info notice) — never auto-sent, nothing on failure; edits never backfill.
63
-
64
- **Projection and hiding.** The plugin's Definition claims the replacement into a divider anchored at its seq. Hiding is two-layer: the shadowed user renderer renders nothing inside a hidden span, and a dynamic stylesheet drops every chat row whose `data-chat-flow-key` (`ChatNodeSeat.tsx`) anchors inside the span, covering assistant steps, tool calls, and turn tails. The hider probes that undocumented attribute on its first non-empty rule set and, on a miss, enters a bounded retry (MutationObserver plus deadline) so a not-yet-mounted chat doesn't disable it; only an expired window disables it — one `console.warn`, renderer-only hiding — and the observer outlives the disable, so late-mounting rows reactivate it. It never throws. The divider expands in place to a read-only replay of the span (user originals — text and the images sent with them — plus assistant text, folded from the live node store; images render through the host's gallery surface — the `renderMessageImages` owner prop passed by pre-0.2.1-alpha.3 hosts when present, otherwise the shared root-scoped `conversation.images.content` factory provided by ui-attachment (0.2.1-alpha.3 split the images slot into `conversation.chat.user.images`/`conversation.chat.message.images` and the factory now carries the official gallery); with the factory unregistered, images are omitted, matching the official images slots' empty behavior) with the 「恢复到对话末尾」 action; a span whose rows fell out of the loaded window shows 「撤回的内容不在当前已加载的历史中」 instead.
65
-
66
- **Restore** is a tail replay of the whole withdrawn span, never an in-place repair: the surface fold is positional — a replaced span splices into exactly one node (`applySurfacePlan`) — so the span cannot re-enter the model context where it was, and its model-side hiding is never undone. The host walks the log interval `[start, seq)` delimited by the replacement itself and appends every replayable entry in original order: user messages verbatim (an edit replacement's content IS the last edit's text, so it replays as such) and each assistant reply's text as a framed producer-sourced user message — `assistant/message` requires the model source and the trace forbids assistant appends outside a step, so role fidelity is carried by the frame `(以下是先前被撤回、现随恢复放回的助手回复)`, stripped from display. An interrupted attempt without `assistant/message` is merged from its `assistant/attempt` event's embedded compact stream (reasoning-only content preserved). Tool calls/results never replay: the pairing cannot be re-entered and side effects are not replayable. Replayed rows render as the 「已恢复」 group — user bubbles with the full action row, assistant text via the official `MarkdownText` — and the divider carries a 「已恢复」 badge while a live restore row cites the span; withdrawing the restored rows again clears the badge and re-enables the action (restore events stay in the log).
67
-
68
- **Why the projection cannot hide the span (the upstream seam a durable fix needs).** The assembler runs every Definition's `match` per event with no veto (`conversation-assembler.ts:370`), and `match(event)` reads only the current event; shadowed events keep `surfaceOp: 'append'` (the range metadata lives on the replacement only), so they still match the built-in Definitions, which exclude *replacement* events, not shadowed ones. Node `visibility` is set only by the owning Definition, the assembler forbids withdrawing a materialized node, and a node key is bound to its Definition's kind — a plugin can neither flip nor impersonate an official node. Shadowing the other `conversation.chat.node` keys fails too: the official components are not exported, and `command`/`turn-tail`/`tool-call` declare child slots a shadow cannot re-declare (`tool-call` owns the unbounded per-tool-name `tool.call.toolview` slot). The official compaction pipeline makes the same choice by design — a replaced span stays in the transcript.
69
-
70
- **Model experience.**
71
-
72
- - *Edit replacement* — the model reads the edited text and nothing that followed the original, plus one short trigger (`(用户编辑了上一条消息,请按编辑后的内容重新回答)`). Token effect: the shadowed span's tokens leave subsequent requests; the edit adds the edited message plus the trigger. KV cache: the prompt prefix is invalidated from the edit point.
73
- - *Withdrawal replacement* — the span is replaced by one placeholder (`(用户撤回了这条消息及其后的所有内容)`). Token effect: the span's tokens leave; one short message is added. KV cache: the prefix is invalidated from the replacement point — the same trade compaction makes; withdrawing an older message discards more cached prefix.
74
- - *Restore replay* — the span's replayable content (user messages verbatim, assistant text behind the frame) appends at the tail in original order, producer-tagged and citing original events; tool tokens stay out. KV cache: none beyond an ordinary tail append.
36
+ Message Tools comes with Basic Essentials. Once installed, the button row sits under every message you send, in any session.
75
37
 
76
- **Exports.** `/client` exports the plugin body (`apply`/`inject`) and the `MessageToolsRemote` type; the host export is the `MessageToolsService` class plus wire types under `/types`.
38
+ ## Known limitations
77
39
 
78
- </details>
40
+ - Edit and withdraw bring back text only: images in the original message are not resent or put back in the composer — upload them again if you need them (they stay visible in the withdrawal record).
41
+ - Restore puts the content back at the end of the conversation, not in its original place; the agent's tool calls from back then are not replayed.
42
+ - Messages still waiting in the queue are not covered — edit those from the queue bar above the composer.
79
43
 
80
- ## Development
44
+ ## Compatibility
81
45
 
82
- Part of the [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo (`packages/message-tools`). Issues and contributions welcome there.
46
+ Requires DSH 0.1.5-rc.1 or later; verified on 0.2.1-rc.1.
83
47
 
84
- ## Changelog
48
+ ## For developers
85
49
 
86
- See [CHANGELOG.md](CHANGELOG.md).
50
+ - Per-host-line compatibility details, how edit / withdraw / restore work and what they do to the model's context: see the [full reference](https://github.com/Khorsheed/dsh-plugins/blob/main/packages/client-message-tools/docs/reference.en.md).
51
+ - You can also install it alone: enter `@khorsheed/dsh-client-message-tools` in Add plugin, or run `dsh plugin --profile web add @khorsheed/dsh-client-message-tools`, then restart.
52
+ - Part of [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) — file issues and contributions there; changes are in [CHANGELOG.md](CHANGELOG.md).
package/README.i18n.yaml CHANGED
@@ -1,6 +1,6 @@
1
1
  # Bilingual-pair consistency record: the git blob hash of each side as of the
2
2
  # last confirmed-consistent state. Both languages carry equal authority; after
3
3
  # editing either side, bring the other along and re-record with:
4
- # pnpm run verify-translation-pairing --write packages/message-tools/README.en.md
5
- packages/message-tools/README.en.md: be0592512db6b390fa808027cde00fe99c4aec65
6
- packages/message-tools/README.md: c86325e3a9f094fe71cf5c4439d206fff0ac88f3
4
+ # pnpm run verify-translation-pairing --write packages/client-message-tools/README.en.md
5
+ packages/client-message-tools/README.en.md: 23904d71e3ab4dfe9cf172b1daa21a0feee60411
6
+ packages/client-message-tools/README.md: 32abe70978661af681ba76496234cd16cc40158e
package/README.md CHANGED
@@ -2,85 +2,51 @@
2
2
 
3
3
  [English](README.en.md) | 中文
4
4
 
5
- 发出去的话还能改、还能撤——撤回是真从模型记忆里删掉,不是打个标记。
5
+ 发出去的话还能改、还能撤——撤回是真的让模型忘掉,不只是在界面上藏起来。
6
6
 
7
- 话说快了、方向跑偏了,以前只能再发一条往回找补。这个插件给你发出的每条消息配上一排小按钮:复制、编辑、撤回。编辑是原地改完重新发,模型按新内容重新回答;撤回会把这条消息和它后面的所有内容一起请出模型上下文,折叠成一条可以展开的「已撤回 N 条消息」分隔线,原话还会自动回填到输入框草稿(绝不替你发送)。后悔了,点分隔线上的「恢复到对话末尾」就能把内容原样放回去。
7
+ 话说快了、方向跑偏了,以前只能再发一条往回找补,跑偏的那段还一直留在模型的记忆里。装上消息工具,你发的每条消息下面都有一排小按钮:想换个说法就点编辑,改完重新发,模型照新内容重新回答;想当没说过就点撤回,这条和它后面的内容模型都不会再看到,原话还会放回输入框,等你改好再发。
8
8
 
9
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions1.png" width="640" alt="用户消息下方的复制、编辑、撤回操作按钮">
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions1.png" width="560" alt="用户消息下方的复制、编辑、撤回操作按钮">
10
10
 
11
- ## 特性
11
+ ## 能做什么
12
12
 
13
- - **每条用户消息都有操作行**——用户气泡与已吸入轮次的 steering 消息带复制、编辑、撤回操作。
14
- - **就地编辑**——内联编辑框带真实可用的模型 chip;保存后从该处重新生成,已编辑的气泡可再次编辑。
15
- - **真撤回,不是打标记**——消息及其后整个尾部离开模型上下文,折叠为可展开的「已撤回 N 条消息」分隔线;展开即可只读回放被撤回的用户原文、随消息发出的图片与助手文本(纯图片消息同样计入 N)。
16
- - **草稿回填**——撤回成功后把原文回填到 composer 草稿,绝不自动发送。
17
- - **恢复到尾部**——「恢复到对话末尾」把用户消息逐字重放、助手文本重放为「已恢复」组;工具调用永不重放。
13
+ - **改完就重新回答**:点编辑,消息原地变成输入框,还能顺手换个模型;点「保存并重新发送」,模型从这里重新回答。改过的消息还能再改。
14
+ - **撤回是真忘掉**:撤回后这条消息和它后面的内容都不再进入模型的上下文,对话里只留一条「已撤回 N 条消息」,点开还能看原文。
15
+ - **原话回到输入框**:撤回的那句话自动放回输入框,改一改再发;它不会替你发出去。
16
+ - **后悔了能放回来**:点「恢复到对话末尾」,撤回的内容原样回到对话最后。
17
+ - **长消息自动折起**:贴了一大段的消息折成十来行,点「展开全部」再看;最新那条一直展开着。
18
+ - **@ 过的会话一眼认出**:消息里 @ 了另一个会话,气泡里显示成带会话标题的小标签,不是一串链接。
18
19
 
19
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions2.png" width="640" alt="原位编辑:消息变成带模型选择的输入框,保存后以新消息重新发送,被编辑消息不再进入模型上下文">
20
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions2.png" width="560" alt="原位编辑:消息变成带模型选择的输入框,保存后以新消息重新发送,被编辑消息不再进入模型上下文">
20
21
 
21
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions3.png" width="640" alt="撤回前的确认弹窗:说明这条消息及其后内容将对模型隐藏、原文会回填到输入框">
22
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions3.png" width="560" alt="撤回前的确认弹窗:说明这条消息及其后内容将对模型隐藏、原文会回填到输入框">
22
23
 
23
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions4.png" width="640" alt="撤回后的内容折叠为可展开的分隔线,底部有「恢复到对话末尾」按钮">
24
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions4.png" width="560" alt="撤回后的内容折叠为可展开的分隔线,底部有「恢复到对话末尾」按钮">
24
25
 
25
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions5.png" width="640" alt="恢复后消息原样回到对话末尾,归入「已恢复」分组">
26
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-actions5.png" width="560" alt="恢复后消息原样回到对话末尾,归入「已恢复」分组">
26
27
 
27
28
  ## 安装
28
29
 
29
- ```sh
30
- dsh plugin --profile web add @khorsheed/dsh-client-message-tools
31
- ```
32
-
33
- 重启 web 实例后生效;卸载即精确还原之前的组合。
30
+ 在 DSH 的「插件 → 添加插件」里输入下面的包名,点安装:
34
31
 
35
- ```sh
36
- dsh plugin --profile web remove @khorsheed/dsh-client-message-tools
32
+ ```
33
+ @khorsheed/dsh-basic
37
34
  ```
38
35
 
39
- ## Compatibility
40
-
41
- - npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):✅ 完整——适配 0.1.5-rc.1 的 format v2/v3(`assistant/attempt` 取代 `assistant/chunk`;surfaceOp replace 字段 `start`/`end` 更名 `startSeq`/`endSeq`),全量构建测试通过;minHost 前移至 0.1.5-rc.1,旧宿主请停留在旧发布线。Session V4 的生产者归属 source(kind `message-tools`)在 0.1.5 宿主同样合法落盘:0.1.5 的 `user/message` 准入只要求非空 kind 字符串。
42
- - 源码线(deepseek-harness master):✅(verifiedHost: 0.2.1-rc.1)——host-021 波对 0.2.1-rc.1 树全量 build+test 绿:图片臂在 0.2.1 上就地消费官方 `conversation.images.content` 工厂(0.1.5–0.2.0 线走 legacy prop 保留路径);影子渲染器字号跟随宿主轴(Khorsheed/dsh-plugins#1)。Session V4 适配:写侧改生产者归属 source(kind `message-tools`,V4 原生准入拒绝退役的 `kind: 'plugin'` 包装),读侧兼认新 kind、`plugin:message-tools`(V3→V4 迁移形态)与 V3 包装存量(0.1.5 宿主原位读取);`conversation.chat.node` rc.1 契约(hookContext/inject 新形状、match 事件 `SessionEventLike` 并集)复验通过;ui-primitives 图标改名(`Icon*Outline14/16` → `Icon*OutlineMedium`)跟进。0.1.7-alpha.1 的会话分组渲染把一回合的过程行收进分组容器(「已调用工具」折叠头以分组键挂在同一 DOM 属性上),DOM 隐藏器现并入「成员全隐藏」分组的容器键与 groupPart 复合行键;0.1.7 之前的宿主没有分组渲染,原有节点键规则已覆盖,行为不变。
43
-
44
- **版本线对照**:0.2.0 之后的首个发布起支持宿主 `0.1.5-rc.1` 及以后;宿主 `0.1.2-rc.1` 请停留在 `0.2.0`,宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 请停留在 0.1.x 发布线(末版 `0.1.0`)。
36
+ 消息工具会跟着「基础体验」一起装好。装完在任意会话里,你发过的消息下面就有这排按钮。
45
37
 
46
38
  ## 已知限制
47
39
 
48
- - **全区间隐藏依赖一个未文档化的 DOM 属性**——上游若移除它,隐藏退化为仅渲染器层(用户消息仍被隐藏),`console.warn` 一次,绝不报错。
49
- - **编辑与撤回回填只带文本**——原消息的图片附件不会带入重发或回填的输入框草稿;图片仍留在分隔线的展开回放里,可以自行另发(宿主没有"文本 + 图片一起设置草稿"的公开 API,图片只能作为一次新上传重新登记)。
50
- - **恢复是尾部重放,不是原位修复**——助手文本以带框架的用户角色消息重放,工具调用/结果永不重放;撤回区间始终留在模型上下文之外,恢复入口只在撤回分隔线上。
51
- - **context 与排队消息不在范围内**——context 消息沿用官方渲染器(无操作行);仍在排队的消息由官方队列条带编辑/移除。
52
-
53
- ## 实现原理
54
-
55
- <details>
56
- <summary>内部结构(点击展开)</summary>
57
-
58
- **架构。** 浏览器半部分以官方用户消息渲染器的视觉克隆(`conversation.chat.node` 槽位、key `user` 与 `steering`、priority -1)替换默认渲染并新增操作行,同时注册四个 `ConversationNodeDefinition`:撤回投影为可展开的「已撤回 N 条消息」分隔线(N = 区间隐藏的用户可见消息数),编辑替换投影为带「已编辑」徽标的原位气泡,恢复条目投影为「已恢复」组里的重放行。Host 半部分是 `messageTools` Typert Remote 服务(`withdraw` / `edit` / `restore`);客户端经 `ctx.remote.$mount` 自行挂载生成的 Remote contribution,无需改动任何核心包。
59
-
60
- **编辑**即原位替换:host 追加一条 `user/message` replacement,其内容就是编辑后的新文本(区间覆盖目标消息及 surface 尾部——编辑旧消息就是放弃其后的一切),随后经 `agent.followup` 投递一条极简生产者来源触发消息启动重新生成。模型上下文里新文本只出现一次;旧内容留在日志里作审计。会话有运行中的轮次时先取消它并等待完全落定——落定指被取消轮次的收尾(工具结果、`turn/end`)全部落盘,而不是 `running` 翻转;等待有界,迟迟不落定的轮次以「编辑失败」拒绝,取消失败则不编辑。支持编辑链:已编辑气泡可再次编辑,目标是上一个 replacement 的 seq。编辑框的模型 chip 与 composer、/model 弹层共享按会话的 `ModelDirectory`(`ctx.get('modelDirectories')`,可选服务)——组合里没有 ui-model-selection 就不渲染 chip——切换经 host 校验后作用于重新生成的轮次。
40
+ - 编辑和撤回只带回文字:原消息里的图片不会跟着重发或回到输入框,要的话自己再传一次(撤回记录里还能看到它们)。
41
+ - 恢复是把内容放回对话末尾,不是放回原来的位置;当时 agent 调用工具的过程不会重演。
42
+ - 还在排队、没发出去的消息不归它管,用输入框上方的排队条去改。
61
43
 
62
- **撤回**是真撤回,不是打标记:host 追加一条 `user/message` surface replacement(与 compaction 同一机制),区间覆盖目标消息及之后的所有 surface 节点,该区间由此离开 `session.surface`,不再进入模型上下文;与编辑同款的 cancel-and-settle 编排保证流式内容不会落到 replacement 之后。事件为生产者来源(source kind `message-tools`),`sourceEventSeqs` 引用每一个被遮蔽节点,方法返回前经 `SessionStore.flush` 落盘——它本身就是审计轨迹。不引入新事件类型:harness 之外的类型无法携带 `ignorable: true`,持久化的未知类型会让 session-persistence 重载时拒绝整个日志。撤回成功后把目标原文回填到 composer 草稿(空草稿直接填入、非空换行追加、落 info 提示)——绝不自动发送,失败则不回填;编辑路径不触发回填。
63
-
64
- **投影与隐藏。** 插件的 Definition 把 replacement 认领为锚定在其 seq 上的分隔线节点。隐藏分两层:被遮蔽的用户渲染器对区间内用户消息渲染为空;一张动态样式表把 `data-chat-flow-key`(`ChatNodeSeat.tsx`)锚点落在区间内的聊天行一律隐藏,覆盖助手步骤、工具调用、turn 尾部。隐藏器在首个非空规则集探测这个未文档化属性,探测落空进入有界重试(MutationObserver 加截止时限),避免聊天区尚未挂载时误停用;只有窗口耗尽仍探不到行才停用——`console.warn` 一次,退化为仅渲染器隐藏——且 observer 在停用后仍存活,行晚到会重新生效,绝不报错。分隔线可就地展开只读回放撤回区间(用户原文与随消息发出的图片、加助手文本,从实时节点存储折叠;图片经宿主画廊面渲染——0.2.1-alpha.3 之前的宿主透传的 `renderMessageImages` owner prop 在场时走它,否则经 root 作用域的共享 `conversation.images.content` 工厂(ui-attachment 提供;0.2.1-alpha.3 把原图片槽位拆分为 `conversation.chat.user.images`/`conversation.chat.message.images` 后由它承载官方画廊);工厂未注册时图片不渲染,与官方图片槽位空缺时的行为一致)并提供「恢复到对话末尾」;行已掉出加载窗口的区间显示「撤回的内容不在当前已加载的历史中」。
65
-
66
- **恢复**是整个被撤回区间的尾部重放,不是原位修复:surface 折叠是位置性的——被替换区间只接续成一个节点(`applySurfacePlan`)——区间无法回到模型上下文原位,其模型侧隐藏也永不回退。host 沿撤回 replacement 自身划定的日志区间 `[start, seq)` 把可重放内容按原始顺序逐条追加到尾部:用户消息逐字重放(编辑替换的内容就是最后一次编辑的新文本,恢复时即以此为准),每条助手回复的文本以带框架的生产者来源用户消息重放——`assistant/message` 只能携带 model 来源,且轨迹不允许在 step 之外追加助手消息,因此角色保真由框架 `(以下是先前被撤回、现随恢复放回的助手回复)` 承担,UI 上不显示该框架。未落成 `assistant/message` 的中断尝试从其 `assistant/attempt` 事件内嵌的紧凑流合并重放(只有 reasoning 时保留 reasoning)。工具调用/结果永不重放:配对无法重新进入,副作用不可重放。重放行渲染为「已恢复」组——用户气泡带完整操作行,助手文本走官方 `MarkdownText`——分隔线在存在引用该区间的存活恢复行期间显示「已恢复」徽标;再次撤回这些恢复行会清掉徽标并重新启用恢复操作(恢复事件始终留在日志里)。
67
-
68
- **为什么投影层做不到(彻底修复所需的上游 seam)。** 组装器对每个事件运行所有已注册 Definition 的 `match`,没有否决机制(`conversation-assembler.ts:370`),且 `match(event)` 只能读当前事件;被遮蔽事件在日志里仍保持 `surfaceOp: 'append'`(区间元数据只存在于 replacement 自身),因此依然命中各内置 Definition——内置 Definition 排除的是 *replacement* 事件,不是被遮蔽事件。节点的 `visibility` 只能由产出它的 Definition 设置,组装器禁止把已物化节点撤回为 null,节点 key 又与其 Definition 的 kind 绑定——插件既无法翻转也无法冒充官方节点。遮蔽其余 `conversation.chat.node` key 同样行不通:官方组件没有导出,`command`/`turn-tail`/`tool-call` 声明了子槽位而遮蔽无法重复声明(`tool-call` 还持有 key 空间无界的按工具名 keyed 的 `tool.call.toolview` 槽位)。官方 compaction 流水线在设计上就做了同样选择——被替换的区间保留在 transcript 里。
69
-
70
- **模型体验。**
71
-
72
- - *编辑替换*——模型读到编辑后的文本,原消息之后的内容全部消失;其后跟一条短触发消息(`(用户编辑了上一条消息,请按编辑后的内容重新回答)`)。Token 影响:被遮蔽区间的全部 token 离开后续请求,新增编辑后消息与触发消息。KV Cache:prompt 前缀从编辑点失效。
73
- - *撤回替换*——区间被一条占位消息(`(用户撤回了这条消息及其后的所有内容)`)取代。Token 影响:区间的全部 token 离开,只新增一条短消息。KV Cache:前缀从替换点失效——取舍与 compaction 相同;撤回越早的消息,失效的缓存前缀越多。
74
- - *恢复重放*——区间的可重放内容(用户消息逐字、助手文本带框架)按原始顺序追加到尾部,插件标记并引用原事件;工具调用/结果的 token 不进入。KV Cache:不超出普通尾部追加的范围。
75
-
76
- **导出。** `/client` 导出插件本体(`apply`/`inject`)与 `MessageToolsRemote` 类型;host 侧导出 `MessageToolsService` 类,`/types` 子路径提供线上类型。
77
-
78
- </details>
79
-
80
- ## 开发
44
+ ## Compatibility
81
45
 
82
- 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/message-tools`)。问题与贡献请移步该仓库。
46
+ 需要 DSH 0.1.5-rc.1 或更新版本,已在 0.2.1-rc.1 上验证。
83
47
 
84
- ## 变更记录
48
+ ## 给开发者
85
49
 
86
- 见 [CHANGELOG.md](CHANGELOG.md)。
50
+ - 各宿主版本线的兼容细节、编辑 / 撤回 / 恢复的实现原理和对模型上下文的影响,见[完整参考](https://github.com/Khorsheed/dsh-plugins/blob/main/packages/client-message-tools/docs/reference.md)。
51
+ - 也可以单独安装:在「添加插件」里输入 `@khorsheed/dsh-client-message-tools`,或 `dsh plugin --profile web add @khorsheed/dsh-client-message-tools`,重启后生效。
52
+ - 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins),问题与贡献请到该仓库;变更见 [CHANGELOG.md](CHANGELOG.md)。
package/lib/client.js CHANGED
@@ -19242,7 +19242,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
19242
19242
  }] }
19243
19243
  },
19244
19244
  sourceLocation: {
19245
- "file": "packages/message-tools/src/index.ts",
19245
+ "file": "packages/client-message-tools/src/index.ts",
19246
19246
  "line": 140,
19247
19247
  "column": 9
19248
19248
  }
@@ -19374,7 +19374,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
19374
19374
  }] }
19375
19375
  },
19376
19376
  sourceLocation: {
19377
- "file": "packages/message-tools/src/index.ts",
19377
+ "file": "packages/client-message-tools/src/index.ts",
19378
19378
  "line": 102,
19379
19379
  "column": 9
19380
19380
  }
@@ -19512,7 +19512,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
19512
19512
  }] }
19513
19513
  },
19514
19514
  sourceLocation: {
19515
- "file": "packages/message-tools/src/index.ts",
19515
+ "file": "packages/client-message-tools/src/index.ts",
19516
19516
  "line": 68,
19517
19517
  "column": 9
19518
19518
  }
@@ -20358,6 +20358,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20358
20358
  "withdrawn.restoreFailed": "恢复失败,请重试",
20359
20359
  "error.edit": "编辑失败,请重试",
20360
20360
  "edited.badge": "已编辑",
20361
+ "fold.expand": "展开全部",
20362
+ "fold.collapse": "收起",
20361
20363
  "error.withdraw": "撤回失败,请重试",
20362
20364
  "model.aria": "切换模型,当前 {model}",
20363
20365
  "model.menuAria": "模型与推理等级",
@@ -20394,6 +20396,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20394
20396
  "withdrawn.restoreFailed": "Could not restore; please retry",
20395
20397
  "error.edit": "Could not apply the edit; please retry",
20396
20398
  "edited.badge": "Edited",
20399
+ "fold.expand": "Show all",
20400
+ "fold.collapse": "Collapse",
20397
20401
  "error.withdraw": "Could not withdraw the message; please retry",
20398
20402
  "model.aria": "Switch model, current {model}",
20399
20403
  "model.menuAria": "Model and reasoning effort",
@@ -20584,9 +20588,54 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20584
20588
  await steps.withdraw();
20585
20589
  if (text !== "") steps.backfill(text);
20586
20590
  }
20591
+ /** The kinds that render as the human's own message bubble. */
20592
+ const USER_KINDS = /* @__PURE__ */ new Set([
20593
+ "user",
20594
+ "steering",
20595
+ "message-tools-edited",
20596
+ "message-tools-restored"
20597
+ ]);
20598
+ /** Whether a message's text is long enough to fold. */
20599
+ function foldable(text) {
20600
+ return text.length > 600 || text.split("\n").length > 12;
20601
+ }
20602
+ /** Selector: the newest user message's seq (-1 when none). Module-level so uSES memoization holds. */
20603
+ function selectNewestUserSeq(chat) {
20604
+ let newest = -1;
20605
+ for (const node of chat.nodes.values()) {
20606
+ const seq = node.data?.seq;
20607
+ if (USER_KINDS.has(node.kind) && typeof seq === "number" && seq > newest) newest = seq;
20608
+ }
20609
+ return newest;
20610
+ }
20611
+ const KEY_PREFIX = "dsh-message-tools:fold:";
20612
+ /** Choices kept per session (newest messages win): bounds the stored map. */
20613
+ const KEEP = 200;
20614
+ /** The human's remembered expand/collapse choices, by message seq. */
20615
+ const foldChoices = {
20616
+ read(sessionId) {
20617
+ try {
20618
+ const raw = globalThis.localStorage?.getItem(KEY_PREFIX + sessionId);
20619
+ const parsed = raw == null ? void 0 : JSON.parse(raw);
20620
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
20621
+ } catch {
20622
+ return {};
20623
+ }
20624
+ },
20625
+ write(sessionId, seq, expanded) {
20626
+ try {
20627
+ const choices = {
20628
+ ...foldChoices.read(sessionId),
20629
+ [String(seq)]: expanded
20630
+ };
20631
+ const kept = Object.keys(choices).map(Number).sort((a, b) => b - a).slice(0, KEEP);
20632
+ globalThis.localStorage?.setItem(KEY_PREFIX + sessionId, JSON.stringify(Object.fromEntries(kept.map((key) => [key, choices[key]]))));
20633
+ } catch {}
20634
+ }
20635
+ };
20587
20636
  //#endregion
20588
20637
  //#region \0dsh-css:src/client/ModelChip.module.css.mjs
20589
- const css$3 = ".he-h5G_chip{align-items:center;gap:6px;display:flex;position:relative}.he-h5G_trigger{height:28px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));cursor:pointer;background:0 0;border:none;border-radius:24px;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;display:inline-flex}.he-h5G_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))}.he-h5G_trigger:disabled{cursor:default;opacity:.6}.he-h5G_triggerEffort{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed))}.he-h5G_menu{z-index:20;border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed)));background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));min-width:220px;max-height:320px;box-shadow:0 4px 16px var(--dsw-alias-shadow,var(--dsw-alias-border-l2,#0000001f));border-radius:8px;padding:4px;position:absolute;bottom:calc(100% + 4px);left:0;overflow-y:auto}.he-h5G_group+.he-h5G_group{margin-top:4px}.he-h5G_groupTitle{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));padding:4px 8px;font-size:12px;line-height:18px}.he-h5G_option{width:100%;color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;font-size:13px;line-height:20px;display:flex}.he-h5G_option:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))}.he-h5G_option:disabled{cursor:default;opacity:.6}.he-h5G_check{width:16px;color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));flex:none;display:inline-flex}.he-h5G_empty{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));padding:8px;font-size:13px}.he-h5G_selectFailed{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));white-space:nowrap;font-size:12px}";
20638
+ const css$3 = ".JPyRfG_chip{align-items:center;gap:6px;display:flex;position:relative}.JPyRfG_trigger{height:28px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));cursor:pointer;background:0 0;border:none;border-radius:24px;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;display:inline-flex}.JPyRfG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))}.JPyRfG_trigger:disabled{cursor:default;opacity:.6}.JPyRfG_triggerEffort{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed))}.JPyRfG_menu{z-index:20;border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed)));background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));min-width:220px;max-height:320px;box-shadow:0 4px 16px var(--dsw-alias-shadow,var(--dsw-alias-border-l2,#0000001f));border-radius:8px;padding:4px;position:absolute;bottom:calc(100% + 4px);left:0;overflow-y:auto}.JPyRfG_group+.JPyRfG_group{margin-top:4px}.JPyRfG_groupTitle{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));padding:4px 8px;font-size:12px;line-height:18px}.JPyRfG_option{width:100%;color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;font-size:13px;line-height:20px;display:flex}.JPyRfG_option:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))}.JPyRfG_option:disabled{cursor:default;opacity:.6}.JPyRfG_check{width:16px;color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));flex:none;display:inline-flex}.JPyRfG_empty{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));padding:8px;font-size:13px}.JPyRfG_selectFailed{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));white-space:nowrap;font-size:12px}";
20590
20639
  const tagId$3 = "@khorsheed/dsh-client-message-tools/ModelChip.module.css";
20591
20640
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
20592
20641
  const tag = document.createElement("style");
@@ -20596,16 +20645,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20596
20645
  document.head.appendChild(tag);
20597
20646
  }
20598
20647
  var ModelChip_module_css_default = {
20599
- "selectFailed": "he-h5G_selectFailed",
20600
- "chip": "he-h5G_chip",
20601
- "group": "he-h5G_group",
20602
- "check": "he-h5G_check",
20603
- "empty": "he-h5G_empty",
20604
- "trigger": "he-h5G_trigger",
20605
- "menu": "he-h5G_menu",
20606
- "groupTitle": "he-h5G_groupTitle",
20607
- "option": "he-h5G_option",
20608
- "triggerEffort": "he-h5G_triggerEffort"
20648
+ "check": "JPyRfG_check",
20649
+ "menu": "JPyRfG_menu",
20650
+ "empty": "JPyRfG_empty",
20651
+ "trigger": "JPyRfG_trigger",
20652
+ "triggerEffort": "JPyRfG_triggerEffort",
20653
+ "selectFailed": "JPyRfG_selectFailed",
20654
+ "option": "JPyRfG_option",
20655
+ "chip": "JPyRfG_chip",
20656
+ "group": "JPyRfG_group",
20657
+ "groupTitle": "JPyRfG_groupTitle"
20609
20658
  };
20610
20659
  //#endregion
20611
20660
  //#region src/client/ModelChip.tsx
@@ -20778,7 +20827,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20778
20827
  }
20779
20828
  //#endregion
20780
20829
  //#region \0dsh-css:src/client/UserMessageView.module.css.mjs
20781
- const css$2 = ".kCWYAa_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.kCWYAa_userStack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}.kCWYAa_bubble{background:var(--dsw-specific-bubble,var(--dsw-alias-bg-layer-1));max-width:100%;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));border-radius:22px;padding:10px 16px}.kCWYAa_textRun{white-space:pre-wrap;word-break:break-word;font-size:inherit;line-height:inherit}.kCWYAa_refChip{color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));white-space:nowrap;vertical-align:baseline;background:#6187d838;border-radius:6px;margin:0 2px;padding:0 8px;font-size:.85em;line-height:1.6;display:inline-block}.kCWYAa_actions{align-items:center;gap:10px;height:28px;display:flex}.kCWYAa_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.kCWYAa_action:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary))}.kCWYAa_editor{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed)));background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));border-radius:8px;flex-direction:column;gap:8px;width:100%;padding:12px;display:flex}.kCWYAa_editorInput{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l1,var(--dsw-alias-label-primary-dimmed)));resize:none;width:100%;max-height:240px;color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));font:inherit;background:0 0;border-radius:6px;outline:none;padding:8px;overflow-y:auto}.kCWYAa_editorActions{justify-content:flex-end;align-items:center;gap:8px;display:flex}.kCWYAa_editorTrailing{align-items:center;margin-right:auto;display:flex}.kCWYAa_actionError{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));align-self:flex-end;padding:2px 4px}.kCWYAa_editedLabel{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));align-self:flex-end}";
20830
+ const css$2 = ".mEulMq_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.mEulMq_userStack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}.mEulMq_bubble{background:var(--dsw-specific-bubble,var(--dsw-alias-bg-layer-1));max-width:100%;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));border-radius:22px;padding:10px 16px}.mEulMq_textRun{white-space:pre-wrap;word-break:break-word;font-size:inherit;line-height:inherit;display:inline}.mEulMq_refChip{color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));white-space:nowrap;vertical-align:baseline;background:#6187d838;border-radius:6px;margin:0 2px;padding:0 8px;font-size:.85em;line-height:1.6;display:inline-block}.mEulMq_actions{align-items:center;gap:10px;height:28px;display:flex}.mEulMq_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.mEulMq_action:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary))}.mEulMq_editor{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed)));background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));border-radius:8px;flex-direction:column;gap:8px;width:100%;padding:12px;display:flex}.mEulMq_editorInput{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l1,var(--dsw-alias-label-primary-dimmed)));resize:none;width:100%;max-height:240px;color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));font:inherit;background:0 0;border-radius:6px;outline:none;padding:8px;overflow-y:auto}.mEulMq_editorActions{justify-content:flex-end;align-items:center;gap:8px;display:flex}.mEulMq_editorTrailing{align-items:center;margin-right:auto;display:flex}.mEulMq_actionError{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));align-self:flex-end;padding:2px 4px}.mEulMq_editedLabel{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));align-self:flex-end}.mEulMq_clamped{max-height:calc(10 * (22px + var(--dsh-content-font-delta,0px)) + 20px);position:relative;overflow:hidden}.mEulMq_fade{cursor:pointer;background:linear-gradient(180deg, color-mix(in srgb, var(--dsw-specific-bubble,var(--dsw-alias-bg-layer-1,#f3f4f6)) 0%, transparent), var(--dsw-specific-bubble,var(--dsw-alias-bg-layer-1,#f3f4f6)));height:48px;position:absolute;bottom:0;left:0;right:0}.mEulMq_foldBar{justify-content:flex-end;margin:2px -8px -4px 0;display:flex}.mEulMq_clamped .mEulMq_foldBar{margin:0;position:absolute;bottom:6px;right:8px}.mEulMq_foldToggle{background:var(--dsw-alias-bg-base,#fff);width:30px;height:20px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));box-shadow:0 0 0 .5px color-mix(in srgb, var(--dsw-alias-label-primary,#000) 8%, transparent);cursor:pointer;border:none;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s;display:inline-flex}.mEulMq_foldToggle svg{transition:transform .16s}.mEulMq_foldToggle[aria-expanded=true] svg{transform:rotate(180deg)}.mEulMq_foldToggle:hover{background:color-mix(in srgb, var(--dsw-alias-bg-base,#fff) 82%, var(--dsw-alias-label-primary,#000));color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary))}.mEulMq_foldToggle:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:2px}";
20782
20831
  const tagId$2 = "@khorsheed/dsh-client-message-tools/UserMessageView.module.css";
20783
20832
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
20784
20833
  const tag = document.createElement("style");
@@ -20788,19 +20837,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20788
20837
  document.head.appendChild(tag);
20789
20838
  }
20790
20839
  var UserMessageView_module_css_default = {
20791
- "action": "kCWYAa_action",
20792
- "editor": "kCWYAa_editor",
20793
- "editorActions": "kCWYAa_editorActions",
20794
- "actions": "kCWYAa_actions",
20795
- "userRow": "kCWYAa_userRow",
20796
- "textRun": "kCWYAa_textRun",
20797
- "userStack": "kCWYAa_userStack",
20798
- "editorInput": "kCWYAa_editorInput",
20799
- "actionError": "kCWYAa_actionError",
20800
- "editedLabel": "kCWYAa_editedLabel",
20801
- "refChip": "kCWYAa_refChip",
20802
- "bubble": "kCWYAa_bubble",
20803
- "editorTrailing": "kCWYAa_editorTrailing"
20840
+ "fade": "mEulMq_fade",
20841
+ "editorTrailing": "mEulMq_editorTrailing",
20842
+ "actionError": "mEulMq_actionError",
20843
+ "clamped": "mEulMq_clamped",
20844
+ "editorActions": "mEulMq_editorActions",
20845
+ "foldToggle": "mEulMq_foldToggle",
20846
+ "textRun": "mEulMq_textRun",
20847
+ "editorInput": "mEulMq_editorInput",
20848
+ "foldBar": "mEulMq_foldBar",
20849
+ "refChip": "mEulMq_refChip",
20850
+ "bubble": "mEulMq_bubble",
20851
+ "editor": "mEulMq_editor",
20852
+ "userStack": "mEulMq_userStack",
20853
+ "actions": "mEulMq_actions",
20854
+ "editedLabel": "mEulMq_editedLabel",
20855
+ "action": "mEulMq_action",
20856
+ "userRow": "mEulMq_userRow"
20804
20857
  };
20805
20858
  //#endregion
20806
20859
  //#region src/client/UserMessageView.tsx
@@ -20814,7 +20867,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20814
20867
  * gallery surface (see message-images.ts): the pre-0.2.1 `renderMessageImages`
20815
20868
  * owner prop when the host still passes it, otherwise the root-scoped
20816
20869
  * `conversation.images.content` factory. User messages inside a withdrawn
20817
- * span render nothing; the withdrawal divider marks the spot.
20870
+ * span render nothing; the withdrawal divider marks the spot. A long message
20871
+ * folds behind a fade; a small chevron seated in the bubble's bottom-right
20872
+ * corner expands / collapses it (see fold.ts).
20818
20873
  */
20819
20874
  /** Selector identity kept module-level so uSES memoization holds. */
20820
20875
  function selectHiddenRanges(chat) {
@@ -20837,8 +20892,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20837
20892
  rest
20838
20893
  };
20839
20894
  }
20840
- /** Plaintext `/name` and `@name` word-boundary tokens decorate as chips; the logged text stays the truth. */
20895
+ /** The host's canonical session mention, `@[title](dsh-session:…)` (the @ menu's session chip, sent as text). */
20896
+ const SESSION_MENTION = /@\[((?:\\.|[^\\\]\n])+)\]\(dsh-session:[^)\s]+\)/gu;
20897
+ /**
20898
+ * A session mention shows as a chip with its title (the host's own user-text
20899
+ * rule does the same); the rest of the text keeps the token projection.
20900
+ */
20841
20901
  function projectUserText(text) {
20902
+ const parts = [];
20903
+ let cursor = 0;
20904
+ for (const match of text.matchAll(SESSION_MENTION)) {
20905
+ if (match.index > cursor) parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: projectTokens(text.slice(cursor, match.index)) }, `t${cursor}`));
20906
+ const label = match[1].replace(/\\(.)/gu, "$1");
20907
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
20908
+ className: UserMessageView_module_css_default.refChip,
20909
+ "data-ref-chip": "session",
20910
+ title: label,
20911
+ children: ["@", label]
20912
+ }, `s${match.index}`));
20913
+ cursor = match.index + match[0].length;
20914
+ }
20915
+ if (parts.length === 0) return projectTokens(text);
20916
+ if (cursor < text.length) parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: projectTokens(text.slice(cursor)) }, `t${cursor}`));
20917
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: parts });
20918
+ }
20919
+ /** Plaintext `/name` and `@name` word-boundary tokens decorate as chips; the logged text stays the truth. */
20920
+ function projectTokens(text) {
20842
20921
  const re = /(^|\s)([/@][\w-]+)(?=\s|$)/g;
20843
20922
  const parts = [];
20844
20923
  let cursor = 0;
@@ -20972,8 +21051,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20972
21051
  });
20973
21052
  }
20974
21053
  /** The shadowed user-message view: bubble plus copy/edit/withdraw actions. */
20975
- const UserMessageView = (0, react.memo)(function UserMessageView({ node, renderMessageImages, loadImage, renderFactorySlot, t, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }) {
20976
- const ranges = chatHookOf(standard)(selectHiddenRanges, shallowEqual);
21054
+ const UserMessageView = (0, react.memo)(function UserMessageView({ node, renderMessageImages, loadImage, renderFactorySlot, t, editMessage, withdrawMessage, backfillDraft, foldScope, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }) {
21055
+ const useChat = chatHookOf(standard);
21056
+ const ranges = useChat(selectHiddenRanges, shallowEqual);
21057
+ const newestUserSeq = useChat(selectNewestUserSeq);
21058
+ const [chosen, setChosen] = (0, react.useState)(() => foldChoices.read(foldScope)[String(node.data.seq)]);
20977
21059
  const [editing, setEditing] = (0, react.useState)(false);
20978
21060
  const [confirming, setConfirming] = (0, react.useState)(false);
20979
21061
  const [acknowledged, setAcknowledged] = (0, react.useState)(false);
@@ -20984,6 +21066,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
20984
21066
  const { text, images, rest } = contentParts(data.content);
20985
21067
  const truncated = (total) => t("json.truncated", { total });
20986
21068
  const showBubble = text !== "" || rest.length > 0;
21069
+ const collapsible = foldable(text);
21070
+ const expanded = chosen ?? data.seq >= newestUserSeq;
21071
+ const clamped = collapsible && !expanded;
21072
+ const toggle = (next) => {
21073
+ setChosen(next);
21074
+ foldChoices.write(foldScope, data.seq, next);
21075
+ };
20987
21076
  const saveEdit = (newText) => {
20988
21077
  setBusy(true);
20989
21078
  setFailed(null);
@@ -21054,12 +21143,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
21054
21143
  align: "end"
21055
21144
  }) : null,
21056
21145
  showBubble && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21057
- className: UserMessageView_module_css_default.bubble,
21058
- children: [projectUserText(text), rest.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
21059
- label: t("message.extraBlock"),
21060
- payload: block,
21061
- truncatedLabel: truncated
21062
- }, index))]
21146
+ className: clamped ? `${UserMessageView_module_css_default.bubble} ${UserMessageView_module_css_default.clamped}` : UserMessageView_module_css_default.bubble,
21147
+ children: [
21148
+ projectUserText(text),
21149
+ rest.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
21150
+ label: t("message.extraBlock"),
21151
+ payload: block,
21152
+ truncatedLabel: truncated
21153
+ }, index)),
21154
+ clamped && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21155
+ className: UserMessageView_module_css_default.fade,
21156
+ "aria-hidden": true,
21157
+ onClick: () => {
21158
+ toggle(true);
21159
+ }
21160
+ }),
21161
+ collapsible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21162
+ className: UserMessageView_module_css_default.foldBar,
21163
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
21164
+ label: expanded ? t("fold.collapse") : t("fold.expand"),
21165
+ side: "bottom",
21166
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
21167
+ type: "button",
21168
+ className: UserMessageView_module_css_default.foldToggle,
21169
+ "aria-label": expanded ? t("fold.collapse") : t("fold.expand"),
21170
+ "aria-expanded": expanded,
21171
+ onClick: () => {
21172
+ toggle(!expanded);
21173
+ },
21174
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutlineMedium, {})
21175
+ })
21176
+ })
21177
+ })
21178
+ ]
21063
21179
  })
21064
21180
  ]
21065
21181
  }),
@@ -21128,7 +21244,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
21128
21244
  });
21129
21245
  //#endregion
21130
21246
  //#region \0dsh-css:src/client/RestoredMessageView.module.css.mjs
21131
- const css$1 = ".hDMXAG_restoredLabel{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));align-items:center;gap:4px;display:inline-flex}.hDMXAG_restoredAssistantRow{color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));flex-direction:column;align-items:flex-start;gap:4px;display:flex}";
21247
+ const css$1 = ".stPzxW_restoredLabel{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));align-items:center;gap:4px;display:inline-flex}.stPzxW_restoredAssistantRow{color:var(--dsw-alias-label-primary,var(--dsw-alias-label-secondary));flex-direction:column;align-items:flex-start;gap:4px;display:flex}";
21132
21248
  const tagId$1 = "@khorsheed/dsh-client-message-tools/RestoredMessageView.module.css";
21133
21249
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
21134
21250
  const tag = document.createElement("style");
@@ -21138,8 +21254,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
21138
21254
  document.head.appendChild(tag);
21139
21255
  }
21140
21256
  var RestoredMessageView_module_css_default = {
21141
- "restoredLabel": "hDMXAG_restoredLabel",
21142
- "restoredAssistantRow": "hDMXAG_restoredAssistantRow"
21257
+ "restoredLabel": "stPzxW_restoredLabel",
21258
+ "restoredAssistantRow": "stPzxW_restoredAssistantRow"
21143
21259
  };
21144
21260
  //#endregion
21145
21261
  //#region src/client/RestoredMessageView.tsx
@@ -21180,7 +21296,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
21180
21296
  }
21181
21297
  //#endregion
21182
21298
  //#region \0dsh-css:src/client/WithdrawnDividerView.module.css.mjs
21183
- const css = ".JtQ9hG_row{padding:2px 0}.JtQ9hG_marker{width:100%;height:calc(24px + var(--dsh-content-font-delta,0px));min-width:0;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.JtQ9hG_marker:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))}.JtQ9hG_leading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex}.JtQ9hG_title{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary-dimmed,var(--dsw-alias-label-tertiary));flex:none}.JtQ9hG_badge{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));flex:none;align-items:center;gap:4px;margin-left:10px;display:inline-flex}.JtQ9hG_body{padding:4px 0 4px 22px}.JtQ9hG_empty{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px))}.JtQ9hG_textRun{white-space:pre-wrap;word-break:break-word;font-size:inherit;line-height:inherit}.JtQ9hG_entryUser,.JtQ9hG_entryAssistant{font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));padding:4px 0}.JtQ9hG_entryKind{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));margin-bottom:2px}.JtQ9hG_entryUser{border-left:2px solid var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed));padding-left:8px}.JtQ9hG_entryImages{margin-top:4px}.JtQ9hG_entryAssistant{border-left:2px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed)));padding-left:8px}.JtQ9hG_restoreRow{align-items:center;gap:8px;padding-top:8px;display:flex}.JtQ9hG_restoreFailed{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));font-size:12px}";
21299
+ const css = ".ChVksa_row{padding:2px 0}.ChVksa_marker{width:100%;height:calc(24px + var(--dsh-content-font-delta,0px));min-width:0;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.ChVksa_marker:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))}.ChVksa_leading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex}.ChVksa_title{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary-dimmed,var(--dsw-alias-label-tertiary));flex:none}.ChVksa_badge{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));flex:none;align-items:center;gap:4px;margin-left:10px;display:inline-flex}.ChVksa_body{padding:4px 0 4px 22px}.ChVksa_empty{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px))}.ChVksa_textRun{white-space:pre-wrap;word-break:break-word;font-size:inherit;line-height:inherit}.ChVksa_entryUser,.ChVksa_entryAssistant{font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-primary));padding:4px 0}.ChVksa_entryKind{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-primary-dimmed));margin-bottom:2px}.ChVksa_entryUser{border-left:2px solid var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed));padding-left:8px}.ChVksa_entryImages{margin-top:4px}.ChVksa_entryAssistant{border-left:2px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2,var(--dsw-alias-label-primary-dimmed)));padding-left:8px}.ChVksa_restoreRow{align-items:center;gap:8px;padding-top:8px;display:flex}.ChVksa_restoreFailed{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));font-size:12px}";
21184
21300
  const tagId = "@khorsheed/dsh-client-message-tools/WithdrawnDividerView.module.css";
21185
21301
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
21186
21302
  const tag = document.createElement("style");
@@ -21190,20 +21306,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
21190
21306
  document.head.appendChild(tag);
21191
21307
  }
21192
21308
  var WithdrawnDividerView_module_css_default = {
21193
- "badge": "JtQ9hG_badge",
21194
- "empty": "JtQ9hG_empty",
21195
- "marker": "JtQ9hG_marker",
21196
- "entryUser": "JtQ9hG_entryUser",
21197
- "restoreRow": "JtQ9hG_restoreRow",
21198
- "entryKind": "JtQ9hG_entryKind",
21199
- "entryImages": "JtQ9hG_entryImages",
21200
- "row": "JtQ9hG_row",
21201
- "textRun": "JtQ9hG_textRun",
21202
- "leading": "JtQ9hG_leading",
21203
- "entryAssistant": "JtQ9hG_entryAssistant",
21204
- "restoreFailed": "JtQ9hG_restoreFailed",
21205
- "title": "JtQ9hG_title",
21206
- "body": "JtQ9hG_body"
21309
+ "entryImages": "ChVksa_entryImages",
21310
+ "restoreRow": "ChVksa_restoreRow",
21311
+ "textRun": "ChVksa_textRun",
21312
+ "entryKind": "ChVksa_entryKind",
21313
+ "entryUser": "ChVksa_entryUser",
21314
+ "body": "ChVksa_body",
21315
+ "marker": "ChVksa_marker",
21316
+ "title": "ChVksa_title",
21317
+ "row": "ChVksa_row",
21318
+ "badge": "ChVksa_badge",
21319
+ "leading": "ChVksa_leading",
21320
+ "empty": "ChVksa_empty",
21321
+ "restoreFailed": "ChVksa_restoreFailed",
21322
+ "entryAssistant": "ChVksa_entryAssistant"
21207
21323
  };
21208
21324
  //#endregion
21209
21325
  //#region src/client/WithdrawnDividerView.tsx
@@ -21481,6 +21597,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
21481
21597
  backfillDraft: (text) => {
21482
21598
  backfill(sessionId, text);
21483
21599
  },
21600
+ foldScope: sessionId,
21484
21601
  modelsAvailable: directory !== void 0,
21485
21602
  loadModels: () => {
21486
21603
  directory?.load().catch(() => void 0);
@@ -130,7 +130,7 @@ const $remote = createTypertRemoteInfo({
130
130
  description: 'the replacement receipt and whether the turn was triggered, or a rejection.',
131
131
  jsonSchema: {"oneOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"value":{"type":"object","properties":{"replacementSeq":{"type":"number","description":"Seq of the replacement event carrying the edited text."},"triggered":{"type":"boolean","description":"Whether the regeneration turn was triggered (false when no live agent)."}},"required":["replacementSeq","triggered"],"additionalProperties":true}},"required":["ok","value"],"additionalProperties":true},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","const":"session-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"target-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-a-user-message"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"already-withdrawn"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"empty-text"}},"required":["code"],"additionalProperties":true}]}},"required":["ok","error"],"additionalProperties":true}]},
132
132
  },
133
- sourceLocation: {"file":"packages/message-tools/src/index.ts","line":140,"column":9},
133
+ sourceLocation: {"file":"packages/client-message-tools/src/index.ts","line":140,"column":9},
134
134
  },
135
135
  'messageTools/restore': {
136
136
  service: 'messageTools',
@@ -165,7 +165,7 @@ const $remote = createTypertRemoteInfo({
165
165
  description: 'the restore receipt, or a rejection from the closed failure union.',
166
166
  jsonSchema: {"oneOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"value":{"type":"object","properties":{"restoredSeq":{"type":"number","description":"Seq of the appended replay event."}},"required":["restoredSeq"],"additionalProperties":true}},"required":["ok","value"],"additionalProperties":true},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","const":"session-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"target-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-a-user-message"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-withdrawn"}},"required":["code"],"additionalProperties":true}]}},"required":["ok","error"],"additionalProperties":true}]},
167
167
  },
168
- sourceLocation: {"file":"packages/message-tools/src/index.ts","line":102,"column":9},
168
+ sourceLocation: {"file":"packages/client-message-tools/src/index.ts","line":102,"column":9},
169
169
  },
170
170
  'messageTools/withdraw': {
171
171
  service: 'messageTools',
@@ -200,7 +200,7 @@ const $remote = createTypertRemoteInfo({
200
200
  description: 'the replacement receipt, or a rejection from the closed failure union.',
201
201
  jsonSchema: {"oneOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"value":{"type":"object","properties":{"replacementSeq":{"type":"number","description":"Seq of the replacement event now standing in for the withdrawn span."},"shadowedCount":{"type":"number","description":"Count of surface nodes the replacement shadows."}},"required":["replacementSeq","shadowedCount"],"additionalProperties":true}},"required":["ok","value"],"additionalProperties":true},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","const":"session-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"target-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-a-user-message"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"already-withdrawn"}},"required":["code"],"additionalProperties":true}]}},"required":["ok","error"],"additionalProperties":true}]},
202
202
  },
203
- sourceLocation: {"file":"packages/message-tools/src/index.ts","line":68,"column":9},
203
+ sourceLocation: {"file":"packages/client-message-tools/src/index.ts","line":68,"column":9},
204
204
  },
205
205
  },
206
206
  }, z)
@@ -130,7 +130,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
130
130
  description: 'the replacement receipt and whether the turn was triggered, or a rejection.',
131
131
  jsonSchema: {"oneOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"value":{"type":"object","properties":{"replacementSeq":{"type":"number","description":"Seq of the replacement event carrying the edited text."},"triggered":{"type":"boolean","description":"Whether the regeneration turn was triggered (false when no live agent)."}},"required":["replacementSeq","triggered"],"additionalProperties":true}},"required":["ok","value"],"additionalProperties":true},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","const":"session-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"target-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-a-user-message"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"already-withdrawn"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"empty-text"}},"required":["code"],"additionalProperties":true}]}},"required":["ok","error"],"additionalProperties":true}]},
132
132
  },
133
- sourceLocation: {"file":"packages/message-tools/src/index.ts","line":140,"column":9},
133
+ sourceLocation: {"file":"packages/client-message-tools/src/index.ts","line":140,"column":9},
134
134
  },
135
135
  'messageTools/restore': {
136
136
  service: 'messageTools',
@@ -165,7 +165,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
165
165
  description: 'the restore receipt, or a rejection from the closed failure union.',
166
166
  jsonSchema: {"oneOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"value":{"type":"object","properties":{"restoredSeq":{"type":"number","description":"Seq of the appended replay event."}},"required":["restoredSeq"],"additionalProperties":true}},"required":["ok","value"],"additionalProperties":true},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","const":"session-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"target-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-a-user-message"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-withdrawn"}},"required":["code"],"additionalProperties":true}]}},"required":["ok","error"],"additionalProperties":true}]},
167
167
  },
168
- sourceLocation: {"file":"packages/message-tools/src/index.ts","line":102,"column":9},
168
+ sourceLocation: {"file":"packages/client-message-tools/src/index.ts","line":102,"column":9},
169
169
  },
170
170
  'messageTools/withdraw': {
171
171
  service: 'messageTools',
@@ -200,7 +200,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
200
200
  description: 'the replacement receipt, or a rejection from the closed failure union.',
201
201
  jsonSchema: {"oneOf":[{"type":"object","properties":{"ok":{"type":"boolean","const":true},"value":{"type":"object","properties":{"replacementSeq":{"type":"number","description":"Seq of the replacement event now standing in for the withdrawn span."},"shadowedCount":{"type":"number","description":"Count of surface nodes the replacement shadows."}},"required":["replacementSeq","shadowedCount"],"additionalProperties":true}},"required":["ok","value"],"additionalProperties":true},{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","const":"session-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"target-not-found"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"not-a-user-message"}},"required":["code"],"additionalProperties":true},{"type":"object","properties":{"code":{"type":"string","const":"already-withdrawn"}},"required":["code"],"additionalProperties":true}]}},"required":["ok","error"],"additionalProperties":true}]},
202
202
  },
203
- sourceLocation: {"file":"packages/message-tools/src/index.ts","line":68,"column":9},
203
+ sourceLocation: {"file":"packages/client-message-tools/src/index.ts","line":68,"column":9},
204
204
  },
205
205
  },
206
206
  }, z)
@@ -8,10 +8,12 @@
8
8
  * gallery surface (see message-images.ts): the pre-0.2.1 `renderMessageImages`
9
9
  * owner prop when the host still passes it, otherwise the root-scoped
10
10
  * `conversation.images.content` factory. User messages inside a withdrawn
11
- * span render nothing; the withdrawal divider marks the spot.
11
+ * span render nothing; the withdrawal divider marks the spot. A long message
12
+ * folds behind a fade; a small chevron seated in the bubble's bottom-right
13
+ * corner expands / collapses it (see fold.ts).
12
14
  */
13
15
  import { type ReactNode } from 'react';
14
16
  import type { UserMessageViewProps } from './slots.ts';
15
17
  /** The shadowed user-message view: bubble plus copy/edit/withdraw actions. */
16
- export declare const UserMessageView: import("react").MemoExoticComponent<({ node, renderMessageImages, loadImage, renderFactorySlot, t, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }: UserMessageViewProps) => ReactNode>;
18
+ export declare const UserMessageView: import("react").MemoExoticComponent<({ node, renderMessageImages, loadImage, renderFactorySlot, t, editMessage, withdrawMessage, backfillDraft, foldScope, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }: UserMessageViewProps) => ReactNode>;
17
19
  //# sourceMappingURL=UserMessageView.d.ts.map
@@ -9,17 +9,20 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
9
9
  * gallery surface (see message-images.ts): the pre-0.2.1 `renderMessageImages`
10
10
  * owner prop when the host still passes it, otherwise the root-scoped
11
11
  * `conversation.images.content` factory. User messages inside a withdrawn
12
- * span render nothing; the withdrawal divider marks the spot.
12
+ * span render nothing; the withdrawal divider marks the spot. A long message
13
+ * folds behind a fade; a small chevron seated in the bubble's bottom-right
14
+ * corner expands / collapses it (see fold.ts).
13
15
  */
14
- import { memo, useEffect, useRef, useState } from 'react';
16
+ import { Fragment, memo, useEffect, useRef, useState } from 'react';
15
17
  import { shallowEqual } from '@deepseek-ai/dsh-client-store';
16
18
  import { Button, JsonBlock, RiskConfirmation, Tooltip, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives';
17
- import { IconCheckOutlineMedium, IconCopyOutlineMedium, IconEditOutlineMedium } from './icons.js';
19
+ import { IconCheckOutlineMedium, IconChevronDownOutlineMedium, IconCopyOutlineMedium, IconEditOutlineMedium } from './icons.js';
18
20
  import { IconUndoOutlineMedium } from './icons-local.js';
19
21
  import { foldHiddenRanges, isSeqHidden } from './withdrawn-node.js';
20
22
  import { chatHookOf } from './chat-hook.js';
21
23
  import { renderImageGallery } from './message-images.js';
22
24
  import { withdrawAndBackfill } from './withdraw-backfill.js';
25
+ import { foldable, foldChoices, selectNewestUserSeq } from './fold.js';
23
26
  import { ModelChip } from './ModelChip.js';
24
27
  import css from './UserMessageView.module.css';
25
28
  /** Selector identity kept module-level so uSES memoization holds. */
@@ -43,8 +46,30 @@ function contentParts(content) {
43
46
  }
44
47
  return { text: texts.join(''), images, rest };
45
48
  }
46
- /** Plaintext `/name` and `@name` word-boundary tokens decorate as chips; the logged text stays the truth. */
49
+ /** The host's canonical session mention, `@[title](dsh-session:…)` (the @ menu's session chip, sent as text). */
50
+ const SESSION_MENTION = /@\[((?:\\.|[^\\\]\n])+)\]\(dsh-session:[^)\s]+\)/gu;
51
+ /**
52
+ * A session mention shows as a chip with its title (the host's own user-text
53
+ * rule does the same); the rest of the text keeps the token projection.
54
+ */
47
55
  function projectUserText(text) {
56
+ const parts = [];
57
+ let cursor = 0;
58
+ for (const match of text.matchAll(SESSION_MENTION)) {
59
+ if (match.index > cursor)
60
+ parts.push(_jsx(Fragment, { children: projectTokens(text.slice(cursor, match.index)) }, `t${cursor}`));
61
+ const label = match[1].replace(/\\(.)/gu, '$1');
62
+ parts.push(_jsxs("span", { className: css.refChip, "data-ref-chip": "session", title: label, children: ["@", label] }, `s${match.index}`));
63
+ cursor = match.index + match[0].length;
64
+ }
65
+ if (parts.length === 0)
66
+ return projectTokens(text);
67
+ if (cursor < text.length)
68
+ parts.push(_jsx(Fragment, { children: projectTokens(text.slice(cursor)) }, `t${cursor}`));
69
+ return _jsx(_Fragment, { children: parts });
70
+ }
71
+ /** Plaintext `/name` and `@name` word-boundary tokens decorate as chips; the logged text stays the truth. */
72
+ function projectTokens(text) {
48
73
  const re = /(^|\s)([/@][\w-]+)(?=\s|$)/g;
49
74
  const parts = [];
50
75
  let cursor = 0;
@@ -136,10 +161,14 @@ function InlineEditor({ initial, busy, t, onSave, onCancel, models }) {
136
161
  } }), _jsxs("div", { className: css.editorActions, children: [_jsx("div", { className: css.editorTrailing, children: models !== undefined && _jsx(ModelChip, { ...models }) }), _jsx(Button, { variant: "outline", disabled: busy, onClick: onCancel, children: t('cancel') }), _jsx(Button, { variant: "primary", disabled: busy || draft.trim() === '', onClick: save, children: t('editor.save') })] })] }));
137
162
  }
138
163
  /** The shadowed user-message view: bubble plus copy/edit/withdraw actions. */
139
- export const UserMessageView = memo(function UserMessageView({ node, renderMessageImages, loadImage, renderFactorySlot, t, editMessage, withdrawMessage, backfillDraft, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }) {
164
+ export const UserMessageView = memo(function UserMessageView({ node, renderMessageImages, loadImage, renderFactorySlot, t, editMessage, withdrawMessage, backfillDraft, foldScope, useModelDirectory, modelsAvailable, loadModels, selectModel, ...standard }) {
140
165
  // Chat data lives in the `useChat` session standard prop on host 0.1.2
141
166
  // (the helper degrades to the frozen empty snapshot without it).
142
- const ranges = chatHookOf(standard)(selectHiddenRanges, shallowEqual);
167
+ const useChat = chatHookOf(standard);
168
+ const ranges = useChat(selectHiddenRanges, shallowEqual);
169
+ const newestUserSeq = useChat(selectNewestUserSeq);
170
+ // The human's own choice for this message (remembered), else the default.
171
+ const [chosen, setChosen] = useState(() => foldChoices.read(foldScope)[String(node.data.seq)]);
143
172
  const [editing, setEditing] = useState(false);
144
173
  const [confirming, setConfirming] = useState(false);
145
174
  const [acknowledged, setAcknowledged] = useState(false);
@@ -151,6 +180,14 @@ export const UserMessageView = memo(function UserMessageView({ node, renderMessa
151
180
  const { text, images, rest } = contentParts(data.content);
152
181
  const truncated = (total) => t('json.truncated', { total });
153
182
  const showBubble = text !== '' || rest.length > 0;
183
+ const collapsible = foldable(text);
184
+ // The newest user message reads unfolded until the next one arrives.
185
+ const expanded = chosen ?? data.seq >= newestUserSeq;
186
+ const clamped = collapsible && !expanded;
187
+ const toggle = (next) => {
188
+ setChosen(next);
189
+ foldChoices.write(foldScope, data.seq, next);
190
+ };
154
191
  const saveEdit = (newText) => {
155
192
  setBusy(true);
156
193
  setFailed(null);
@@ -177,5 +214,5 @@ export const UserMessageView = memo(function UserMessageView({ node, renderMessa
177
214
  ? { useModelDirectory, loadModels, selectModel, t }
178
215
  : undefined }), failed === 'edit' && _jsx("div", { className: css.actionError, role: "status", children: t('error.edit') })] }) }));
179
216
  }
180
- return (_jsxs("div", { className: css.userRow, "data-time-hover-root": true, children: [_jsxs("div", { className: css.userStack, children: [node.kind === 'message-tools-edited' && (_jsx("div", { className: css.editedLabel, children: t('edited.badge') })), node.kind === 'message-tools-restored' && (_jsx("div", { className: css.editedLabel, children: t('withdrawn.restored') })), images.length > 0 ? renderImageGallery(renderMessageImages, renderFactorySlot, loadImage, { images, align: 'end' }) : null, showBubble && (_jsxs("div", { className: css.bubble, children: [projectUserText(text), rest.map((block, index) => (_jsx(JsonBlock, { label: t('message.extraBlock'), payload: block, truncatedLabel: truncated }, index)))] }))] }), _jsxs("div", { className: css.actions, children: [_jsx(CopyButton, { text: text, t: t }), _jsx(Tooltip, { label: t('edit'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('edit'), onClick: () => { setFailed(null); setEditing(true); }, children: _jsx(IconEditOutlineMedium, {}) }) }), _jsx(Tooltip, { label: t('action.withdraw'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('action.withdraw'), onClick: () => { setFailed(null); setAcknowledged(false); setConfirming(true); }, children: _jsx(IconUndoOutlineMedium, {}) }) })] }), failed === 'withdraw' && _jsx("div", { className: css.actionError, role: "status", children: t('error.withdraw') }), confirming && (_jsx(RiskConfirmation, { open: true, title: t('withdraw.title'), description: t('withdraw.description'), acknowledgeLabel: t('withdraw.acknowledge'), cancelLabel: t('cancel'), closeLabel: t('close'), confirmLabel: t('withdraw.confirm'), acknowledged: acknowledged, disabled: busy, onAcknowledgedChange: setAcknowledged, onCancel: () => { setConfirming(false); setAcknowledged(false); }, onConfirm: confirmWithdraw }))] }));
217
+ return (_jsxs("div", { className: css.userRow, "data-time-hover-root": true, children: [_jsxs("div", { className: css.userStack, children: [node.kind === 'message-tools-edited' && (_jsx("div", { className: css.editedLabel, children: t('edited.badge') })), node.kind === 'message-tools-restored' && (_jsx("div", { className: css.editedLabel, children: t('withdrawn.restored') })), images.length > 0 ? renderImageGallery(renderMessageImages, renderFactorySlot, loadImage, { images, align: 'end' }) : null, showBubble && (_jsxs("div", { className: clamped ? `${css.bubble} ${css.clamped}` : css.bubble, children: [projectUserText(text), rest.map((block, index) => (_jsx(JsonBlock, { label: t('message.extraBlock'), payload: block, truncatedLabel: truncated }, index))), clamped && _jsx("div", { className: css.fade, "aria-hidden": true, onClick: () => { toggle(true); } }), collapsible && (_jsx("div", { className: css.foldBar, children: _jsx(Tooltip, { label: expanded ? t('fold.collapse') : t('fold.expand'), side: "bottom", children: _jsx("button", { type: "button", className: css.foldToggle, "aria-label": expanded ? t('fold.collapse') : t('fold.expand'), "aria-expanded": expanded, onClick: () => { toggle(!expanded); }, children: _jsx(IconChevronDownOutlineMedium, {}) }) }) }))] }))] }), _jsxs("div", { className: css.actions, children: [_jsx(CopyButton, { text: text, t: t }), _jsx(Tooltip, { label: t('edit'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('edit'), onClick: () => { setFailed(null); setEditing(true); }, children: _jsx(IconEditOutlineMedium, {}) }) }), _jsx(Tooltip, { label: t('action.withdraw'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('action.withdraw'), onClick: () => { setFailed(null); setAcknowledged(false); setConfirming(true); }, children: _jsx(IconUndoOutlineMedium, {}) }) })] }), failed === 'withdraw' && _jsx("div", { className: css.actionError, role: "status", children: t('error.withdraw') }), confirming && (_jsx(RiskConfirmation, { open: true, title: t('withdraw.title'), description: t('withdraw.description'), acknowledgeLabel: t('withdraw.acknowledge'), cancelLabel: t('cancel'), closeLabel: t('close'), confirmLabel: t('withdraw.confirm'), acknowledged: acknowledged, disabled: busy, onAcknowledgedChange: setAcknowledged, onCancel: () => { setConfirming(false); setAcknowledged(false); }, onConfirm: confirmWithdraw }))] }));
181
218
  });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Long user messages fold: a message past {@link FOLD_CHARS} characters or
3
+ * {@link FOLD_LINES} lines starts clamped, except the session's newest user
4
+ * message (what the human just sent stays readable while it is answered; it
5
+ * folds once the next one arrives). The human's own expand/collapse wins and
6
+ * is remembered per message in browser storage — a per-viewer convenience:
7
+ * unreadable storage only means the default applies.
8
+ */
9
+ import type { ChatSlice } from './chat-hook.ts';
10
+ /** Characters past which a message folds (roughly ten rendered lines). */
11
+ export declare const FOLD_CHARS = 600;
12
+ /** Lines past which a message folds even when short in characters. */
13
+ export declare const FOLD_LINES = 12;
14
+ /** Whether a message's text is long enough to fold. */
15
+ export declare function foldable(text: string): boolean;
16
+ /** Selector: the newest user message's seq (-1 when none). Module-level so uSES memoization holds. */
17
+ export declare function selectNewestUserSeq(chat: ChatSlice): number;
18
+ /** The human's remembered expand/collapse choices, by message seq. */
19
+ export declare const foldChoices: {
20
+ read(sessionId: string): Record<string, boolean>;
21
+ write(sessionId: string, seq: number, expanded: boolean): void;
22
+ };
23
+ //# sourceMappingURL=fold.d.ts.map
@@ -0,0 +1,44 @@
1
+ /** Characters past which a message folds (roughly ten rendered lines). */
2
+ export const FOLD_CHARS = 600;
3
+ /** Lines past which a message folds even when short in characters. */
4
+ export const FOLD_LINES = 12;
5
+ /** The kinds that render as the human's own message bubble. */
6
+ const USER_KINDS = new Set(['user', 'steering', 'message-tools-edited', 'message-tools-restored']);
7
+ /** Whether a message's text is long enough to fold. */
8
+ export function foldable(text) {
9
+ return text.length > FOLD_CHARS || text.split('\n').length > FOLD_LINES;
10
+ }
11
+ /** Selector: the newest user message's seq (-1 when none). Module-level so uSES memoization holds. */
12
+ export function selectNewestUserSeq(chat) {
13
+ let newest = -1;
14
+ for (const node of chat.nodes.values()) {
15
+ const seq = node.data?.seq;
16
+ if (USER_KINDS.has(node.kind) && typeof seq === 'number' && seq > newest)
17
+ newest = seq;
18
+ }
19
+ return newest;
20
+ }
21
+ const KEY_PREFIX = 'dsh-message-tools:fold:';
22
+ /** Choices kept per session (newest messages win): bounds the stored map. */
23
+ const KEEP = 200;
24
+ /** The human's remembered expand/collapse choices, by message seq. */
25
+ export const foldChoices = {
26
+ read(sessionId) {
27
+ try {
28
+ const raw = globalThis.localStorage?.getItem(KEY_PREFIX + sessionId);
29
+ const parsed = raw == null ? undefined : JSON.parse(raw);
30
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ },
36
+ write(sessionId, seq, expanded) {
37
+ try {
38
+ const choices = { ...foldChoices.read(sessionId), [String(seq)]: expanded };
39
+ const kept = Object.keys(choices).map(Number).sort((a, b) => b - a).slice(0, KEEP);
40
+ globalThis.localStorage?.setItem(KEY_PREFIX + sessionId, JSON.stringify(Object.fromEntries(kept.map(key => [key, choices[key]]))));
41
+ }
42
+ catch { /* a convenience: never fail the view */ }
43
+ },
44
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
3
- * (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
3
+ * (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
4
4
  * share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
5
5
  * no host runtime identity, identical on every line. Re-run the generator
6
6
  * after adding an icon import or bumping the harness checkout.
@@ -176,6 +176,7 @@ export async function apply(ctx) {
176
176
  withdraw: () => withdraw(targetSeq),
177
177
  }, ctx.sessions.binding(sessionId)?.session.getSnapshot().running === true),
178
178
  backfillDraft: (text) => { backfill(sessionId, text); },
179
+ foldScope: sessionId,
179
180
  modelsAvailable: directory !== undefined,
180
181
  loadModels: () => {
181
182
  // Load failures surface on the directory store itself.
@@ -21,6 +21,8 @@ export declare const zh: {
21
21
  'withdrawn.restoreFailed': string;
22
22
  'error.edit': string;
23
23
  'edited.badge': string;
24
+ 'fold.expand': string;
25
+ 'fold.collapse': string;
24
26
  'error.withdraw': string;
25
27
  'model.aria': string;
26
28
  'model.menuAria': string;
@@ -65,6 +67,8 @@ export declare const en: {
65
67
  'withdrawn.restoreFailed': string;
66
68
  'error.edit': string;
67
69
  'edited.badge': string;
70
+ 'fold.expand': string;
71
+ 'fold.collapse': string;
68
72
  'error.withdraw': string;
69
73
  'model.aria': string;
70
74
  'model.menuAria': string;
@@ -21,6 +21,8 @@ export const zh = {
21
21
  'withdrawn.restoreFailed': '恢复失败,请重试',
22
22
  'error.edit': '编辑失败,请重试',
23
23
  'edited.badge': '已编辑',
24
+ 'fold.expand': '展开全部',
25
+ 'fold.collapse': '收起',
24
26
  'error.withdraw': '撤回失败,请重试',
25
27
  'model.aria': '切换模型,当前 {model}',
26
28
  'model.menuAria': '模型与推理等级',
@@ -57,6 +59,8 @@ export const en = {
57
59
  'withdrawn.restoreFailed': 'Could not restore; please retry',
58
60
  'error.edit': 'Could not apply the edit; please retry',
59
61
  'edited.badge': 'Edited',
62
+ 'fold.expand': 'Show all',
63
+ 'fold.collapse': 'Collapse',
60
64
  'error.withdraw': 'Could not withdraw the message; please retry',
61
65
  'model.aria': 'Switch model, current {model}',
62
66
  'model.menuAria': 'Model and reasoning effort',
@@ -46,6 +46,8 @@ export interface MessageToolsInjected {
46
46
  * the same path.
47
47
  */
48
48
  backfillDraft: (text: string) => void;
49
+ /** The session id the fold choices are remembered under. */
50
+ foldScope: string;
49
51
  }
50
52
  /** Full props of the shadowed 'user'/'steering'/'message-tools-edited'/'message-tools-restored' renderer (all keys share it). */
51
53
  export type UserMessageViewProps = PropsRuntime<'conversation.chat.node', 'user' | 'steering' | 'message-tools-edited' | 'message-tools-restored'> & PropsRenderFactories & InjectFace<MessageToolsInjected> & PropsLocale<'message-tools'> & LegacyMessageImagesProp;
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "cordis",
8
8
  "messages"
9
9
  ],
10
- "version": "0.3.4",
10
+ "version": "0.3.5",
11
11
  "type": "module",
12
12
  "main": "lib/index.js",
13
13
  "types": "lib/types/index.d.ts",