@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.inner.cjs
CHANGED
|
@@ -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/
|
|
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
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
2813
|
+
if (ticket === call && !signal.aborted) {
|
|
2814
|
+
editing = false;
|
|
2815
|
+
modified = false;
|
|
2816
|
+
editNeedsPreview = false;
|
|
2817
|
+
}
|
|
2675
2818
|
} catch (error) {
|
|
2676
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
4425
|
+
"data-modal-autofocus": true,
|
|
4097
4426
|
onClick: () => {
|
|
4098
|
-
|
|
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
|
-
|
|
4433
|
+
setConfirming(false);
|
|
4434
|
+
props.model.sendEdit(props.sessionId, props.messageId, signal.current.signal, void 0, true);
|
|
4106
4435
|
},
|
|
4107
|
-
children: copy("
|
|
4436
|
+
children: copy("editConfirm")
|
|
4108
4437
|
})] }),
|
|
4109
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
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
|
|
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(
|
|
4191
|
-
name:
|
|
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, {
|