@klarkxy/dsh-session-manager 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -21,7 +21,9 @@ const reasons = {
21
21
  stale: "这个聊天已经更新,请再试一次。",
22
22
  running: "这个聊天仍在运行。请等待当前工作结束后再试。",
23
23
  cleanup: "清理没有完成。",
24
- unsupported: "当前版本不能完成这个操作。"
24
+ unsupported: "当前版本不能完成这个操作。",
25
+ retryPending: "重发尚未完成。再次发送会继续同一次操作,不会新建另一份聊天。",
26
+ retryUncertain: "上次创建聊天的结果无法确认,已停止再次创建。请先检查会话列表中是否已有新聊天。"
25
27
  },
26
28
  en: {
27
29
  modified: "Later operations may have made changes. Retry is unavailable.",
@@ -30,7 +32,9 @@ const reasons = {
30
32
  stale: "This chat has changed. Try again.",
31
33
  running: "This chat is still running. Wait for the current work to finish, then try again.",
32
34
  cleanup: "Cleanup did not finish.",
33
- unsupported: "This runtime cannot complete that action."
35
+ unsupported: "This runtime cannot complete that action.",
36
+ retryPending: "Resend is incomplete. Send again to continue the same operation without creating another chat.",
37
+ retryUncertain: "The previous chat creation could not be confirmed. Another chat will not be created; check the session list for an existing new chat."
34
38
  }
35
39
  };
36
40
  const text = {
@@ -48,6 +52,12 @@ const text = {
48
52
  edit: "编辑并重发",
49
53
  send: "发送",
50
54
  editTitle: "编辑并重发",
55
+ editBranchWarning: "对话将从这条消息继续,之前的版本会移入归档。已执行的文件、命令和外部修改不会撤销,模型可能再次执行相关操作。",
56
+ editConfirm: "确认重发",
57
+ editAddAttachment: "添加附件",
58
+ editRemoveAttachment: "删除附件",
59
+ editImage: "图片",
60
+ editPreviewFailed: "预览不可用",
51
61
  draft: "消息文本",
52
62
  continueDefault: "继续",
53
63
  welcomeBack: "欢迎回来",
@@ -65,18 +75,20 @@ const text = {
65
75
  settingsMode: "继续灰影",
66
76
  modeFixed: "固定文本",
67
77
  modeAuto: "模型生成",
68
- settingsModeHint: "输入框为空时显示的灰色继续文本:Tab 填入,点击发送。",
78
+ settingsModeHint: "输入框为空时显示建议;按 Tab 填入,点击直接发送。",
79
+ continueSourceLabel: "内容来源",
69
80
  continueTextLabel: "固定文本内容",
70
81
  continueTextPlaceholder: "留空使用默认「继续」",
71
- ghostModelLabel: "灰影模型",
72
- ghostModelHint: "只在上一轮结束后生成一次,并缓存在这个会话中;留空跟随会话或宿主默认。",
73
- welcomeBackGroup: "欢迎回来",
74
- recapEnabledLabel: "欢迎回来摘要",
75
- recapEnabledHint: "回到空闲的会话时,在对话顶部生成回顾卡片。",
76
- recapMinutesLabel: "摘要阈值(分钟)",
77
- compactEnabledLabel: "压缩建议",
78
- compactEnabledHint: "空闲较久时,回到会话会建议先压缩上下文。",
79
- compactMinutesLabel: "建议阈值(分钟)",
82
+ ghostModelLabel: "生成模型",
83
+ ghostModelHint: "每轮结束后生成一次并缓存;不选择时使用当前会话或宿主的默认模型。",
84
+ welcomeBackGroup: "回到会话时",
85
+ recapEnabledLabel: "显示会话摘要",
86
+ recapEnabledHint: "在对话中回顾上次进展。",
87
+ recapMinutesLabel: "离开多久后显示摘要",
88
+ compactEnabledLabel: "建议压缩上下文",
89
+ compactEnabledHint: "长时间离开后,建议先压缩上下文。",
90
+ compactMinutesLabel: "离开多久后显示建议",
91
+ minutesUnit: "分钟",
80
92
  modelLabel: "摘要模型",
81
93
  modelFollowOption: "当前会话 / 宿主默认模型",
82
94
  modelGroupHint: "只用于欢迎回来摘要;留空跟随会话或宿主默认。",
@@ -102,6 +114,12 @@ const text = {
102
114
  edit: "Edit and resend",
103
115
  send: "Send",
104
116
  editTitle: "Edit and resend",
117
+ editBranchWarning: "The conversation continues from this message; the previous version moves to the archive. File, command, and external changes already made are not undone — the model may perform those actions again.",
118
+ editConfirm: "Confirm resend",
119
+ editAddAttachment: "Add attachments",
120
+ editRemoveAttachment: "Remove attachment",
121
+ editImage: "Image",
122
+ editPreviewFailed: "Preview unavailable",
105
123
  draft: "Message text",
106
124
  continueDefault: "Continue",
107
125
  welcomeBack: "Welcome back",
@@ -119,18 +137,20 @@ const text = {
119
137
  settingsMode: "Continue ghost",
120
138
  modeFixed: "Fixed text",
121
139
  modeAuto: "Model-generated",
122
- settingsModeHint: "The grey continuation shown on an empty composer: Tab fills it, clicking sends it.",
140
+ settingsModeHint: "Show a suggestion when the composer is empty. Press Tab to fill it, or click to send.",
141
+ continueSourceLabel: "Source",
123
142
  continueTextLabel: "Fixed text",
124
143
  continueTextPlaceholder: "Empty uses the default \"Continue\"",
125
- ghostModelLabel: "Ghost model",
126
- ghostModelHint: "Generated once after the previous turn ends and cached for this session; empty follows the conversation or host default.",
127
- welcomeBackGroup: "Welcome back",
128
- recapEnabledLabel: "Welcome-back recap",
129
- recapEnabledHint: "Shows a recap card at the top of the conversation when you return to an idle session.",
130
- recapMinutesLabel: "Recap threshold (minutes)",
131
- compactEnabledLabel: "Compaction suggestion",
132
- compactEnabledHint: "Suggests compacting the context when you return after a longer idle stretch.",
133
- compactMinutesLabel: "Suggestion threshold (minutes)",
144
+ ghostModelLabel: "Generation model",
145
+ ghostModelHint: "Generated once after each turn and cached. The default follows the current conversation or host.",
146
+ welcomeBackGroup: "When you return",
147
+ recapEnabledLabel: "Show session recap",
148
+ recapEnabledHint: "Review your previous progress in the conversation.",
149
+ recapMinutesLabel: "Show recap after",
150
+ compactEnabledLabel: "Suggest context compaction",
151
+ compactEnabledHint: "Suggest compacting the context after a long time away.",
152
+ compactMinutesLabel: "Show suggestion after",
153
+ minutesUnit: "minutes",
134
154
  modelLabel: "Recap model",
135
155
  modelFollowOption: "Current conversation / host default",
136
156
  modelGroupHint: "Used only for the welcome-back recap; empty follows the conversation or host default.",
@@ -147,7 +167,9 @@ function uiText(locale, key) {
147
167
  return text[locale][key];
148
168
  }
149
169
  function failureReason(failure) {
150
- if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/fork-unavailable" || failure.code === "session-manager/unavailable") return "unsupported";
170
+ if (failure.code === "session/retry-pending") return "retryPending";
171
+ if (failure.code === "session/retry-uncertain") return "retryUncertain";
172
+ if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/retry-media-unavailable" || failure.code === "session-manager/unavailable") return "unsupported";
151
173
  if (failure.code === "session-manager/busy") return "running";
152
174
  if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
153
175
  if (failure.code === "session/delete-pending") return "cleanup";
@@ -185,11 +207,13 @@ const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
185
207
  const WELCOME_BACK_PREPARE_ENDPOINT = "welcomeback.prepare";
186
208
  const WELCOME_BACK_COMPACT_ENDPOINT = "welcomeback.compact";
187
209
  const CONTINUE_SUGGEST_ENDPOINT = "continue.suggest";
210
+ const EDIT_PREPARE_ENDPOINT = "edit.prepare";
211
+ const EDIT_FILE_ENDPOINT = "edit.file";
188
212
  const CONFIG_READ_ENDPOINT = "config.read";
189
213
  const CONFIG_WRITE_ENDPOINT = "config.write";
190
214
  /** Card entry id in the chat flow; the recap renders at the latest completed turn's tail. */
191
215
  const WELCOME_BACK_CARD_ID = "@klarkxy/dsh-session-manager.welcome-back";
192
- const WELCOME_BACK_CARD_SLOT = "conversation.chat.turnTail";
216
+ const TURN_TAIL_SLOT = "conversation.chat.turnTail";
193
217
  const CONTINUE_GHOST_ID = "@klarkxy/dsh-session-manager.continue-ghost";
194
218
  const CONTINUE_GHOST_SLOT = "conversation.input.overlay";
195
219
  //#endregion
@@ -1656,29 +1680,199 @@ ${modelMenuCss}
1656
1680
  .${FREE_CHAT_ROOT} .dsh-free-actions {
1657
1681
  display: inline-flex;
1658
1682
  align-items: center;
1659
- gap: 4px;
1683
+ gap: 8px;
1660
1684
  min-width: 0;
1661
1685
  }
1662
1686
  .${FREE_CHAT_ROOT} .dsh-free-alert { margin: 0; }
1663
- /* Settings page: a feature is its toggle row plus the nested well that holds
1664
- * the fields the switch gates. The well reuses the nested-card surface
1665
- * (layer-2 on layer-1) so the grouping reads without another heading. */
1687
+ .${FREE_CHAT_ROOT} .dsh-free-warning {
1688
+ margin: 0 0 12px;
1689
+ color: var(--dsw-alias-label-secondary);
1690
+ }
1691
+ .${FREE_CHAT_ROOT}.dsm-user-row {
1692
+ display: flex;
1693
+ flex-direction: column;
1694
+ align-items: flex-end;
1695
+ gap: 6px;
1696
+ }
1697
+ .${FREE_CHAT_ROOT} .dsm-user-stack {
1698
+ display: flex;
1699
+ flex-direction: column;
1700
+ align-items: flex-end;
1701
+ gap: 8px;
1702
+ min-width: 0;
1703
+ max-width: 82%;
1704
+ }
1705
+ .${FREE_CHAT_ROOT} .dsm-user-bubble {
1706
+ max-width: 100%;
1707
+ padding: 10px 16px;
1708
+ border-radius: var(--dsw-radius-md);
1709
+ background: var(--dsw-alias-bg-layer-3);
1710
+ color: var(--dsw-alias-label-primary);
1711
+ font-size: 14px;
1712
+ line-height: 22px;
1713
+ white-space: pre-wrap;
1714
+ word-break: break-word;
1715
+ }
1716
+ .${FREE_CHAT_ROOT} .dsm-user-chrome {
1717
+ display: flex;
1718
+ align-items: center;
1719
+ gap: 8px;
1720
+ min-height: 28px;
1721
+ color: var(--dsw-alias-label-tertiary);
1722
+ }
1723
+ .${FREE_CHAT_ROOT} .dsm-user-time {
1724
+ font-size: 13px;
1725
+ white-space: nowrap;
1726
+ }
1727
+ .${FREE_CHAT_ROOT} .dsm-icon-action {
1728
+ display: inline-flex;
1729
+ align-items: center;
1730
+ justify-content: center;
1731
+ width: 28px;
1732
+ height: 28px;
1733
+ padding: 6px;
1734
+ border: none;
1735
+ border-radius: var(--dsw-radius-sm);
1736
+ background: transparent;
1737
+ color: var(--dsw-alias-label-tertiary);
1738
+ cursor: pointer;
1739
+ }
1740
+ .${FREE_CHAT_ROOT} .dsm-icon-action:hover:not(:disabled) {
1741
+ background: var(--dsw-alias-interactive-bg-hover);
1742
+ color: var(--dsw-alias-label-secondary);
1743
+ }
1744
+ .${FREE_CHAT_ROOT} .dsm-icon-action svg { width: 15px; height: 15px; }
1745
+ .${FREE_CHAT_ROOT} .dsm-user-attachments {
1746
+ display: flex;
1747
+ flex-wrap: wrap;
1748
+ justify-content: flex-end;
1749
+ max-width: 100%;
1750
+ gap: 8px;
1751
+ }
1752
+ .${FREE_CHAT_ROOT} .dsm-file-card {
1753
+ display: inline-flex;
1754
+ flex: 0 0 240px;
1755
+ align-items: center;
1756
+ gap: 10px;
1757
+ box-sizing: border-box;
1758
+ width: 240px;
1759
+ min-height: 64px;
1760
+ padding: 8px 12px;
1761
+ border: 0.5px solid var(--dsw-alias-border-l2);
1762
+ border-radius: var(--dsw-radius-md);
1763
+ background: var(--dsw-alias-bg-layer-3);
1764
+ }
1765
+ .${FREE_CHAT_ROOT} .dsm-file-icon { flex: none; width: 28px; height: 28px; }
1766
+ .${FREE_CHAT_ROOT} .dsm-file-content {
1767
+ display: flex;
1768
+ flex: 1;
1769
+ flex-direction: column;
1770
+ min-width: 0;
1771
+ }
1772
+ .${FREE_CHAT_ROOT} .dsm-file-name {
1773
+ overflow: hidden;
1774
+ color: var(--dsw-alias-label-primary);
1775
+ font-size: 14px;
1776
+ font-weight: 500;
1777
+ line-height: 22px;
1778
+ white-space: nowrap;
1779
+ text-overflow: ellipsis;
1780
+ }
1781
+ .${FREE_CHAT_ROOT} .dsm-file-meta {
1782
+ overflow: hidden;
1783
+ color: var(--dsw-alias-label-tertiary);
1784
+ font-size: 12px;
1785
+ line-height: 15px;
1786
+ white-space: nowrap;
1787
+ text-overflow: ellipsis;
1788
+ }
1789
+ .${FREE_CHAT_ROOT} .dsm-reference-summary {
1790
+ color: var(--dsw-alias-label-tertiary);
1791
+ font-size: 13px;
1792
+ line-height: 18px;
1793
+ }
1794
+ /* Settings page: one quiet reading surface. Rules, not nested wells, separate
1795
+ * sections; dependent controls align with the feature they belong to. */
1796
+ .${FREE_CHAT_ROOT} .dsm-settings {
1797
+ display: flex;
1798
+ flex-direction: column;
1799
+ gap: 20px;
1800
+ min-width: 0;
1801
+ }
1802
+ .${FREE_CHAT_ROOT} .dsm-setting {
1803
+ display: flex;
1804
+ flex-direction: column;
1805
+ gap: 12px;
1806
+ min-width: 0;
1807
+ }
1808
+ .${FREE_CHAT_ROOT} .dsm-setting + .dsm-setting {
1809
+ padding-top: 20px;
1810
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
1811
+ }
1812
+ .${FREE_CHAT_ROOT} .dsm-setting-header {
1813
+ display: flex;
1814
+ flex-direction: column;
1815
+ gap: 4px;
1816
+ }
1817
+ .${FREE_CHAT_ROOT} .dsm-ghost-controls {
1818
+ display: grid;
1819
+ grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr);
1820
+ align-items: start;
1821
+ gap: 16px;
1822
+ }
1823
+ /* SegmentedTabs is 42px tall; its adjacent text controls share that baseline. */
1824
+ .${FREE_CHAT_ROOT} .dsm-ghost-controls .dsh-model-menu-trigger,
1825
+ .${FREE_CHAT_ROOT} .dsm-ghost-controls .dsm-ghost-input {
1826
+ height: 42px;
1827
+ }
1666
1828
  .${FREE_CHAT_ROOT} .dsm-feature + .dsm-feature {
1829
+ margin-top: 4px;
1667
1830
  border-top: 0.5px solid var(--dsw-alias-border-l2);
1668
1831
  }
1669
- .${FREE_CHAT_ROOT} .dsm-subs {
1832
+ .${FREE_CHAT_ROOT} .dsm-feature > .dsh-ui-toggle-row { padding: 8px 0; }
1833
+ .${FREE_CHAT_ROOT} .dsm-feature-options {
1670
1834
  display: flex;
1671
1835
  flex-direction: column;
1672
- gap: 12px;
1673
- margin: 0 0 4px;
1674
- padding: 12px;
1675
- border-radius: var(--dsw-radius-md);
1676
- background: var(--dsw-alias-bg-layer-2);
1836
+ gap: 8px;
1837
+ padding: 4px 0 8px;
1677
1838
  }
1678
- .${FREE_CHAT_ROOT} .dsm-subs[data-disabled='true'] .dsh-ui-label {
1839
+ .${FREE_CHAT_ROOT} .dsm-feature-options[data-disabled='true'] .dsh-ui-label,
1840
+ .${FREE_CHAT_ROOT} .dsm-feature-options[data-disabled='true'] .dsh-ui-meta {
1679
1841
  color: var(--dsw-alias-label-tertiary);
1680
1842
  }
1681
- .${FREE_CHAT_ROOT} .dsm-minutes { width: 120px; }
1843
+ .${FREE_CHAT_ROOT} .dsm-compact-options { max-width: calc(50% - 10px); }
1844
+ .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-root,
1845
+ .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-trigger {
1846
+ width: 100%;
1847
+ max-width: none;
1848
+ }
1849
+ .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-trigger {
1850
+ justify-content: space-between;
1851
+ height: 32px;
1852
+ padding: 0 10px;
1853
+ border: 0.5px solid var(--dsw-alias-border-l4);
1854
+ border-radius: var(--dsw-radius-md);
1855
+ background: var(--dsw-alias-bg-layer-3);
1856
+ color: var(--dsw-alias-label-primary);
1857
+ }
1858
+ .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-triggerLabel {
1859
+ flex: 1;
1860
+ text-align: left;
1861
+ }
1862
+ .${FREE_CHAT_ROOT} .dsm-settings-footer .dsh-ui-notice--error {
1863
+ flex: 0 0 100%;
1864
+ min-width: 0;
1865
+ }
1866
+ .${FREE_CHAT_ROOT} .dsm-minutes-control {
1867
+ display: flex;
1868
+ align-items: center;
1869
+ gap: 8px;
1870
+ }
1871
+ .${FREE_CHAT_ROOT} .dsm-minutes { width: 88px; }
1872
+ @media (max-width: 560px) {
1873
+ .${FREE_CHAT_ROOT} .dsm-ghost-controls { grid-template-columns: minmax(0, 1fr); }
1874
+ .${FREE_CHAT_ROOT} .dsm-compact-options { max-width: none; }
1875
+ }
1682
1876
  /* Welcome-back card: one flat full-width row at the latest turn's tail in the
1683
1877
  * chat flow — a single bordered surface with the recap laid out inline, no
1684
1878
  * nested box. */
@@ -1736,6 +1930,22 @@ ${modelMenuCss}
1736
1930
  color: var(--dsw-alias-label-secondary);
1737
1931
  }
1738
1932
  .${FREE_CHAT_ROOT} .dsh-welcome-done { margin: 6px 0 0; }
1933
+
1934
+ .${FREE_CHAT_ROOT} .dsm-user-original { display: contents; }
1935
+ .${FREE_CHAT_ROOT} .dsm-user-original[hidden] { display: none; }
1936
+ .${FREE_CHAT_ROOT} .dsm-message-edit { width: min(100%, 720px); align-self: flex-end; }
1937
+ .${FREE_CHAT_ROOT} .dsm-message-editor { width: 100%; box-sizing: border-box; padding: 14px; border-radius: 18px; background: var(--dsw-alias-bg-layer-3); }
1938
+ .${FREE_CHAT_ROOT} .dsm-message-editor .dsh-free-draft { width: 100%; min-height: 80px; max-height: 360px; resize: vertical; border: none; background: transparent; box-shadow: none; padding: 4px; box-sizing: border-box; }
1939
+ .${FREE_CHAT_ROOT} .dsm-edit-footer { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
1940
+ .${FREE_CHAT_ROOT} .dsm-edit-spacer { flex: 1; }
1941
+ .${FREE_CHAT_ROOT} .dsm-edit-attachments { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
1942
+ .${FREE_CHAT_ROOT} .dsm-edit-attachment { display: flex; align-items: center; gap: 6px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; padding: 6px; max-width: 100%; }
1943
+ .${FREE_CHAT_ROOT} .dsm-edit-attachment img { width: 72px; height: 60px; object-fit: contain; border-radius: 6px; }
1944
+ .${FREE_CHAT_ROOT} .dsm-edit-attachment > span { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1945
+ .${FREE_CHAT_ROOT} .dsm-edit-attachment > svg { width: 24px; height: 24px; }
1946
+ .${FREE_CHAT_ROOT} .dsm-edit-image { position: relative; padding: 0; border: none; }
1947
+ .${FREE_CHAT_ROOT} .dsm-edit-image img { width: auto; height: auto; max-width: 220px; max-height: 140px; object-fit: contain; border-radius: 10px; }
1948
+ .${FREE_CHAT_ROOT} .dsm-edit-image > button { position: absolute; top: 4px; right: 4px; border-radius: 50%; background: var(--dsw-alias-bg-layer-3); width: 24px; height: 24px; padding: 0; }
1739
1949
  `;
1740
1950
  //#endregion
1741
1951
  //#region src/free-panel.tsx
@@ -2122,11 +2332,15 @@ function bindTemporaryWorkspaceHost(ctx) {
2122
2332
  //#region src/session-actions.ts
2123
2333
  const UNSUPPORTED_CAPABILITIES = {
2124
2334
  retry: false,
2335
+ retryModified: false,
2125
2336
  freeSession: false,
2126
2337
  deleteSession: false
2127
2338
  };
2128
2339
  function isRecoverable(failure) {
2129
- return failure.code === "transport" || failure.code === "session/delete-pending";
2340
+ return failure.code === "transport" || failure.code === "session/delete-pending" || failure.code === "session/retry-pending" || failure.code === "session/retry-uncertain";
2341
+ }
2342
+ function isStaleRetryFailure(failure) {
2343
+ return failure?.code === "session/retry-revision" || failure?.code === "session/branch-moved" || failure?.code === "session/retry-input" || failure?.code === "session/retry-conflict";
2130
2344
  }
2131
2345
  function readFailure(error) {
2132
2346
  if (isFailure(error)) return {
@@ -2187,6 +2401,8 @@ async function readCapabilities(host) {
2187
2401
  const record = value;
2188
2402
  return {
2189
2403
  retry: record.retry === true,
2404
+ retryModified: record.retryModified === true,
2405
+ ...record.retryContent === true ? { retryContent: true } : {},
2190
2406
  freeSession: record.freeSession === true,
2191
2407
  deleteSession: record.deleteSession === true
2192
2408
  };
@@ -2219,19 +2435,16 @@ async function deleteOwnedSession(host, sessionId, requestId) {
2219
2435
  if (phase !== "deleted" && phase !== "cleanup-pending") throw new HostOperationError("session/delete-pending", "missing-outcome");
2220
2436
  return { phase };
2221
2437
  }
2222
- function directRetryRequest(input) {
2438
+ /** Build the body for one edited prompt; the host keeps the original attachments when only text changes. */
2439
+ function editRetryRequest(input) {
2223
2440
  return {
2224
2441
  sessionId: input.sessionId,
2225
2442
  messageId: input.messageId,
2226
2443
  expectedRevision: input.expectedRevision,
2227
2444
  expectedInputRevision: input.expectedInputRevision,
2228
- requestId: input.requestId
2229
- };
2230
- }
2231
- function editRetryRequest(input) {
2232
- return {
2233
- ...directRetryRequest(input),
2234
- text: input.text
2445
+ requestId: input.requestId,
2446
+ ...input.content === void 0 ? { text: input.text } : { content: input.content },
2447
+ ...input.allowModified === true ? { allowModified: true } : {}
2235
2448
  };
2236
2449
  }
2237
2450
  var SubmissionLedger = class {
@@ -2416,6 +2629,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2416
2629
  let ready = false;
2417
2630
  let busy = false;
2418
2631
  let editing = false;
2632
+ let modified = false;
2633
+ let editNeedsPreview = false;
2419
2634
  let draft = "";
2420
2635
  let failure;
2421
2636
  let expectedRevision = 0;
@@ -2427,8 +2642,10 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2427
2642
  function build() {
2428
2643
  return {
2429
2644
  controlsRetry: controlsFor(capabilities, ready, host).retry,
2645
+ controlsContent: ready && capabilities.retryContent === true,
2430
2646
  busy,
2431
2647
  editing,
2648
+ modified,
2432
2649
  draft,
2433
2650
  sessionId: ownerSession,
2434
2651
  messageId: ownerMessage,
@@ -2444,7 +2661,7 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2444
2661
  const record = value;
2445
2662
  const inputRevision = typeof record.inputRevision === "string" ? record.inputRevision : "";
2446
2663
  const eligible = record.eligible === true;
2447
- if (eligible && inputRevision === "") throw new HostOperationError("session/lifecycle-unavailable", "input-revision");
2664
+ if ((eligible || record.reason === "modified") && inputRevision === "") throw new HostOperationError("session/lifecycle-unavailable", "input-revision");
2448
2665
  return {
2449
2666
  branchRevision: record.branchRevision,
2450
2667
  inputRevision,
@@ -2482,7 +2699,11 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2482
2699
  await ledger.replay(async (body) => {
2483
2700
  await callRetry(body, signal, ticket);
2484
2701
  });
2485
- if (pending !== void 0 && ticket === call && !signal.aborted && ownerSession === pending.sessionId && ownerMessage === pending.messageId) editing = false;
2702
+ if (pending !== void 0 && ticket === call && !signal.aborted && ownerSession === pending.sessionId && ownerMessage === pending.messageId) {
2703
+ editing = false;
2704
+ modified = false;
2705
+ editNeedsPreview = false;
2706
+ }
2486
2707
  } catch (error) {
2487
2708
  if (ticket === call && !signal.aborted) failure = readFailure(error);
2488
2709
  } finally {
@@ -2501,8 +2722,9 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2501
2722
  ready = true;
2502
2723
  publish();
2503
2724
  },
2504
- async retry(sessionId, id, signal) {
2725
+ async beginEdit(sessionId, id, text, signal) {
2505
2726
  if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
2727
+ if (editing && (ownerSession !== sessionId || ownerMessage !== id)) return;
2506
2728
  if (ledger.pending() !== void 0) {
2507
2729
  if (!samePending(sessionId, id)) return;
2508
2730
  await replaySame(signal);
@@ -2511,63 +2733,25 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2511
2733
  const ticket = ++call;
2512
2734
  busy = true;
2513
2735
  failure = void 0;
2514
- editing = false;
2736
+ modified = false;
2737
+ editNeedsPreview = false;
2515
2738
  ownerSession = sessionId;
2516
2739
  ownerMessage = id;
2517
2740
  publish();
2518
2741
  try {
2519
2742
  const seen = await preview(sessionId, id, signal, ticket);
2520
2743
  if (seen === void 0) return;
2521
- if (!seen.eligible) {
2522
- failure = {
2523
- code: "session/retry-blocked",
2524
- reason: seen.reason ?? "unknown"
2525
- };
2526
- return;
2527
- }
2528
2744
  if (signal.aborted || ticket !== call) return;
2529
2745
  expectedRevision = seen.branchRevision;
2530
2746
  expectedInputRevision = seen.inputRevision;
2531
- const body = directRetryRequest({
2532
- sessionId,
2533
- messageId: id,
2534
- expectedRevision: seen.branchRevision,
2535
- expectedInputRevision: seen.inputRevision,
2536
- requestId: ids()
2537
- });
2538
- await ledger.submit(body, () => callRetry(body, signal, ticket));
2539
- } catch (error) {
2540
- if (ticket === call && !signal.aborted) failure = readFailure(error);
2541
- } finally {
2542
- settle();
2543
- }
2544
- },
2545
- async beginEdit(sessionId, id, text, signal) {
2546
- if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
2547
- if (ledger.pending() !== void 0) {
2548
- if (!samePending(sessionId, id)) return;
2549
- await replaySame(signal);
2550
- return;
2551
- }
2552
- const ticket = ++call;
2553
- busy = true;
2554
- failure = void 0;
2555
- ownerSession = sessionId;
2556
- ownerMessage = id;
2557
- publish();
2558
- try {
2559
- const seen = await preview(sessionId, id, signal, ticket);
2560
- if (seen === void 0) return;
2561
- if (!seen.eligible) {
2747
+ if (!seen.eligible && (seen.reason !== "modified" || !capabilities.retryModified)) {
2562
2748
  failure = {
2563
2749
  code: "session/retry-blocked",
2564
2750
  reason: seen.reason ?? "unknown"
2565
2751
  };
2566
2752
  return;
2567
2753
  }
2568
- if (signal.aborted || ticket !== call) return;
2569
- expectedRevision = seen.branchRevision;
2570
- expectedInputRevision = seen.inputRevision;
2754
+ modified = !seen.eligible;
2571
2755
  draft = text;
2572
2756
  editing = true;
2573
2757
  } catch (error) {
@@ -2584,9 +2768,11 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2584
2768
  cancelEdit() {
2585
2769
  if (busy) return;
2586
2770
  editing = false;
2771
+ modified = false;
2772
+ editNeedsPreview = false;
2587
2773
  publish();
2588
2774
  },
2589
- async sendEdit(sessionId, id, signal) {
2775
+ async sendEdit(sessionId, id, signal, content, confirmed) {
2590
2776
  if (signal.aborted || busy || !controlsFor(capabilities, ready, host).retry) return;
2591
2777
  if (!editing || ownerSession !== sessionId || ownerMessage !== id) return;
2592
2778
  if (ledger.pending() !== void 0) {
@@ -2596,23 +2782,49 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2596
2782
  }
2597
2783
  const ticket = ++call;
2598
2784
  busy = true;
2785
+ const refresh = editNeedsPreview || isStaleRetryFailure(failure);
2599
2786
  failure = void 0;
2600
2787
  const text = draft;
2601
2788
  publish();
2602
2789
  try {
2603
2790
  if (signal.aborted || ticket !== call) return;
2791
+ if (refresh) {
2792
+ editNeedsPreview = true;
2793
+ const seen = await preview(sessionId, id, signal, ticket);
2794
+ if (seen === void 0) return;
2795
+ expectedRevision = seen.branchRevision;
2796
+ expectedInputRevision = seen.inputRevision;
2797
+ if (!seen.eligible && (seen.reason !== "modified" || !capabilities.retryModified)) {
2798
+ failure = {
2799
+ code: "session/retry-blocked",
2800
+ reason: seen.reason ?? "unknown"
2801
+ };
2802
+ return;
2803
+ }
2804
+ modified = !seen.eligible;
2805
+ editNeedsPreview = false;
2806
+ }
2807
+ if (content !== void 0 && capabilities.retryContent !== true) throw new HostOperationError("session/lifecycle-unavailable");
2604
2808
  const body = editRetryRequest({
2605
2809
  sessionId,
2606
2810
  messageId: id,
2607
2811
  expectedRevision,
2608
2812
  expectedInputRevision,
2609
2813
  requestId: ids(),
2610
- text
2814
+ text,
2815
+ ...content === void 0 ? {} : { content },
2816
+ allowModified: confirmed === true
2611
2817
  });
2612
2818
  await ledger.submit(body, () => callRetry(body, signal, ticket));
2613
- if (ticket === call && !signal.aborted) editing = false;
2819
+ if (ticket === call && !signal.aborted) {
2820
+ editing = false;
2821
+ modified = false;
2822
+ editNeedsPreview = false;
2823
+ }
2614
2824
  } catch (error) {
2615
- if (ticket === call && !signal.aborted) failure = readFailure(error);
2825
+ const nextFailure = readFailure(error);
2826
+ if (isStaleRetryFailure(nextFailure)) editNeedsPreview = true;
2827
+ if (ticket === call && !signal.aborted) failure = nextFailure;
2616
2828
  } finally {
2617
2829
  settle();
2618
2830
  }
@@ -2621,6 +2833,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2621
2833
  if (ownerSession !== sessionId || ownerMessage !== messageId) return;
2622
2834
  call += 1;
2623
2835
  editing = false;
2836
+ modified = false;
2837
+ editNeedsPreview = false;
2624
2838
  failure = void 0;
2625
2839
  draft = "";
2626
2840
  if (!ledger.busy) busy = false;
@@ -2629,6 +2843,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2629
2843
  cancel() {
2630
2844
  call += 1;
2631
2845
  editing = false;
2846
+ modified = false;
2847
+ editNeedsPreview = false;
2632
2848
  failure = void 0;
2633
2849
  if (!ledger.busy) busy = false;
2634
2850
  publish();
@@ -2820,11 +3036,11 @@ function createContinueModel(host) {
2820
3036
  load(sessionId) {
2821
3037
  if (sessionId === "" || lifetime.signal.aborted || inflight.has(sessionId)) return;
2822
3038
  const current = read(sessionId);
2823
- if (current.text !== void 0 && current.seq === void 0) return;
3039
+ if (current.text !== void 0 && current.seq === void 0 && current.pending !== true) return;
2824
3040
  const ticket = new AbortController();
2825
3041
  inflight.set(sessionId, ticket);
2826
3042
  const signal = AbortSignal.any([lifetime.signal, ticket.signal]);
2827
- callContinueSuggest(host, sessionId, current.seq, signal).then((value) => {
3043
+ callContinueSuggest(host, sessionId, current.text ? current.seq : void 0, signal).then((value) => {
2828
3044
  if (signal.aborted || inflight.get(sessionId) !== ticket) return;
2829
3045
  write(sessionId, value);
2830
3046
  }).catch(() => {}).finally(() => {
@@ -2863,7 +3079,9 @@ async function callContinueSuggest(host, sessionId, knownSeq, signal) {
2863
3079
  const seq = typeof value?.seq === "number" && Number.isSafeInteger(value.seq) && value.seq >= 0 ? value.seq : void 0;
2864
3080
  return {
2865
3081
  text,
2866
- ...seq === void 0 ? {} : { seq }
3082
+ ...seq === void 0 ? {} : { seq },
3083
+ ...value?.pending === true ? { pending: true } : {},
3084
+ ...value?.fallback === true ? { fallback: true } : {}
2867
3085
  };
2868
3086
  }
2869
3087
  function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
@@ -3320,8 +3538,10 @@ function ContinueGhost(props) {
3320
3538
  const blank = props.useSession?.((state) => state.blank) ?? true;
3321
3539
  const running = props.useSessionStatus?.((state) => state.get(sessionId)?.running === true) ?? false;
3322
3540
  const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, () => props.model.getSnapshot(sessionId), () => props.model.getSnapshot(sessionId));
3323
- const text = snapshot.text ?? uiText(props.locale, "continueDefault");
3324
- const active = sessionId !== "" && !blank && bare && draft === "" && phase === "plain";
3541
+ const shown = running || snapshot.pending === true || snapshot.text === void 0 ? "" : snapshot.text ?? (snapshot.fallback === true ? uiText(props.locale, "continueDefault") : "");
3542
+ const idle = sessionId !== "" && !blank && bare && draft === "" && phase === "plain" && !running;
3543
+ const active = shown !== "" && idle;
3544
+ const text = shown;
3325
3545
  const latest = (0, react.useRef)({
3326
3546
  active,
3327
3547
  text
@@ -3337,18 +3557,18 @@ function ContinueGhost(props) {
3337
3557
  const finished = wasRunning.current && !running;
3338
3558
  wasRunning.current = running;
3339
3559
  if (finished && sessionId !== "") props.model.invalidate(sessionId);
3340
- if (active && !running) props.model.load(sessionId);
3560
+ if (idle && !running) props.model.load(sessionId);
3341
3561
  }, [
3342
3562
  props.model,
3343
- active,
3563
+ idle,
3344
3564
  running,
3345
3565
  sessionId
3346
3566
  ]);
3347
3567
  (0, react.useEffect)(() => {
3348
- if (active && !running && snapshot.text === void 0) props.model.load(sessionId);
3568
+ if (idle && !running && snapshot.text === void 0) props.model.load(sessionId);
3349
3569
  }, [
3350
3570
  props.model,
3351
- active,
3571
+ idle,
3352
3572
  running,
3353
3573
  sessionId,
3354
3574
  snapshot
@@ -3644,117 +3864,135 @@ function SessionManagerSettings(props) {
3644
3864
  };
3645
3865
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3646
3866
  className: `${FREE_CHAT_ROOT} dsh-ui-page`,
3647
- children: [
3648
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3649
- className: "dsh-ui-card",
3867
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3868
+ className: "dsm-settings",
3869
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3870
+ className: "dsm-setting",
3871
+ "aria-labelledby": `${tabsId}-ghost-heading`,
3650
3872
  children: [
3651
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3652
- className: "dsh-ui-heading",
3653
- children: copy("settingsMode")
3654
- }),
3655
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3656
- className: "dsh-ui-hint",
3657
- children: copy("settingsModeHint")
3658
- }),
3659
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
3660
- label: copy("settingsMode"),
3661
- items: [{
3662
- id: `${tabsId}-fixed`,
3663
- panelId: fixedPanelId,
3664
- value: "fixed",
3665
- label: copy("modeFixed")
3666
- }, {
3667
- id: `${tabsId}-auto`,
3668
- panelId: autoPanelId,
3669
- value: "auto",
3670
- label: copy("modeAuto")
3671
- }],
3672
- value: draft.continueMode,
3673
- onChange: (value) => edit({
3674
- ...draft,
3675
- continueMode: value
3676
- })
3677
- }) }),
3678
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3679
- role: "tabpanel",
3680
- id: fixedPanelId,
3681
- "aria-labelledby": `${tabsId}-fixed`,
3682
- hidden: draft.continueMode !== "fixed",
3683
- children: draft.continueMode === "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3684
- className: "dsh-ui-field",
3685
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3686
- className: "dsh-ui-label",
3687
- children: copy("continueTextLabel")
3688
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
3689
- value: draft.continueText,
3690
- placeholder: copy("continueTextPlaceholder"),
3691
- onChange: (event) => edit({
3692
- ...draft,
3693
- continueText: event.target.value
3694
- })
3695
- })]
3696
- }) : null
3873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
3874
+ className: "dsm-setting-header",
3875
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3876
+ id: `${tabsId}-ghost-heading`,
3877
+ className: "dsh-ui-title",
3878
+ children: copy("settingsMode")
3879
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3880
+ className: "dsh-ui-hint",
3881
+ children: copy("settingsModeHint")
3882
+ })]
3697
3883
  }),
3698
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3699
- role: "tabpanel",
3700
- id: autoPanelId,
3701
- "aria-labelledby": `${tabsId}-auto`,
3702
- hidden: draft.continueMode !== "auto",
3703
- children: draft.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3704
- className: "dsh-ui-field",
3705
- children: [
3706
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3884
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3885
+ className: "dsm-ghost-controls",
3886
+ children: [
3887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3888
+ className: "dsh-ui-field",
3889
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3707
3890
  className: "dsh-ui-label",
3708
- children: copy("ghostModelLabel")
3709
- }),
3710
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3711
- locale: props.locale,
3712
- open: ghostMenuOpen,
3713
- onOpenChange: setGhostMenuOpen,
3714
- listClassName: FREE_CHAT_ROOT,
3715
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3716
- type: "button",
3717
- className: "dsh-model-menu-trigger",
3718
- "aria-label": `${copy("ghostModelLabel")}: ${ghostLabel}`,
3719
- "aria-haspopup": "menu",
3720
- "aria-expanded": ghostMenuOpen,
3721
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3722
- className: "dsh-model-menu-triggerLabel",
3723
- children: ghostLabel
3724
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${ghostMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
3725
- }),
3726
- choices: selectableChoices,
3727
- selected: ghostRoute,
3728
- onPick: (route) => edit({
3729
- ...draft,
3730
- ghostModel: modelRouteKey(route.provider, route.model)
3731
- }),
3732
- leading: [{
3733
- id: "follow",
3734
- label: copy("modelFollowOption"),
3735
- selected: !ghostRoute
3891
+ children: copy("continueSourceLabel")
3892
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
3893
+ label: copy("continueSourceLabel"),
3894
+ items: [{
3895
+ id: `${tabsId}-fixed`,
3896
+ panelId: fixedPanelId,
3897
+ value: "fixed",
3898
+ label: copy("modeFixed")
3899
+ }, {
3900
+ id: `${tabsId}-auto`,
3901
+ panelId: autoPanelId,
3902
+ value: "auto",
3903
+ label: copy("modeAuto")
3736
3904
  }],
3737
- onPickLeading: () => edit({
3905
+ value: draft.continueMode,
3906
+ onChange: (value) => edit({
3738
3907
  ...draft,
3739
- ghostModel: ""
3740
- }),
3741
- modelValue: ghostLabel
3742
- }) }),
3743
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3744
- className: "dsh-ui-hint",
3745
- children: copy("ghostModelHint")
3746
- })
3747
- ]
3748
- }) : null
3749
- })
3908
+ continueMode: value
3909
+ })
3910
+ })]
3911
+ }),
3912
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3913
+ role: "tabpanel",
3914
+ id: fixedPanelId,
3915
+ "aria-labelledby": `${tabsId}-fixed`,
3916
+ hidden: draft.continueMode !== "fixed",
3917
+ children: draft.continueMode === "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3918
+ className: "dsh-ui-field",
3919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3920
+ className: "dsh-ui-label",
3921
+ children: copy("continueTextLabel")
3922
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
3923
+ className: "dsm-ghost-input",
3924
+ value: draft.continueText,
3925
+ placeholder: copy("continueTextPlaceholder"),
3926
+ onChange: (event) => edit({
3927
+ ...draft,
3928
+ continueText: event.target.value
3929
+ })
3930
+ })]
3931
+ }) : null
3932
+ }),
3933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3934
+ role: "tabpanel",
3935
+ id: autoPanelId,
3936
+ "aria-labelledby": `${tabsId}-auto`,
3937
+ hidden: draft.continueMode !== "auto",
3938
+ children: draft.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3939
+ className: "dsh-ui-field dsm-model-field",
3940
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3941
+ className: "dsh-ui-label",
3942
+ children: copy("ghostModelLabel")
3943
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3944
+ locale: props.locale,
3945
+ open: ghostMenuOpen,
3946
+ onOpenChange: setGhostMenuOpen,
3947
+ listClassName: FREE_CHAT_ROOT,
3948
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3949
+ type: "button",
3950
+ className: "dsh-model-menu-trigger",
3951
+ "aria-label": `${copy("ghostModelLabel")}: ${ghostLabel}`,
3952
+ "aria-haspopup": "menu",
3953
+ "aria-expanded": ghostMenuOpen,
3954
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3955
+ className: "dsh-model-menu-triggerLabel",
3956
+ children: ghostLabel
3957
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${ghostMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
3958
+ }),
3959
+ choices: selectableChoices,
3960
+ selected: ghostRoute,
3961
+ onPick: (route) => edit({
3962
+ ...draft,
3963
+ ghostModel: modelRouteKey(route.provider, route.model)
3964
+ }),
3965
+ leading: [{
3966
+ id: "follow",
3967
+ label: copy("modelFollowOption"),
3968
+ selected: !ghostRoute
3969
+ }],
3970
+ onPickLeading: () => edit({
3971
+ ...draft,
3972
+ ghostModel: ""
3973
+ }),
3974
+ modelValue: ghostLabel
3975
+ })]
3976
+ }) : null
3977
+ })
3978
+ ]
3979
+ }),
3980
+ draft.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3981
+ className: "dsh-ui-hint",
3982
+ children: copy("ghostModelHint")
3983
+ }) : null
3750
3984
  ]
3751
- }),
3752
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3753
- className: "dsh-ui-card",
3985
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3986
+ className: "dsm-setting",
3987
+ "aria-labelledby": `${tabsId}-return-heading`,
3754
3988
  children: [
3755
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3756
- className: "dsh-ui-heading",
3757
- children: copy("welcomeBackGroup")
3989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
3990
+ className: "dsm-setting-header",
3991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3992
+ id: `${tabsId}-return-heading`,
3993
+ className: "dsh-ui-title",
3994
+ children: copy("welcomeBackGroup")
3995
+ })
3758
3996
  }),
3759
3997
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3760
3998
  className: "dsm-feature",
@@ -3778,84 +4016,82 @@ function SessionManagerSettings(props) {
3778
4016
  })
3779
4017
  })]
3780
4018
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3781
- className: "dsm-subs",
4019
+ className: "dsm-feature-options",
3782
4020
  "data-disabled": draft.recapEnabled ? void 0 : "true",
3783
- children: [
3784
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4021
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4022
+ className: "dsh-ui-fields-grid",
4023
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3785
4024
  className: "dsh-ui-field",
3786
4025
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3787
4026
  className: "dsh-ui-label",
3788
4027
  children: copy("recapMinutesLabel")
3789
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
3790
- className: "dsm-minutes",
3791
- value: draft.recapMinutes,
3792
- inputMode: "numeric",
3793
- disabled: !draft.recapEnabled,
3794
- "aria-invalid": recapInvalid || void 0,
3795
- "aria-describedby": recapInvalid ? `${tabsId}-recap-error` : void 0,
3796
- onChange: (event) => edit({
3797
- ...draft,
3798
- recapMinutes: event.target.value
3799
- })
4028
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4029
+ className: "dsm-minutes-control",
4030
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
4031
+ className: "dsm-minutes",
4032
+ value: draft.recapMinutes,
4033
+ inputMode: "numeric",
4034
+ disabled: !draft.recapEnabled,
4035
+ "aria-invalid": recapInvalid || void 0,
4036
+ "aria-describedby": recapInvalid ? `${tabsId}-recap-error` : void 0,
4037
+ onChange: (event) => edit({
4038
+ ...draft,
4039
+ recapMinutes: event.target.value
4040
+ })
4041
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4042
+ className: "dsh-ui-meta",
4043
+ children: copy("minutesUnit")
4044
+ })]
3800
4045
  })]
3801
4046
  }), recapInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3802
4047
  id: `${tabsId}-recap-error`,
3803
4048
  className: "dsh-ui-error",
3804
4049
  role: "alert",
3805
4050
  children: copy("minutesInvalid")
3806
- }) : null] }),
3807
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3808
- className: "dsh-ui-field",
3809
- children: [
3810
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3811
- className: "dsh-ui-label",
3812
- children: copy("modelLabel")
4051
+ }) : null] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4052
+ className: "dsh-ui-field dsm-model-field",
4053
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4054
+ className: "dsh-ui-label",
4055
+ children: copy("modelLabel")
4056
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
4057
+ locale: props.locale,
4058
+ open: menuOpen,
4059
+ onOpenChange: setMenuOpen,
4060
+ listClassName: FREE_CHAT_ROOT,
4061
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4062
+ type: "button",
4063
+ className: "dsh-model-menu-trigger",
4064
+ disabled: !draft.recapEnabled,
4065
+ "aria-label": `${copy("modelLabel")}: ${selectedLabel}`,
4066
+ "aria-haspopup": "menu",
4067
+ "aria-expanded": menuOpen,
4068
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4069
+ className: "dsh-model-menu-triggerLabel",
4070
+ children: selectedLabel
4071
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
3813
4072
  }),
3814
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3815
- locale: props.locale,
3816
- open: menuOpen,
3817
- onOpenChange: setMenuOpen,
3818
- listClassName: FREE_CHAT_ROOT,
3819
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3820
- type: "button",
3821
- className: "dsh-model-menu-trigger",
3822
- disabled: !draft.recapEnabled,
3823
- "aria-label": `${copy("modelLabel")}: ${selectedLabel}`,
3824
- "aria-haspopup": "menu",
3825
- "aria-expanded": menuOpen,
3826
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3827
- className: "dsh-model-menu-triggerLabel",
3828
- children: selectedLabel
3829
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
3830
- }),
3831
- choices: selectableChoices,
3832
- selected: selectedRoute,
3833
- onPick: (route) => edit({
3834
- ...draft,
3835
- model: modelRouteKey(route.provider, route.model)
3836
- }),
3837
- leading: [{
3838
- id: "follow",
3839
- label: copy("modelFollowOption"),
3840
- selected: !selectedRoute
3841
- }],
3842
- onPickLeading: () => edit({
3843
- ...draft,
3844
- model: ""
3845
- }),
3846
- modelValue: selectedLabel
3847
- }) }),
3848
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3849
- className: "dsh-ui-hint",
3850
- children: copy("modelGroupHint")
3851
- })
3852
- ]
3853
- }),
3854
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
3855
- state: catalog,
3856
- locale: props.locale
3857
- })
3858
- ]
4073
+ choices: selectableChoices,
4074
+ selected: selectedRoute,
4075
+ onPick: (route) => edit({
4076
+ ...draft,
4077
+ model: modelRouteKey(route.provider, route.model)
4078
+ }),
4079
+ leading: [{
4080
+ id: "follow",
4081
+ label: copy("modelFollowOption"),
4082
+ selected: !selectedRoute
4083
+ }],
4084
+ onPickLeading: () => edit({
4085
+ ...draft,
4086
+ model: ""
4087
+ }),
4088
+ modelValue: selectedLabel
4089
+ })]
4090
+ })]
4091
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
4092
+ state: catalog,
4093
+ locale: props.locale
4094
+ })]
3859
4095
  })]
3860
4096
  }),
3861
4097
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3880,24 +4116,30 @@ function SessionManagerSettings(props) {
3880
4116
  })
3881
4117
  })]
3882
4118
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3883
- className: "dsm-subs",
4119
+ className: "dsm-feature-options dsm-compact-options",
3884
4120
  "data-disabled": draft.compactEnabled ? void 0 : "true",
3885
4121
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3886
4122
  className: "dsh-ui-field",
3887
4123
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3888
4124
  className: "dsh-ui-label",
3889
4125
  children: copy("compactMinutesLabel")
3890
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
3891
- className: "dsm-minutes",
3892
- value: draft.compactMinutes,
3893
- inputMode: "numeric",
3894
- disabled: !draft.compactEnabled,
3895
- "aria-invalid": compactInvalid || void 0,
3896
- "aria-describedby": compactInvalid ? `${tabsId}-compact-error` : void 0,
3897
- onChange: (event) => edit({
3898
- ...draft,
3899
- compactMinutes: event.target.value
3900
- })
4126
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4127
+ className: "dsm-minutes-control",
4128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
4129
+ className: "dsm-minutes",
4130
+ value: draft.compactMinutes,
4131
+ inputMode: "numeric",
4132
+ disabled: !draft.compactEnabled,
4133
+ "aria-invalid": compactInvalid || void 0,
4134
+ "aria-describedby": compactInvalid ? `${tabsId}-compact-error` : void 0,
4135
+ onChange: (event) => edit({
4136
+ ...draft,
4137
+ compactMinutes: event.target.value
4138
+ })
4139
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4140
+ className: "dsh-ui-meta",
4141
+ children: copy("minutesUnit")
4142
+ })]
3901
4143
  })]
3902
4144
  }), compactInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3903
4145
  id: `${tabsId}-compact-error`,
@@ -3908,39 +4150,215 @@ function SessionManagerSettings(props) {
3908
4150
  })]
3909
4151
  })
3910
4152
  ]
3911
- }),
3912
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3913
- className: "dsh-ui-form-footer",
3914
- children: [
3915
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3916
- variant: "primary",
3917
- disabled: invalid || snapshot.saving,
3918
- onClick: save,
3919
- children: snapshot.saving ? copy("settingsSaving") : copy("saveSettings")
3920
- }),
3921
- snapshot.saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3922
- className: "dsh-ui-notice",
3923
- role: "status",
3924
- children: copy("settingsSaved")
3925
- }) : null,
3926
- externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3927
- className: "dsh-ui-notice dsh-ui-notice--error",
3928
- role: "alert",
3929
- children: copy("externalChange")
3930
- }) : null,
3931
- snapshot.failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3932
- className: "dsh-ui-notice dsh-ui-notice--error",
3933
- role: "alert",
3934
- children: failureText(props.locale, snapshot.failure)
3935
- }) : null
3936
- ]
3937
- })
3938
- ]
4153
+ })]
4154
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4155
+ className: "dsh-ui-form-footer dsm-settings-footer",
4156
+ children: [
4157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4158
+ variant: "primary",
4159
+ disabled: invalid || snapshot.saving,
4160
+ onClick: save,
4161
+ children: snapshot.saving ? copy("settingsSaving") : copy("saveSettings")
4162
+ }),
4163
+ snapshot.saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4164
+ className: "dsh-ui-notice",
4165
+ role: "status",
4166
+ children: copy("settingsSaved")
4167
+ }) : null,
4168
+ externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4169
+ className: "dsh-ui-notice dsh-ui-notice--error",
4170
+ role: "alert",
4171
+ children: copy("externalChange")
4172
+ }) : null,
4173
+ snapshot.failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4174
+ className: "dsh-ui-notice dsh-ui-notice--error",
4175
+ role: "alert",
4176
+ children: failureText(props.locale, snapshot.failure)
4177
+ }) : null
4178
+ ]
4179
+ })]
4180
+ });
4181
+ }
4182
+ //#endregion
4183
+ //#region src/message-editor.tsx
4184
+ function AttachmentPreview(props) {
4185
+ const [src, setSrc] = (0, react.useState)();
4186
+ const [failed, setFailed] = (0, react.useState)(false);
4187
+ const item = props.item;
4188
+ const name = item.type === "upload" ? item.file.name : item.type === "file" ? item.name : uiText(props.locale, "editImage");
4189
+ (0, react.useEffect)(() => {
4190
+ let active = true;
4191
+ let url;
4192
+ setSrc(void 0);
4193
+ setFailed(false);
4194
+ if (item.type === "upload" && item.file.type.startsWith("image/")) {
4195
+ url = URL.createObjectURL(item.file);
4196
+ setSrc(url);
4197
+ } else if (item.type === "image" && props.host.attachment) unwrap(props.host.attachment({
4198
+ sessionId: props.sessionId,
4199
+ attachmentId: item.attachmentId
4200
+ })).then((value) => {
4201
+ const image = value;
4202
+ if (active && image.data && image.attachment?.mediaType?.startsWith("image/")) setSrc(`data:${image.attachment.mediaType};base64,${image.data}`);
4203
+ else if (active) setFailed(true);
4204
+ }).catch(() => {
4205
+ if (active) setFailed(true);
4206
+ });
4207
+ return () => {
4208
+ active = false;
4209
+ if (url) URL.revokeObjectURL(url);
4210
+ };
4211
+ }, [
4212
+ item,
4213
+ props.host,
4214
+ props.sessionId
4215
+ ]);
4216
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4217
+ src ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4218
+ src,
4219
+ alt: name
4220
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.FileTypeIcon, { path: name }),
4221
+ item.type === "file" || item.type === "upload" && !item.file.type.startsWith("image/") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4222
+ title: name,
4223
+ children: name
4224
+ }) : null,
4225
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4226
+ role: "status",
4227
+ children: uiText(props.locale, "editPreviewFailed")
4228
+ }) : null
4229
+ ] });
4230
+ }
4231
+ /** Lives in the original bubble; drafts and attachments remain local until Send. */
4232
+ function MessageEditor(props) {
4233
+ const textarea = (0, react.useRef)(null);
4234
+ const copy = (key) => uiText(props.locale, key);
4235
+ const attachments = props.attachments ?? [];
4236
+ const disabled = props.busy || props.locked === true;
4237
+ (0, react.useEffect)(() => {
4238
+ textarea.current?.focus();
4239
+ textarea.current?.setSelectionRange(props.text.length, props.text.length);
4240
+ }, []);
4241
+ (0, react.useEffect)(() => {
4242
+ const element = textarea.current;
4243
+ if (!element) return;
4244
+ element.style.height = "auto";
4245
+ element.style.height = `${Math.max(80, Math.min(element.scrollHeight, 360))}px`;
4246
+ }, [props.text]);
4247
+ const add = (files) => {
4248
+ if (!disabled && files.length > 0) props.setAttachments?.([...attachments, ...files.map((file) => ({
4249
+ type: "upload",
4250
+ file
4251
+ }))]);
4252
+ };
4253
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4254
+ className: "dsm-message-edit",
4255
+ role: "group",
4256
+ "aria-label": copy("editTitle"),
4257
+ onDragOver: (event) => {
4258
+ if (!disabled && props.setAttachments && event.dataTransfer.types.includes("Files")) event.preventDefault();
4259
+ },
4260
+ onDrop: (event) => {
4261
+ if (props.setAttachments && event.dataTransfer.files.length) {
4262
+ event.preventDefault();
4263
+ add(Array.from(event.dataTransfer.files));
4264
+ }
4265
+ },
4266
+ children: [attachments.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4267
+ className: "dsm-edit-attachments",
4268
+ children: attachments.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4269
+ className: `dsm-edit-attachment ${item.type === "image" || item.type === "upload" && item.file.type.startsWith("image/") ? "dsm-edit-image" : ""}`,
4270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentPreview, {
4271
+ item,
4272
+ sessionId: props.sessionId,
4273
+ host: props.host ?? {},
4274
+ locale: props.locale
4275
+ }), props.setAttachments ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4276
+ size: "sm",
4277
+ variant: "ghost",
4278
+ disabled,
4279
+ "aria-label": `${copy("editRemoveAttachment")} ${index + 1}`,
4280
+ onClick: () => {
4281
+ props.setAttachments?.(attachments.filter((_, position) => position !== index));
4282
+ },
4283
+ children: "×"
4284
+ }) : null]
4285
+ }, item.type === "upload" ? `${index}:${item.file.name}` : `${index}:${item.attachmentId}`))
4286
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4287
+ className: "dsm-message-editor",
4288
+ children: [
4289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4290
+ ref: textarea,
4291
+ className: "dsh-ui-textarea dsh-free-draft",
4292
+ "aria-label": copy("draft"),
4293
+ value: props.text,
4294
+ disabled,
4295
+ onChange: (event) => {
4296
+ props.setText(event.target.value);
4297
+ },
4298
+ onPaste: (event) => {
4299
+ const files = Array.from(event.clipboardData.files);
4300
+ if (files.length && props.setAttachments) {
4301
+ event.preventDefault();
4302
+ add(files);
4303
+ }
4304
+ },
4305
+ onKeyDown: (event) => {
4306
+ if (event.key === "Escape" && !props.busy) {
4307
+ event.stopPropagation();
4308
+ props.onCancel();
4309
+ }
4310
+ }
4311
+ }),
4312
+ props.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4313
+ className: "dsh-ui-error",
4314
+ role: "alert",
4315
+ children: props.error
4316
+ }) : null,
4317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4318
+ className: "dsm-edit-footer",
4319
+ children: [
4320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsm-edit-spacer" }),
4321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4322
+ variant: "ghost",
4323
+ disabled: props.busy,
4324
+ onClick: props.onCancel,
4325
+ children: copy("cancel")
4326
+ }),
4327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4328
+ variant: "primary",
4329
+ disabled: props.busy || !props.text.trim() && !attachments.length,
4330
+ onClick: props.onSend,
4331
+ children: copy("send")
4332
+ })
4333
+ ]
4334
+ })
4335
+ ]
4336
+ })]
4337
+ });
4338
+ }
4339
+ //#endregion
4340
+ //#region src/edit-icon.tsx
4341
+ /** The stock edit primitive includes a box; message editing uses a standalone pencil. */
4342
+ function EditMessageIcon() {
4343
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4344
+ width: "16",
4345
+ height: "16",
4346
+ viewBox: "0 0 16 16",
4347
+ fill: "none",
4348
+ "aria-hidden": "true",
4349
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4350
+ d: "m10.5 2.5 3 3M2 14l1-4L10.5 2.5a2.12 2.12 0 0 1 3 3L6 13l-4 1Z",
4351
+ stroke: "currentColor",
4352
+ strokeWidth: "1",
4353
+ strokeLinecap: "round",
4354
+ strokeLinejoin: "round"
4355
+ })
3939
4356
  });
3940
4357
  }
3941
4358
  //#endregion
3942
4359
  //#region src/user-actions.tsx
3943
4360
  function UserMessageActions(props) {
4361
+ const [confirming, setConfirming] = (0, react.useState)(false);
3944
4362
  const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
3945
4363
  const signal = (0, react.useRef)(new AbortController());
3946
4364
  (0, react.useEffect)(() => {
@@ -3964,75 +4382,624 @@ function UserMessageActions(props) {
3964
4382
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3965
4383
  className: `${FREE_CHAT_ROOT} dsh-free-actions`,
3966
4384
  children: [
3967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3968
- label: copy("retry"),
3969
- side: "bottom",
3970
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3971
- size: "sm",
3972
- variant: "ghost",
3973
- "aria-label": copy("retry"),
3974
- disabled: snapshot.busy,
3975
- onClick: () => {
3976
- props.model.retry(props.sessionId, props.messageId, signal.current.signal);
3977
- },
3978
- children: copy("retry")
3979
- })
3980
- }),
3981
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4385
+ !props.hideEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3982
4386
  label: copy("edit"),
3983
4387
  side: "bottom",
3984
4388
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3985
4389
  size: "sm",
4390
+ className: "dsm-icon-action",
3986
4391
  variant: "ghost",
3987
4392
  "aria-label": copy("edit"),
3988
- disabled: snapshot.busy,
4393
+ disabled: snapshot.busy || snapshot.editing && !mine,
3989
4394
  onClick: () => {
3990
4395
  props.model.beginEdit(props.sessionId, props.messageId, props.text, signal.current.signal);
3991
4396
  },
3992
- children: copy("edit")
4397
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditMessageIcon, {})
3993
4398
  })
3994
- }),
3995
- failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4399
+ }) : null,
4400
+ failure && !editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3996
4401
  className: "dsh-ui-error dsh-free-alert",
3997
4402
  role: "alert",
3998
4403
  children: failureText(props.locale, failure)
3999
4404
  }) : null,
4405
+ editing && !props.hideEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageEditor, {
4406
+ text: draft,
4407
+ setText: (value) => {
4408
+ props.model.setDraft(value);
4409
+ },
4410
+ sessionId: props.sessionId,
4411
+ locale: props.locale,
4412
+ busy: snapshot.busy,
4413
+ error: failure ? failureText(props.locale, failure) : void 0,
4414
+ onCancel: () => {
4415
+ props.model.cancelEdit();
4416
+ },
4417
+ onSend: () => {
4418
+ setConfirming(true);
4419
+ }
4420
+ }) : null,
4000
4421
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4001
- open: editing,
4422
+ open: confirming && editing,
4002
4423
  className: FREE_CHAT_ROOT,
4003
4424
  title: copy("editTitle"),
4004
4425
  closeLabel: copy("close"),
4005
4426
  onClose: () => {
4006
- props.model.cancelEdit();
4427
+ setConfirming(false);
4007
4428
  },
4008
4429
  footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4009
4430
  variant: "ghost",
4010
- disabled: snapshot.busy,
4431
+ "data-modal-autofocus": true,
4011
4432
  onClick: () => {
4012
- props.model.cancelEdit();
4433
+ setConfirming(false);
4013
4434
  },
4014
4435
  children: copy("cancel")
4015
4436
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4016
4437
  variant: "primary",
4017
- disabled: snapshot.busy,
4018
4438
  onClick: () => {
4019
- props.model.sendEdit(props.sessionId, props.messageId, signal.current.signal);
4439
+ setConfirming(false);
4440
+ props.model.sendEdit(props.sessionId, props.messageId, signal.current.signal, void 0, true);
4020
4441
  },
4021
- children: copy("send")
4442
+ children: copy("editConfirm")
4022
4443
  })] }),
4023
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4024
- className: "dsh-ui-textarea dsh-free-draft",
4025
- "data-modal-autofocus": true,
4026
- "aria-label": copy("draft"),
4027
- value: draft,
4028
- onChange: (event) => {
4029
- props.model.setDraft(event.target.value);
4030
- }
4444
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy("editBranchWarning") })
4445
+ })
4446
+ ]
4447
+ });
4448
+ }
4449
+ //#endregion
4450
+ //#region src/edit-host.ts
4451
+ function bindEditHost(client) {
4452
+ const rpc = client.connection?.rpc;
4453
+ const remote = client.remote?.session;
4454
+ return {
4455
+ ...typeof rpc?.call === "function" ? {
4456
+ prepare: (request, signal) => rpc.call(TEMP_WORKSPACE_CHANNEL, EDIT_PREPARE_ENDPOINT, request, signal),
4457
+ stageFile: (request, signal) => rpc.call(TEMP_WORKSPACE_CHANNEL, EDIT_FILE_ENDPOINT, request, signal)
4458
+ } : {},
4459
+ uploadFile: async (sessionId, file, signal) => {
4460
+ const query = new URLSearchParams({
4461
+ sessionId,
4462
+ name: file.name
4463
+ });
4464
+ const response = await fetch(`/api/session/uploadFileBinary?${query}`, {
4465
+ method: "POST",
4466
+ credentials: "same-origin",
4467
+ headers: { "content-type": "application/octet-stream" },
4468
+ body: file,
4469
+ signal
4470
+ });
4471
+ if (!response.ok) throw new HostOperationError("session/attachment-invalid");
4472
+ return response.json();
4473
+ },
4474
+ ...typeof remote?.attachment === "function" ? { attachment: (request) => remote.attachment(request) } : {}
4475
+ };
4476
+ }
4477
+ function readPreview(value) {
4478
+ if (typeof value !== "object" || value === null) throw new HostOperationError("session-manager/unavailable");
4479
+ const record = value;
4480
+ if (typeof record.sessionId !== "string" || typeof record.promptSeq !== "number" || typeof record.text !== "string" || typeof record.modified !== "boolean") throw new HostOperationError("session-manager/unavailable");
4481
+ const content = record.content;
4482
+ if (content !== void 0 && (!Array.isArray(content) || !content.every(isEditContent))) throw new HostOperationError("session-manager/unavailable");
4483
+ return {
4484
+ sessionId: record.sessionId,
4485
+ promptSeq: record.promptSeq,
4486
+ text: record.text,
4487
+ ...content === void 0 ? {} : { content },
4488
+ modified: record.modified
4489
+ };
4490
+ }
4491
+ function isEditContent(value) {
4492
+ if (typeof value !== "object" || value === null) return false;
4493
+ const part = value;
4494
+ return part.type === "text" ? typeof part.text === "string" : (part.type === "image" || part.type === "file" && typeof part.name === "string" && typeof part.bytes === "number") && typeof part.attachmentId === "string" && part.attachmentId !== "";
4495
+ }
4496
+ /** Build the exact body for the edited prompt; images are read from the source Session. */
4497
+ async function buildContent(host, preview, text, signal, attachments) {
4498
+ const content = [];
4499
+ let wroteText = false;
4500
+ const parts = attachments === void 0 ? preview.content ?? [{
4501
+ type: "text",
4502
+ text: preview.text
4503
+ }] : [{
4504
+ type: "text",
4505
+ text
4506
+ }, ...attachments];
4507
+ for (const part of parts) {
4508
+ signal.throwIfAborted();
4509
+ if (part.type === "text") {
4510
+ if (text === preview.text) content.push(part);
4511
+ else if (!wroteText) content.push({
4512
+ type: "text",
4513
+ text
4514
+ });
4515
+ wroteText = true;
4516
+ continue;
4517
+ }
4518
+ if (part.type === "file") {
4519
+ content.push(part);
4520
+ continue;
4521
+ }
4522
+ if (part.type === "upload") {
4523
+ if ([
4524
+ "image/png",
4525
+ "image/jpeg",
4526
+ "image/webp",
4527
+ "image/gif"
4528
+ ].includes(part.file.type)) {
4529
+ const data = await new Promise((resolve, reject) => {
4530
+ const reader = new FileReader();
4531
+ reader.onload = () => {
4532
+ resolve(String(reader.result).split(",")[1]);
4533
+ };
4534
+ reader.onerror = () => {
4535
+ reject(reader.error);
4536
+ };
4537
+ reader.readAsDataURL(part.file);
4538
+ });
4539
+ signal.throwIfAborted();
4540
+ content.push({
4541
+ type: "image",
4542
+ data,
4543
+ mediaType: part.file.type,
4544
+ name: part.file.name
4545
+ });
4546
+ } else content.push(part);
4547
+ continue;
4548
+ }
4549
+ if (!host.attachment) throw new HostOperationError("session/lifecycle-unavailable");
4550
+ const image = await unwrap(host.attachment({
4551
+ sessionId: preview.sessionId,
4552
+ attachmentId: part.attachmentId
4553
+ }));
4554
+ if (image?.attachment?.attachmentId !== part.attachmentId || typeof image.data !== "string" || image.data === "" || typeof image.attachment.mediaType !== "string" || !image.attachment.mediaType.startsWith("image/")) throw new HostOperationError("session/attachment-invalid");
4555
+ content.push({
4556
+ type: "image",
4557
+ data: image.data,
4558
+ mediaType: image.attachment.mediaType,
4559
+ ...typeof image.attachment.name === "string" ? { name: image.attachment.name } : {}
4560
+ });
4561
+ }
4562
+ if (!wroteText && text !== "") content.push({
4563
+ type: "text",
4564
+ text
4565
+ });
4566
+ signal.throwIfAborted();
4567
+ return content;
4568
+ }
4569
+ /** Prepare the exact edit body for the existing Session; the host sends it on submit. */
4570
+ async function prepareEditContent(host, preview, text, submission, signal, attachments) {
4571
+ submission.prepared ??= await buildContent(host, preview, text, signal, attachments);
4572
+ await stageContent(host, preview, submission, preview.sessionId, signal);
4573
+ return submission.content;
4574
+ }
4575
+ async function stageContent(host, preview, submission, sessionId, signal) {
4576
+ const prepared = submission.prepared;
4577
+ if (submission.content !== void 0) return;
4578
+ const content = [];
4579
+ const receipts = submission.receipts ??= /* @__PURE__ */ new Map();
4580
+ for (const [index, part] of prepared.entries()) {
4581
+ signal.throwIfAborted();
4582
+ if (part.type !== "upload" && !(part.type === "file" && "attachmentId" in part)) {
4583
+ content.push(part);
4584
+ continue;
4585
+ }
4586
+ let receiptId = receipts.get(index);
4587
+ if (receiptId === void 0) {
4588
+ const result = part.type === "upload" ? host.uploadFile?.(sessionId, part.file, signal) : host.stageFile?.({
4589
+ sessionId: preview.sessionId,
4590
+ promptSeq: preview.promptSeq,
4591
+ attachmentId: part.attachmentId
4592
+ }, signal);
4593
+ if (!result) throw new HostOperationError("session/lifecycle-unavailable");
4594
+ const value = await unwrap(result);
4595
+ if (typeof value?.receiptId !== "string" || value.receiptId === "") throw new HostOperationError("session/attachment-invalid");
4596
+ receiptId = value.receiptId;
4597
+ receipts.set(index, receiptId);
4598
+ }
4599
+ content.push({
4600
+ type: "file",
4601
+ receiptId
4602
+ });
4603
+ }
4604
+ submission.content = content;
4605
+ }
4606
+ /** Read the exact human prompt that the editor edits. */
4607
+ async function previewEdit(host, sessionId, promptSeq, signal) {
4608
+ if (typeof host.prepare !== "function") throw new HostOperationError("session/lifecycle-unavailable");
4609
+ return readPreview(await unwrap(host.prepare({
4610
+ sessionId,
4611
+ promptSeq
4612
+ }, signal)));
4613
+ }
4614
+ //#endregion
4615
+ //#region src/edit-actions.tsx
4616
+ /** Edit one human prompt in its own Session; the host switches branches when the edit is sent. */
4617
+ function EditActions(props) {
4618
+ const sessionId = props.sessionId ?? "";
4619
+ const native = (0, react.useSyncExternalStore)(props.nativeRetry.subscribe, props.nativeRetry.getSnapshot, props.nativeRetry.getSnapshot);
4620
+ const [confirming, setConfirming] = (0, react.useState)(false);
4621
+ const [busy, setBusy] = (0, react.useState)(false);
4622
+ const [editing, setEditing] = (0, react.useState)(false);
4623
+ const [draft, setDraft] = (0, react.useState)("");
4624
+ const [attachments, setAttachments] = (0, react.useState)([]);
4625
+ const [mediaChanged, setMediaChanged] = (0, react.useState)(false);
4626
+ const [locked, setLocked] = (0, react.useState)(false);
4627
+ const [failure, setFailure] = (0, react.useState)();
4628
+ const preview = (0, react.useRef)();
4629
+ const submission = (0, react.useRef)();
4630
+ const signal = (0, react.useRef)(new AbortController());
4631
+ (0, react.useEffect)(() => {
4632
+ const controller = new AbortController();
4633
+ signal.current = controller;
4634
+ return () => {
4635
+ controller.abort();
4636
+ };
4637
+ }, [sessionId, props.promptSeq]);
4638
+ const allowed = props.useSessionAccess?.(sessionId, "session/prompt") ?? true;
4639
+ const copy = (key) => uiText(props.locale, key);
4640
+ const mine = native.sessionId === sessionId && native.messageId === props.messageId;
4641
+ (0, react.useEffect)(() => {
4642
+ if (editing && !busy && native.controlsRetry && !mine) setEditing(false);
4643
+ }, [
4644
+ editing,
4645
+ busy,
4646
+ native.controlsRetry,
4647
+ mine
4648
+ ]);
4649
+ if (!allowed || !sessionId || props.promptSeq === void 0 || !props.host.prepare) return props.render?.(null, null) ?? null;
4650
+ const run = async (action) => {
4651
+ if (busy || signal.current.signal.aborted) return;
4652
+ setBusy(true);
4653
+ setFailure(void 0);
4654
+ try {
4655
+ if (!native.controlsRetry || !props.messageId) throw new HostOperationError("session/lifecycle-unavailable");
4656
+ if (action === "edit") {
4657
+ const value = await previewEdit(props.host, sessionId, props.promptSeq, signal.current.signal);
4658
+ await props.nativeRetry.beginEdit(sessionId, props.messageId, value.text, signal.current.signal);
4659
+ const state = props.nativeRetry.getSnapshot();
4660
+ if (!state.editing) {
4661
+ setFailure(state.failure);
4662
+ return;
4663
+ }
4664
+ preview.current = value;
4665
+ submission.current = { requestId: (props.ids ?? (() => crypto.randomUUID()))() };
4666
+ setDraft(value.text);
4667
+ setAttachments((value.content ?? []).filter((part) => part.type !== "text"));
4668
+ setMediaChanged(false);
4669
+ setLocked(false);
4670
+ setEditing(true);
4671
+ return;
4672
+ }
4673
+ if (!preview.current || !submission.current) return;
4674
+ if (mediaChanged && native.controlsContent !== true) throw new HostOperationError("session/retry-media-unavailable");
4675
+ setLocked(true);
4676
+ const content = mediaChanged ? await prepareEditContent(props.host, preview.current, draft, submission.current, signal.current.signal, attachments) : void 0;
4677
+ props.nativeRetry.setDraft(draft);
4678
+ await props.nativeRetry.sendEdit(sessionId, props.messageId, signal.current.signal, content, true);
4679
+ const state = props.nativeRetry.getSnapshot();
4680
+ setFailure(state.failure);
4681
+ if (!state.editing) setEditing(false);
4682
+ else if (isStaleRetryFailure(state.failure)) setLocked(false);
4683
+ } catch (error) {
4684
+ if (!signal.current.signal.aborted) setFailure(readFailure(error));
4685
+ } finally {
4686
+ if (!signal.current.signal.aborted) setBusy(false);
4687
+ }
4688
+ };
4689
+ const cancel = () => {
4690
+ if (busy) return;
4691
+ props.nativeRetry.cancelEdit();
4692
+ setEditing(false);
4693
+ setFailure(void 0);
4694
+ };
4695
+ const invalidatePrepared = () => {
4696
+ if (submission.current) submission.current = { requestId: submission.current.requestId };
4697
+ };
4698
+ const actions = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4699
+ className: `${FREE_CHAT_ROOT} dsh-free-actions`,
4700
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4701
+ label: copy("edit"),
4702
+ side: "bottom",
4703
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4704
+ className: "dsm-icon-action",
4705
+ size: "sm",
4706
+ variant: "ghost",
4707
+ "aria-label": copy("edit"),
4708
+ disabled: busy || native.busy || native.editing && !mine,
4709
+ onClick: () => {
4710
+ run("edit");
4711
+ },
4712
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditMessageIcon, {})
4713
+ })
4714
+ }), failure && !editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4715
+ className: "dsh-ui-error dsh-free-alert",
4716
+ role: "alert",
4717
+ children: failureText(props.locale, failure)
4718
+ }) : null]
4719
+ });
4720
+ const editor = editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageEditor, {
4721
+ text: draft,
4722
+ setText: (text) => {
4723
+ invalidatePrepared();
4724
+ setDraft(text);
4725
+ },
4726
+ attachments,
4727
+ setAttachments: native.controlsContent ? (items) => {
4728
+ invalidatePrepared();
4729
+ setAttachments(items);
4730
+ setMediaChanged(true);
4731
+ } : void 0,
4732
+ sessionId,
4733
+ host: props.host,
4734
+ locale: props.locale,
4735
+ busy,
4736
+ locked,
4737
+ onCancel: cancel,
4738
+ onSend: () => {
4739
+ setConfirming(true);
4740
+ },
4741
+ error: failure ? failureText(props.locale, failure) : void 0
4742
+ }) : null;
4743
+ const content = props.render ? props.render(actions, editor) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [actions, editor] });
4744
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [content, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4745
+ open: confirming && editing,
4746
+ className: FREE_CHAT_ROOT,
4747
+ title: copy("editTitle"),
4748
+ closeLabel: copy("close"),
4749
+ onClose: () => {
4750
+ setConfirming(false);
4751
+ },
4752
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4753
+ variant: "ghost",
4754
+ "data-modal-autofocus": true,
4755
+ onClick: () => {
4756
+ setConfirming(false);
4757
+ },
4758
+ children: copy("cancel")
4759
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4760
+ variant: "primary",
4761
+ onClick: () => {
4762
+ setConfirming(false);
4763
+ run("send");
4764
+ },
4765
+ children: copy("editConfirm")
4766
+ })] }),
4767
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy("editBranchWarning") })
4768
+ })] });
4769
+ }
4770
+ //#endregion
4771
+ //#region src/user-node.tsx
4772
+ function contentParts(content) {
4773
+ const texts = [];
4774
+ const attachments = [];
4775
+ const rest = [];
4776
+ for (const block of content) if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
4777
+ else if (block.type === "image" && block.attachment !== void 0) attachments.push({
4778
+ type: "image",
4779
+ image: { attachment: block.attachment }
4780
+ });
4781
+ else if (block.type === "file" && block.attachment !== void 0) attachments.push({
4782
+ type: "file",
4783
+ file: block.attachment
4784
+ });
4785
+ else rest.push(block);
4786
+ return {
4787
+ text: texts.join(""),
4788
+ attachments,
4789
+ rest
4790
+ };
4791
+ }
4792
+ function pad2(value) {
4793
+ return String(value).padStart(2, "0");
4794
+ }
4795
+ function messageClock(time, t) {
4796
+ const date = new Date(time);
4797
+ const now = /* @__PURE__ */ new Date();
4798
+ const clock = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
4799
+ if (date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()) return clock;
4800
+ const params = {
4801
+ y: date.getFullYear(),
4802
+ m: date.getMonth() + 1,
4803
+ d: date.getDate()
4804
+ };
4805
+ return `${date.getFullYear() === now.getFullYear() ? t("clock.md", params) : t("clock.ymd", params)} ${clock}`;
4806
+ }
4807
+ function MessageChrome(props) {
4808
+ const [copied, setCopied] = (0, react.useState)(false);
4809
+ const timer = (0, react.useRef)(void 0);
4810
+ const pending = (0, react.useRef)(false);
4811
+ (0, react.useEffect)(() => () => {
4812
+ pending.current = false;
4813
+ if (timer.current !== void 0) clearTimeout(timer.current);
4814
+ }, []);
4815
+ const copy = () => {
4816
+ if (copied || pending.current) return;
4817
+ pending.current = true;
4818
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(props.text).then((ok) => {
4819
+ pending.current = false;
4820
+ if (!ok) return;
4821
+ setCopied(true);
4822
+ timer.current = setTimeout(() => {
4823
+ setCopied(false);
4824
+ }, 1e3);
4825
+ });
4826
+ };
4827
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4828
+ className: "dsm-user-chrome",
4829
+ children: [
4830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4831
+ className: "dsm-user-time",
4832
+ children: messageClock(props.time, props.t)
4833
+ }),
4834
+ props.children,
4835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4836
+ label: copied ? props.t("copied") : props.t("copy"),
4837
+ side: "bottom",
4838
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4839
+ size: "sm",
4840
+ variant: "ghost",
4841
+ className: "dsm-icon-action",
4842
+ "aria-label": copied ? props.t("copied") : props.t("copy"),
4843
+ onClick: copy,
4844
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutlineRegular, {})
4031
4845
  })
4032
4846
  })
4033
4847
  ]
4034
4848
  });
4035
4849
  }
4850
+ /** User-message bubble whose own action row edits the exact prompt in place. */
4851
+ function UserNodeWithEdit(props) {
4852
+ const { text, attachments, rest } = contentParts(props.node.data.content);
4853
+ const compactImages = attachments.length > 1;
4854
+ const truncated = (total) => props.t("json.truncated", { total });
4855
+ const data = props.node.data;
4856
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditActions, {
4857
+ promptSeq: data.seq,
4858
+ sessionId: props.sessionId,
4859
+ locale: props.locale,
4860
+ host: props.host,
4861
+ nativeRetry: props.nativeRetry,
4862
+ useSessionAccess: props.useSessionAccess,
4863
+ ids: props.ids,
4864
+ messageId: props.node.id,
4865
+ render: (actions, editor) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4866
+ className: `${FREE_CHAT_ROOT} dsm-user-row`,
4867
+ children: [editor, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4868
+ className: "dsm-user-original",
4869
+ hidden: Boolean(editor),
4870
+ children: [!props.renderMessageImages && props.renderFactorySlot ? props.renderFactorySlot("conversation.chat.user.content", {
4871
+ content: {
4872
+ text,
4873
+ attachments,
4874
+ extraBlocks: rest,
4875
+ ...data.referenceLabels === void 0 ? {} : { referenceLabels: data.referenceLabels }
4876
+ },
4877
+ loadImage: props.loadImage,
4878
+ facts: {
4879
+ ...data.referenceLabels === void 0 ? {} : { sessionLabels: data.referenceLabels },
4880
+ ...data.skillNames === void 0 ? {} : { skillNames: data.skillNames }
4881
+ },
4882
+ interactive: true
4883
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4884
+ className: "dsm-user-stack",
4885
+ children: [
4886
+ attachments.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4887
+ className: "dsm-user-attachments",
4888
+ "data-message-attachments": true,
4889
+ children: attachments.map((attachment, index) => attachment.type === "image" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: props.renderMessageImages?.({
4890
+ images: [attachment.image],
4891
+ align: "end",
4892
+ compact: compactImages
4893
+ }) }, `image:${index}`) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4894
+ className: "dsm-file-card",
4895
+ title: attachment.file.name,
4896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.FileTypeIcon, {
4897
+ path: attachment.file.name,
4898
+ className: "dsm-file-icon"
4899
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4900
+ className: "dsm-file-content",
4901
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4902
+ className: "dsm-file-name",
4903
+ children: attachment.file.name
4904
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4905
+ className: "dsm-file-meta",
4906
+ children: [(0, _deepseek_ai_dsh_client_ui_primitives.fileExtension)(attachment.file.name).toUpperCase().slice(0, 8), (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(attachment.file.bytes)].filter(Boolean).join(" ")
4907
+ })]
4908
+ })]
4909
+ }, `file:${index}`))
4910
+ }) : null,
4911
+ text !== "" || rest.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4912
+ className: "dsm-user-bubble",
4913
+ children: [text, rest.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
4914
+ label: props.t("message.extraBlock"),
4915
+ payload: block,
4916
+ truncatedLabel: truncated
4917
+ }, index))]
4918
+ }) : null,
4919
+ data.referenceLabels !== void 0 && data.referenceLabels.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4920
+ className: "dsm-reference-summary",
4921
+ children: props.t("message.referenceSummary", { labels: data.referenceLabels.join(props.t("message.referenceSeparator")) })
4922
+ }) : null
4923
+ ]
4924
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(MessageChrome, {
4925
+ text,
4926
+ time: data.time,
4927
+ t: props.t,
4928
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMessageActions, {
4929
+ messageId: props.node.id,
4930
+ text,
4931
+ sessionId: props.sessionId ?? "",
4932
+ locale: props.locale,
4933
+ model: props.nativeRetry,
4934
+ hideEdit: Boolean(actions)
4935
+ }), actions]
4936
+ })]
4937
+ })]
4938
+ })
4939
+ });
4940
+ }
4941
+ //#endregion
4942
+ //#region src/retry-contracts.ts
4943
+ const RETRY_CAPABILITIES_ENDPOINT = "retry.capabilities";
4944
+ const RETRY_PREVIEW_ENDPOINT = "retry.preview";
4945
+ const RETRY_COMMIT_ENDPOINT = "retry.commit";
4946
+ //#endregion
4947
+ //#region src/retry-host.ts
4948
+ function bindRetryHost(rpc) {
4949
+ const call = (endpoint, payload, signal) => {
4950
+ if (typeof rpc?.call !== "function") throw new HostOperationError("session/lifecycle-unavailable");
4951
+ return rpc.call(TEMP_WORKSPACE_CHANNEL, endpoint, payload, signal);
4952
+ };
4953
+ return {
4954
+ async capabilities() {
4955
+ const value = await unwrap(call(RETRY_CAPABILITIES_ENDPOINT, {}));
4956
+ if (!isCapabilities(value)) throw new HostOperationError("session-manager/unavailable");
4957
+ return value;
4958
+ },
4959
+ async preview(request, signal) {
4960
+ const value = await unwrap(call(RETRY_PREVIEW_ENDPOINT, request, signal));
4961
+ if (!isPreview(value)) throw new HostOperationError("session-manager/unavailable");
4962
+ return value;
4963
+ },
4964
+ async commit(request, signal) {
4965
+ const value = await unwrap(call(RETRY_COMMIT_ENDPOINT, request, signal));
4966
+ if (!isCommit(value)) throw new HostOperationError("session-manager/unavailable");
4967
+ return value;
4968
+ }
4969
+ };
4970
+ }
4971
+ /**
4972
+ * Commit then navigate: the forked child is the chat the user keeps, so the
4973
+ * browser follows a successful commit to the new session. `open` is absent
4974
+ * only when the host has no workspace navigation; the commit value returns
4975
+ * either way.
4976
+ */
4977
+ function retryCommitAndOpen(commit, open) {
4978
+ return async (request, signal) => {
4979
+ const value = await commit(request, signal);
4980
+ try {
4981
+ await open?.(value.sessionId);
4982
+ } catch {
4983
+ throw new HostOperationError("session/retry-pending");
4984
+ }
4985
+ return value;
4986
+ };
4987
+ }
4988
+ function isCapabilities(value) {
4989
+ if (typeof value !== "object" || value === null) return false;
4990
+ const item = value;
4991
+ return typeof item.retry === "boolean" && typeof item.retryModified === "boolean" && typeof item.retryContent === "boolean";
4992
+ }
4993
+ function isPreview(value) {
4994
+ if (typeof value !== "object" || value === null) return false;
4995
+ const item = value;
4996
+ return typeof item.sessionId === "string" && typeof item.messageId === "string" && Number.isSafeInteger(item.promptSeq) && typeof item.branchRevision === "number" && typeof item.inputRevision === "string" && typeof item.eligible === "boolean" && typeof item.modified === "boolean";
4997
+ }
4998
+ function isCommit(value) {
4999
+ if (typeof value !== "object" || value === null) return false;
5000
+ const item = value;
5001
+ return typeof item.sessionId === "string" && typeof item.sourceSessionId === "string" && typeof item.archived === "boolean";
5002
+ }
4036
5003
  //#endregion
4037
5004
  //#region src/client.tsx
4038
5005
  const name = "dsh-session-manager-client";
@@ -4052,7 +5019,14 @@ function apply(ctx) {
4052
5019
  mountModelSettingsChanges(client, PLUGIN_NAME);
4053
5020
  const host = bindHost(client);
4054
5021
  const chats = createFreeChatModel(host);
4055
- const retries = createRetryModel(host);
5022
+ const retryHost = bindRetryHost(client.connection?.rpc);
5023
+ const retries = createRetryModel({
5024
+ list: host.list,
5025
+ capabilities: () => retryHost.capabilities(),
5026
+ previewRetry: (request) => retryHost.preview(request),
5027
+ retry: retryCommitAndOpen(retryHost.commit, host.openSession)
5028
+ });
5029
+ const editHost = bindEditHost(client);
4056
5030
  const temporaryHost = bindTemporaryWorkspaceHost(client);
4057
5031
  const temporary = createTemporaryWorkspaceModel(temporaryHost, void 0, () => uiText(copyLocale(client.locale?.getSnapshot().active), "tempWorkspace"));
4058
5032
  const presenceHost = bindPresenceHost(client);
@@ -4101,8 +5075,22 @@ function apply(ctx) {
4101
5075
  locale: useLiveLocale(client.locale),
4102
5076
  model: continues
4103
5077
  }))));
4104
- stops.push(client.slots.inject(WELCOME_BACK_CARD_SLOT, () => client.slots.register({
4105
- name: WELCOME_BACK_CARD_SLOT,
5078
+ stops.push(client.slots.inject("conversation.chat.node", () => client.slots.register({
5079
+ name: "conversation.chat.node",
5080
+ key: "user",
5081
+ locale: "chat",
5082
+ priority: -10
5083
+ }, (props) => {
5084
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserNodeWithEdit, {
5085
+ ...props,
5086
+ sessionId: props.sessionId,
5087
+ locale: useLiveLocale(client.locale),
5088
+ host: editHost,
5089
+ nativeRetry: retries
5090
+ });
5091
+ })));
5092
+ stops.push(client.slots.inject(TURN_TAIL_SLOT, () => client.slots.register({
5093
+ name: TURN_TAIL_SLOT,
4106
5094
  id: WELCOME_BACK_CARD_ID,
4107
5095
  order: 30
4108
5096
  }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WelcomeBackCard, {