@klarkxy/dsh-session-manager 0.3.2 → 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: "欢迎回来",
@@ -104,6 +114,12 @@ const text = {
104
114
  edit: "Edit and resend",
105
115
  send: "Send",
106
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",
107
123
  draft: "Message text",
108
124
  continueDefault: "Continue",
109
125
  welcomeBack: "Welcome back",
@@ -151,7 +167,9 @@ function uiText(locale, key) {
151
167
  return text[locale][key];
152
168
  }
153
169
  function failureReason(failure) {
154
- 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";
155
173
  if (failure.code === "session-manager/busy") return "running";
156
174
  if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
157
175
  if (failure.code === "session/delete-pending") return "cleanup";
@@ -189,11 +207,13 @@ const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
189
207
  const WELCOME_BACK_PREPARE_ENDPOINT = "welcomeback.prepare";
190
208
  const WELCOME_BACK_COMPACT_ENDPOINT = "welcomeback.compact";
191
209
  const CONTINUE_SUGGEST_ENDPOINT = "continue.suggest";
210
+ const EDIT_PREPARE_ENDPOINT = "edit.prepare";
211
+ const EDIT_FILE_ENDPOINT = "edit.file";
192
212
  const CONFIG_READ_ENDPOINT = "config.read";
193
213
  const CONFIG_WRITE_ENDPOINT = "config.write";
194
214
  /** Card entry id in the chat flow; the recap renders at the latest completed turn's tail. */
195
215
  const WELCOME_BACK_CARD_ID = "@klarkxy/dsh-session-manager.welcome-back";
196
- const WELCOME_BACK_CARD_SLOT = "conversation.chat.turnTail";
216
+ const TURN_TAIL_SLOT = "conversation.chat.turnTail";
197
217
  const CONTINUE_GHOST_ID = "@klarkxy/dsh-session-manager.continue-ghost";
198
218
  const CONTINUE_GHOST_SLOT = "conversation.input.overlay";
199
219
  //#endregion
@@ -1660,10 +1680,117 @@ ${modelMenuCss}
1660
1680
  .${FREE_CHAT_ROOT} .dsh-free-actions {
1661
1681
  display: inline-flex;
1662
1682
  align-items: center;
1663
- gap: 4px;
1683
+ gap: 8px;
1664
1684
  min-width: 0;
1665
1685
  }
1666
1686
  .${FREE_CHAT_ROOT} .dsh-free-alert { margin: 0; }
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
+ }
1667
1794
  /* Settings page: one quiet reading surface. Rules, not nested wells, separate
1668
1795
  * sections; dependent controls align with the feature they belong to. */
1669
1796
  .${FREE_CHAT_ROOT} .dsm-settings {
@@ -1803,6 +1930,22 @@ ${modelMenuCss}
1803
1930
  color: var(--dsw-alias-label-secondary);
1804
1931
  }
1805
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; }
1806
1949
  `;
1807
1950
  //#endregion
1808
1951
  //#region src/free-panel.tsx
@@ -2189,11 +2332,15 @@ function bindTemporaryWorkspaceHost(ctx) {
2189
2332
  //#region src/session-actions.ts
2190
2333
  const UNSUPPORTED_CAPABILITIES = {
2191
2334
  retry: false,
2335
+ retryModified: false,
2192
2336
  freeSession: false,
2193
2337
  deleteSession: false
2194
2338
  };
2195
2339
  function isRecoverable(failure) {
2196
- 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";
2197
2344
  }
2198
2345
  function readFailure(error) {
2199
2346
  if (isFailure(error)) return {
@@ -2254,6 +2401,8 @@ async function readCapabilities(host) {
2254
2401
  const record = value;
2255
2402
  return {
2256
2403
  retry: record.retry === true,
2404
+ retryModified: record.retryModified === true,
2405
+ ...record.retryContent === true ? { retryContent: true } : {},
2257
2406
  freeSession: record.freeSession === true,
2258
2407
  deleteSession: record.deleteSession === true
2259
2408
  };
@@ -2286,19 +2435,16 @@ async function deleteOwnedSession(host, sessionId, requestId) {
2286
2435
  if (phase !== "deleted" && phase !== "cleanup-pending") throw new HostOperationError("session/delete-pending", "missing-outcome");
2287
2436
  return { phase };
2288
2437
  }
2289
- 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) {
2290
2440
  return {
2291
2441
  sessionId: input.sessionId,
2292
2442
  messageId: input.messageId,
2293
2443
  expectedRevision: input.expectedRevision,
2294
2444
  expectedInputRevision: input.expectedInputRevision,
2295
- requestId: input.requestId
2296
- };
2297
- }
2298
- function editRetryRequest(input) {
2299
- return {
2300
- ...directRetryRequest(input),
2301
- text: input.text
2445
+ requestId: input.requestId,
2446
+ ...input.content === void 0 ? { text: input.text } : { content: input.content },
2447
+ ...input.allowModified === true ? { allowModified: true } : {}
2302
2448
  };
2303
2449
  }
2304
2450
  var SubmissionLedger = class {
@@ -2483,6 +2629,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2483
2629
  let ready = false;
2484
2630
  let busy = false;
2485
2631
  let editing = false;
2632
+ let modified = false;
2633
+ let editNeedsPreview = false;
2486
2634
  let draft = "";
2487
2635
  let failure;
2488
2636
  let expectedRevision = 0;
@@ -2494,8 +2642,10 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2494
2642
  function build() {
2495
2643
  return {
2496
2644
  controlsRetry: controlsFor(capabilities, ready, host).retry,
2645
+ controlsContent: ready && capabilities.retryContent === true,
2497
2646
  busy,
2498
2647
  editing,
2648
+ modified,
2499
2649
  draft,
2500
2650
  sessionId: ownerSession,
2501
2651
  messageId: ownerMessage,
@@ -2511,7 +2661,7 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2511
2661
  const record = value;
2512
2662
  const inputRevision = typeof record.inputRevision === "string" ? record.inputRevision : "";
2513
2663
  const eligible = record.eligible === true;
2514
- 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");
2515
2665
  return {
2516
2666
  branchRevision: record.branchRevision,
2517
2667
  inputRevision,
@@ -2549,7 +2699,11 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2549
2699
  await ledger.replay(async (body) => {
2550
2700
  await callRetry(body, signal, ticket);
2551
2701
  });
2552
- 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
+ }
2553
2707
  } catch (error) {
2554
2708
  if (ticket === call && !signal.aborted) failure = readFailure(error);
2555
2709
  } finally {
@@ -2568,8 +2722,9 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2568
2722
  ready = true;
2569
2723
  publish();
2570
2724
  },
2571
- async retry(sessionId, id, signal) {
2725
+ async beginEdit(sessionId, id, text, signal) {
2572
2726
  if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
2727
+ if (editing && (ownerSession !== sessionId || ownerMessage !== id)) return;
2573
2728
  if (ledger.pending() !== void 0) {
2574
2729
  if (!samePending(sessionId, id)) return;
2575
2730
  await replaySame(signal);
@@ -2578,63 +2733,25 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2578
2733
  const ticket = ++call;
2579
2734
  busy = true;
2580
2735
  failure = void 0;
2581
- editing = false;
2736
+ modified = false;
2737
+ editNeedsPreview = false;
2582
2738
  ownerSession = sessionId;
2583
2739
  ownerMessage = id;
2584
2740
  publish();
2585
2741
  try {
2586
2742
  const seen = await preview(sessionId, id, signal, ticket);
2587
2743
  if (seen === void 0) return;
2588
- if (!seen.eligible) {
2589
- failure = {
2590
- code: "session/retry-blocked",
2591
- reason: seen.reason ?? "unknown"
2592
- };
2593
- return;
2594
- }
2595
2744
  if (signal.aborted || ticket !== call) return;
2596
2745
  expectedRevision = seen.branchRevision;
2597
2746
  expectedInputRevision = seen.inputRevision;
2598
- const body = directRetryRequest({
2599
- sessionId,
2600
- messageId: id,
2601
- expectedRevision: seen.branchRevision,
2602
- expectedInputRevision: seen.inputRevision,
2603
- requestId: ids()
2604
- });
2605
- await ledger.submit(body, () => callRetry(body, signal, ticket));
2606
- } catch (error) {
2607
- if (ticket === call && !signal.aborted) failure = readFailure(error);
2608
- } finally {
2609
- settle();
2610
- }
2611
- },
2612
- async beginEdit(sessionId, id, text, signal) {
2613
- if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
2614
- if (ledger.pending() !== void 0) {
2615
- if (!samePending(sessionId, id)) return;
2616
- await replaySame(signal);
2617
- return;
2618
- }
2619
- const ticket = ++call;
2620
- busy = true;
2621
- failure = void 0;
2622
- ownerSession = sessionId;
2623
- ownerMessage = id;
2624
- publish();
2625
- try {
2626
- const seen = await preview(sessionId, id, signal, ticket);
2627
- if (seen === void 0) return;
2628
- if (!seen.eligible) {
2747
+ if (!seen.eligible && (seen.reason !== "modified" || !capabilities.retryModified)) {
2629
2748
  failure = {
2630
2749
  code: "session/retry-blocked",
2631
2750
  reason: seen.reason ?? "unknown"
2632
2751
  };
2633
2752
  return;
2634
2753
  }
2635
- if (signal.aborted || ticket !== call) return;
2636
- expectedRevision = seen.branchRevision;
2637
- expectedInputRevision = seen.inputRevision;
2754
+ modified = !seen.eligible;
2638
2755
  draft = text;
2639
2756
  editing = true;
2640
2757
  } catch (error) {
@@ -2651,9 +2768,11 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2651
2768
  cancelEdit() {
2652
2769
  if (busy) return;
2653
2770
  editing = false;
2771
+ modified = false;
2772
+ editNeedsPreview = false;
2654
2773
  publish();
2655
2774
  },
2656
- async sendEdit(sessionId, id, signal) {
2775
+ async sendEdit(sessionId, id, signal, content, confirmed) {
2657
2776
  if (signal.aborted || busy || !controlsFor(capabilities, ready, host).retry) return;
2658
2777
  if (!editing || ownerSession !== sessionId || ownerMessage !== id) return;
2659
2778
  if (ledger.pending() !== void 0) {
@@ -2663,23 +2782,49 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2663
2782
  }
2664
2783
  const ticket = ++call;
2665
2784
  busy = true;
2785
+ const refresh = editNeedsPreview || isStaleRetryFailure(failure);
2666
2786
  failure = void 0;
2667
2787
  const text = draft;
2668
2788
  publish();
2669
2789
  try {
2670
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");
2671
2808
  const body = editRetryRequest({
2672
2809
  sessionId,
2673
2810
  messageId: id,
2674
2811
  expectedRevision,
2675
2812
  expectedInputRevision,
2676
2813
  requestId: ids(),
2677
- text
2814
+ text,
2815
+ ...content === void 0 ? {} : { content },
2816
+ allowModified: confirmed === true
2678
2817
  });
2679
2818
  await ledger.submit(body, () => callRetry(body, signal, ticket));
2680
- if (ticket === call && !signal.aborted) editing = false;
2819
+ if (ticket === call && !signal.aborted) {
2820
+ editing = false;
2821
+ modified = false;
2822
+ editNeedsPreview = false;
2823
+ }
2681
2824
  } catch (error) {
2682
- 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;
2683
2828
  } finally {
2684
2829
  settle();
2685
2830
  }
@@ -2688,6 +2833,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2688
2833
  if (ownerSession !== sessionId || ownerMessage !== messageId) return;
2689
2834
  call += 1;
2690
2835
  editing = false;
2836
+ modified = false;
2837
+ editNeedsPreview = false;
2691
2838
  failure = void 0;
2692
2839
  draft = "";
2693
2840
  if (!ledger.busy) busy = false;
@@ -2696,6 +2843,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2696
2843
  cancel() {
2697
2844
  call += 1;
2698
2845
  editing = false;
2846
+ modified = false;
2847
+ editNeedsPreview = false;
2699
2848
  failure = void 0;
2700
2849
  if (!ledger.busy) busy = false;
2701
2850
  publish();
@@ -4031,8 +4180,185 @@ function SessionManagerSettings(props) {
4031
4180
  });
4032
4181
  }
4033
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
+ })
4356
+ });
4357
+ }
4358
+ //#endregion
4034
4359
  //#region src/user-actions.tsx
4035
4360
  function UserMessageActions(props) {
4361
+ const [confirming, setConfirming] = (0, react.useState)(false);
4036
4362
  const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
4037
4363
  const signal = (0, react.useRef)(new AbortController());
4038
4364
  (0, react.useEffect)(() => {
@@ -4056,75 +4382,624 @@ function UserMessageActions(props) {
4056
4382
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4057
4383
  className: `${FREE_CHAT_ROOT} dsh-free-actions`,
4058
4384
  children: [
4059
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4060
- label: copy("retry"),
4061
- side: "bottom",
4062
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4063
- size: "sm",
4064
- variant: "ghost",
4065
- "aria-label": copy("retry"),
4066
- disabled: snapshot.busy,
4067
- onClick: () => {
4068
- props.model.retry(props.sessionId, props.messageId, signal.current.signal);
4069
- },
4070
- children: copy("retry")
4071
- })
4072
- }),
4073
- /* @__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, {
4074
4386
  label: copy("edit"),
4075
4387
  side: "bottom",
4076
4388
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4077
4389
  size: "sm",
4390
+ className: "dsm-icon-action",
4078
4391
  variant: "ghost",
4079
4392
  "aria-label": copy("edit"),
4080
- disabled: snapshot.busy,
4393
+ disabled: snapshot.busy || snapshot.editing && !mine,
4081
4394
  onClick: () => {
4082
4395
  props.model.beginEdit(props.sessionId, props.messageId, props.text, signal.current.signal);
4083
4396
  },
4084
- children: copy("edit")
4397
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditMessageIcon, {})
4085
4398
  })
4086
- }),
4087
- failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4399
+ }) : null,
4400
+ failure && !editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4088
4401
  className: "dsh-ui-error dsh-free-alert",
4089
4402
  role: "alert",
4090
4403
  children: failureText(props.locale, failure)
4091
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,
4092
4421
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4093
- open: editing,
4422
+ open: confirming && editing,
4094
4423
  className: FREE_CHAT_ROOT,
4095
4424
  title: copy("editTitle"),
4096
4425
  closeLabel: copy("close"),
4097
4426
  onClose: () => {
4098
- props.model.cancelEdit();
4427
+ setConfirming(false);
4099
4428
  },
4100
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, {
4101
4430
  variant: "ghost",
4102
- disabled: snapshot.busy,
4431
+ "data-modal-autofocus": true,
4103
4432
  onClick: () => {
4104
- props.model.cancelEdit();
4433
+ setConfirming(false);
4105
4434
  },
4106
4435
  children: copy("cancel")
4107
4436
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4108
4437
  variant: "primary",
4109
- disabled: snapshot.busy,
4110
4438
  onClick: () => {
4111
- 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);
4112
4441
  },
4113
- children: copy("send")
4442
+ children: copy("editConfirm")
4114
4443
  })] }),
4115
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4116
- className: "dsh-ui-textarea dsh-free-draft",
4117
- "data-modal-autofocus": true,
4118
- "aria-label": copy("draft"),
4119
- value: draft,
4120
- onChange: (event) => {
4121
- props.model.setDraft(event.target.value);
4122
- }
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, {})
4123
4845
  })
4124
4846
  })
4125
4847
  ]
4126
4848
  });
4127
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
+ }
4128
5003
  //#endregion
4129
5004
  //#region src/client.tsx
4130
5005
  const name = "dsh-session-manager-client";
@@ -4144,7 +5019,14 @@ function apply(ctx) {
4144
5019
  mountModelSettingsChanges(client, PLUGIN_NAME);
4145
5020
  const host = bindHost(client);
4146
5021
  const chats = createFreeChatModel(host);
4147
- 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);
4148
5030
  const temporaryHost = bindTemporaryWorkspaceHost(client);
4149
5031
  const temporary = createTemporaryWorkspaceModel(temporaryHost, void 0, () => uiText(copyLocale(client.locale?.getSnapshot().active), "tempWorkspace"));
4150
5032
  const presenceHost = bindPresenceHost(client);
@@ -4193,8 +5075,22 @@ function apply(ctx) {
4193
5075
  locale: useLiveLocale(client.locale),
4194
5076
  model: continues
4195
5077
  }))));
4196
- stops.push(client.slots.inject(WELCOME_BACK_CARD_SLOT, () => client.slots.register({
4197
- 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,
4198
5094
  id: WELCOME_BACK_CARD_ID,
4199
5095
  order: 30
4200
5096
  }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WelcomeBackCard, {