@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.
@@ -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: "欢迎回来",
@@ -98,6 +108,12 @@ const text = {
98
108
  edit: "Edit and resend",
99
109
  send: "Send",
100
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",
101
117
  draft: "Message text",
102
118
  continueDefault: "Continue",
103
119
  welcomeBack: "Welcome back",
@@ -145,7 +161,9 @@ function uiText(locale, key) {
145
161
  return text[locale][key];
146
162
  }
147
163
  function failureReason(failure) {
148
- 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";
149
167
  if (failure.code === "session-manager/busy") return "running";
150
168
  if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
151
169
  if (failure.code === "session/delete-pending") return "cleanup";
@@ -183,11 +201,13 @@ const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
183
201
  const WELCOME_BACK_PREPARE_ENDPOINT = "welcomeback.prepare";
184
202
  const WELCOME_BACK_COMPACT_ENDPOINT = "welcomeback.compact";
185
203
  const CONTINUE_SUGGEST_ENDPOINT = "continue.suggest";
204
+ const EDIT_PREPARE_ENDPOINT = "edit.prepare";
205
+ const EDIT_FILE_ENDPOINT = "edit.file";
186
206
  const CONFIG_READ_ENDPOINT = "config.read";
187
207
  const CONFIG_WRITE_ENDPOINT = "config.write";
188
208
  /** Card entry id in the chat flow; the recap renders at the latest completed turn's tail. */
189
209
  const WELCOME_BACK_CARD_ID = "@klarkxy/dsh-session-manager.welcome-back";
190
- const WELCOME_BACK_CARD_SLOT = "conversation.chat.turnTail";
210
+ const TURN_TAIL_SLOT = "conversation.chat.turnTail";
191
211
  const CONTINUE_GHOST_ID = "@klarkxy/dsh-session-manager.continue-ghost";
192
212
  const CONTINUE_GHOST_SLOT = "conversation.input.overlay";
193
213
  //#endregion
@@ -1654,10 +1674,117 @@ ${modelMenuCss}
1654
1674
  .${FREE_CHAT_ROOT} .dsh-free-actions {
1655
1675
  display: inline-flex;
1656
1676
  align-items: center;
1657
- gap: 4px;
1677
+ gap: 8px;
1658
1678
  min-width: 0;
1659
1679
  }
1660
1680
  .${FREE_CHAT_ROOT} .dsh-free-alert { margin: 0; }
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
+ }
1661
1788
  /* Settings page: one quiet reading surface. Rules, not nested wells, separate
1662
1789
  * sections; dependent controls align with the feature they belong to. */
1663
1790
  .${FREE_CHAT_ROOT} .dsm-settings {
@@ -1797,6 +1924,22 @@ ${modelMenuCss}
1797
1924
  color: var(--dsw-alias-label-secondary);
1798
1925
  }
1799
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; }
1800
1943
  `;
1801
1944
  //#endregion
1802
1945
  //#region src/free-panel.tsx
@@ -2183,11 +2326,15 @@ function bindTemporaryWorkspaceHost(ctx) {
2183
2326
  //#region src/session-actions.ts
2184
2327
  const UNSUPPORTED_CAPABILITIES = {
2185
2328
  retry: false,
2329
+ retryModified: false,
2186
2330
  freeSession: false,
2187
2331
  deleteSession: false
2188
2332
  };
2189
2333
  function isRecoverable(failure) {
2190
- 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";
2191
2338
  }
2192
2339
  function readFailure(error) {
2193
2340
  if (isFailure(error)) return {
@@ -2248,6 +2395,8 @@ async function readCapabilities(host) {
2248
2395
  const record = value;
2249
2396
  return {
2250
2397
  retry: record.retry === true,
2398
+ retryModified: record.retryModified === true,
2399
+ ...record.retryContent === true ? { retryContent: true } : {},
2251
2400
  freeSession: record.freeSession === true,
2252
2401
  deleteSession: record.deleteSession === true
2253
2402
  };
@@ -2280,19 +2429,16 @@ async function deleteOwnedSession(host, sessionId, requestId) {
2280
2429
  if (phase !== "deleted" && phase !== "cleanup-pending") throw new HostOperationError("session/delete-pending", "missing-outcome");
2281
2430
  return { phase };
2282
2431
  }
2283
- 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) {
2284
2434
  return {
2285
2435
  sessionId: input.sessionId,
2286
2436
  messageId: input.messageId,
2287
2437
  expectedRevision: input.expectedRevision,
2288
2438
  expectedInputRevision: input.expectedInputRevision,
2289
- requestId: input.requestId
2290
- };
2291
- }
2292
- function editRetryRequest(input) {
2293
- return {
2294
- ...directRetryRequest(input),
2295
- text: input.text
2439
+ requestId: input.requestId,
2440
+ ...input.content === void 0 ? { text: input.text } : { content: input.content },
2441
+ ...input.allowModified === true ? { allowModified: true } : {}
2296
2442
  };
2297
2443
  }
2298
2444
  var SubmissionLedger = class {
@@ -2477,6 +2623,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2477
2623
  let ready = false;
2478
2624
  let busy = false;
2479
2625
  let editing = false;
2626
+ let modified = false;
2627
+ let editNeedsPreview = false;
2480
2628
  let draft = "";
2481
2629
  let failure;
2482
2630
  let expectedRevision = 0;
@@ -2488,8 +2636,10 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2488
2636
  function build() {
2489
2637
  return {
2490
2638
  controlsRetry: controlsFor(capabilities, ready, host).retry,
2639
+ controlsContent: ready && capabilities.retryContent === true,
2491
2640
  busy,
2492
2641
  editing,
2642
+ modified,
2493
2643
  draft,
2494
2644
  sessionId: ownerSession,
2495
2645
  messageId: ownerMessage,
@@ -2505,7 +2655,7 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2505
2655
  const record = value;
2506
2656
  const inputRevision = typeof record.inputRevision === "string" ? record.inputRevision : "";
2507
2657
  const eligible = record.eligible === true;
2508
- 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");
2509
2659
  return {
2510
2660
  branchRevision: record.branchRevision,
2511
2661
  inputRevision,
@@ -2543,7 +2693,11 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2543
2693
  await ledger.replay(async (body) => {
2544
2694
  await callRetry(body, signal, ticket);
2545
2695
  });
2546
- 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
+ }
2547
2701
  } catch (error) {
2548
2702
  if (ticket === call && !signal.aborted) failure = readFailure(error);
2549
2703
  } finally {
@@ -2562,8 +2716,9 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2562
2716
  ready = true;
2563
2717
  publish();
2564
2718
  },
2565
- async retry(sessionId, id, signal) {
2719
+ async beginEdit(sessionId, id, text, signal) {
2566
2720
  if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
2721
+ if (editing && (ownerSession !== sessionId || ownerMessage !== id)) return;
2567
2722
  if (ledger.pending() !== void 0) {
2568
2723
  if (!samePending(sessionId, id)) return;
2569
2724
  await replaySame(signal);
@@ -2572,63 +2727,25 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2572
2727
  const ticket = ++call;
2573
2728
  busy = true;
2574
2729
  failure = void 0;
2575
- editing = false;
2730
+ modified = false;
2731
+ editNeedsPreview = false;
2576
2732
  ownerSession = sessionId;
2577
2733
  ownerMessage = id;
2578
2734
  publish();
2579
2735
  try {
2580
2736
  const seen = await preview(sessionId, id, signal, ticket);
2581
2737
  if (seen === void 0) return;
2582
- if (!seen.eligible) {
2583
- failure = {
2584
- code: "session/retry-blocked",
2585
- reason: seen.reason ?? "unknown"
2586
- };
2587
- return;
2588
- }
2589
2738
  if (signal.aborted || ticket !== call) return;
2590
2739
  expectedRevision = seen.branchRevision;
2591
2740
  expectedInputRevision = seen.inputRevision;
2592
- const body = directRetryRequest({
2593
- sessionId,
2594
- messageId: id,
2595
- expectedRevision: seen.branchRevision,
2596
- expectedInputRevision: seen.inputRevision,
2597
- requestId: ids()
2598
- });
2599
- await ledger.submit(body, () => callRetry(body, signal, ticket));
2600
- } catch (error) {
2601
- if (ticket === call && !signal.aborted) failure = readFailure(error);
2602
- } finally {
2603
- settle();
2604
- }
2605
- },
2606
- async beginEdit(sessionId, id, text, signal) {
2607
- if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
2608
- if (ledger.pending() !== void 0) {
2609
- if (!samePending(sessionId, id)) return;
2610
- await replaySame(signal);
2611
- return;
2612
- }
2613
- const ticket = ++call;
2614
- busy = true;
2615
- failure = void 0;
2616
- ownerSession = sessionId;
2617
- ownerMessage = id;
2618
- publish();
2619
- try {
2620
- const seen = await preview(sessionId, id, signal, ticket);
2621
- if (seen === void 0) return;
2622
- if (!seen.eligible) {
2741
+ if (!seen.eligible && (seen.reason !== "modified" || !capabilities.retryModified)) {
2623
2742
  failure = {
2624
2743
  code: "session/retry-blocked",
2625
2744
  reason: seen.reason ?? "unknown"
2626
2745
  };
2627
2746
  return;
2628
2747
  }
2629
- if (signal.aborted || ticket !== call) return;
2630
- expectedRevision = seen.branchRevision;
2631
- expectedInputRevision = seen.inputRevision;
2748
+ modified = !seen.eligible;
2632
2749
  draft = text;
2633
2750
  editing = true;
2634
2751
  } catch (error) {
@@ -2645,9 +2762,11 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2645
2762
  cancelEdit() {
2646
2763
  if (busy) return;
2647
2764
  editing = false;
2765
+ modified = false;
2766
+ editNeedsPreview = false;
2648
2767
  publish();
2649
2768
  },
2650
- async sendEdit(sessionId, id, signal) {
2769
+ async sendEdit(sessionId, id, signal, content, confirmed) {
2651
2770
  if (signal.aborted || busy || !controlsFor(capabilities, ready, host).retry) return;
2652
2771
  if (!editing || ownerSession !== sessionId || ownerMessage !== id) return;
2653
2772
  if (ledger.pending() !== void 0) {
@@ -2657,23 +2776,49 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2657
2776
  }
2658
2777
  const ticket = ++call;
2659
2778
  busy = true;
2779
+ const refresh = editNeedsPreview || isStaleRetryFailure(failure);
2660
2780
  failure = void 0;
2661
2781
  const text = draft;
2662
2782
  publish();
2663
2783
  try {
2664
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");
2665
2802
  const body = editRetryRequest({
2666
2803
  sessionId,
2667
2804
  messageId: id,
2668
2805
  expectedRevision,
2669
2806
  expectedInputRevision,
2670
2807
  requestId: ids(),
2671
- text
2808
+ text,
2809
+ ...content === void 0 ? {} : { content },
2810
+ allowModified: confirmed === true
2672
2811
  });
2673
2812
  await ledger.submit(body, () => callRetry(body, signal, ticket));
2674
- if (ticket === call && !signal.aborted) editing = false;
2813
+ if (ticket === call && !signal.aborted) {
2814
+ editing = false;
2815
+ modified = false;
2816
+ editNeedsPreview = false;
2817
+ }
2675
2818
  } catch (error) {
2676
- 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;
2677
2822
  } finally {
2678
2823
  settle();
2679
2824
  }
@@ -2682,6 +2827,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2682
2827
  if (ownerSession !== sessionId || ownerMessage !== messageId) return;
2683
2828
  call += 1;
2684
2829
  editing = false;
2830
+ modified = false;
2831
+ editNeedsPreview = false;
2685
2832
  failure = void 0;
2686
2833
  draft = "";
2687
2834
  if (!ledger.busy) busy = false;
@@ -2690,6 +2837,8 @@ function createRetryModel(host, ids = () => crypto.randomUUID()) {
2690
2837
  cancel() {
2691
2838
  call += 1;
2692
2839
  editing = false;
2840
+ modified = false;
2841
+ editNeedsPreview = false;
2693
2842
  failure = void 0;
2694
2843
  if (!ledger.busy) busy = false;
2695
2844
  publish();
@@ -4025,8 +4174,185 @@ function SessionManagerSettings(props) {
4025
4174
  });
4026
4175
  }
4027
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
+ })
4350
+ });
4351
+ }
4352
+ //#endregion
4028
4353
  //#region src/user-actions.tsx
4029
4354
  function UserMessageActions(props) {
4355
+ const [confirming, setConfirming] = (0, react.useState)(false);
4030
4356
  const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
4031
4357
  const signal = (0, react.useRef)(new AbortController());
4032
4358
  (0, react.useEffect)(() => {
@@ -4050,75 +4376,624 @@ function UserMessageActions(props) {
4050
4376
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4051
4377
  className: `${FREE_CHAT_ROOT} dsh-free-actions`,
4052
4378
  children: [
4053
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
4054
- label: copy("retry"),
4055
- side: "bottom",
4056
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4057
- size: "sm",
4058
- variant: "ghost",
4059
- "aria-label": copy("retry"),
4060
- disabled: snapshot.busy,
4061
- onClick: () => {
4062
- props.model.retry(props.sessionId, props.messageId, signal.current.signal);
4063
- },
4064
- children: copy("retry")
4065
- })
4066
- }),
4067
- /* @__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, {
4068
4380
  label: copy("edit"),
4069
4381
  side: "bottom",
4070
4382
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4071
4383
  size: "sm",
4384
+ className: "dsm-icon-action",
4072
4385
  variant: "ghost",
4073
4386
  "aria-label": copy("edit"),
4074
- disabled: snapshot.busy,
4387
+ disabled: snapshot.busy || snapshot.editing && !mine,
4075
4388
  onClick: () => {
4076
4389
  props.model.beginEdit(props.sessionId, props.messageId, props.text, signal.current.signal);
4077
4390
  },
4078
- children: copy("edit")
4391
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditMessageIcon, {})
4079
4392
  })
4080
- }),
4081
- failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4393
+ }) : null,
4394
+ failure && !editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4082
4395
  className: "dsh-ui-error dsh-free-alert",
4083
4396
  role: "alert",
4084
4397
  children: failureText(props.locale, failure)
4085
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,
4086
4415
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
4087
- open: editing,
4416
+ open: confirming && editing,
4088
4417
  className: FREE_CHAT_ROOT,
4089
4418
  title: copy("editTitle"),
4090
4419
  closeLabel: copy("close"),
4091
4420
  onClose: () => {
4092
- props.model.cancelEdit();
4421
+ setConfirming(false);
4093
4422
  },
4094
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, {
4095
4424
  variant: "ghost",
4096
- disabled: snapshot.busy,
4425
+ "data-modal-autofocus": true,
4097
4426
  onClick: () => {
4098
- props.model.cancelEdit();
4427
+ setConfirming(false);
4099
4428
  },
4100
4429
  children: copy("cancel")
4101
4430
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4102
4431
  variant: "primary",
4103
- disabled: snapshot.busy,
4104
4432
  onClick: () => {
4105
- 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);
4106
4435
  },
4107
- children: copy("send")
4436
+ children: copy("editConfirm")
4108
4437
  })] }),
4109
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4110
- className: "dsh-ui-textarea dsh-free-draft",
4111
- "data-modal-autofocus": true,
4112
- "aria-label": copy("draft"),
4113
- value: draft,
4114
- onChange: (event) => {
4115
- props.model.setDraft(event.target.value);
4116
- }
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, {})
4117
4839
  })
4118
4840
  })
4119
4841
  ]
4120
4842
  });
4121
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
+ }
4122
4997
  //#endregion
4123
4998
  //#region src/client.tsx
4124
4999
  const name = "dsh-session-manager-client";
@@ -4138,7 +5013,14 @@ function apply(ctx) {
4138
5013
  mountModelSettingsChanges(client, PLUGIN_NAME);
4139
5014
  const host = bindHost(client);
4140
5015
  const chats = createFreeChatModel(host);
4141
- 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);
4142
5024
  const temporaryHost = bindTemporaryWorkspaceHost(client);
4143
5025
  const temporary = createTemporaryWorkspaceModel(temporaryHost, void 0, () => uiText(copyLocale(client.locale?.getSnapshot().active), "tempWorkspace"));
4144
5026
  const presenceHost = bindPresenceHost(client);
@@ -4187,8 +5069,22 @@ function apply(ctx) {
4187
5069
  locale: useLiveLocale(client.locale),
4188
5070
  model: continues
4189
5071
  }))));
4190
- stops.push(client.slots.inject(WELCOME_BACK_CARD_SLOT, () => client.slots.register({
4191
- 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,
4192
5088
  id: WELCOME_BACK_CARD_ID,
4193
5089
  order: 30
4194
5090
  }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WelcomeBackCard, {