@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/README.md +4 -1
- package/README.zh-CN.md +4 -1
- package/lib/client.inner.cjs +996 -100
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +996 -100
- package/lib/index.js +757 -8
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
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/
|
|
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
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
2819
|
+
if (ticket === call && !signal.aborted) {
|
|
2820
|
+
editing = false;
|
|
2821
|
+
modified = false;
|
|
2822
|
+
editNeedsPreview = false;
|
|
2823
|
+
}
|
|
2681
2824
|
} catch (error) {
|
|
2682
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
4431
|
+
"data-modal-autofocus": true,
|
|
4103
4432
|
onClick: () => {
|
|
4104
|
-
|
|
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
|
-
|
|
4439
|
+
setConfirming(false);
|
|
4440
|
+
props.model.sendEdit(props.sessionId, props.messageId, signal.current.signal, void 0, true);
|
|
4112
4441
|
},
|
|
4113
|
-
children: copy("
|
|
4442
|
+
children: copy("editConfirm")
|
|
4114
4443
|
})] }),
|
|
4115
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
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
|
|
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(
|
|
4197
|
-
name:
|
|
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, {
|