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