@klarkxy/dsh-session-manager 0.0.0-stage → 0.1.0-rc.1
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/LICENSE +40 -0
- package/README.md +20 -2
- package/README.zh-CN.md +21 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +6 -0
- package/lib/client.inner.cjs +1717 -0
- package/lib/client.inner.cjs.map +1 -0
- package/lib/client.js +1726 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +1106 -0
- package/lib/index.js.map +1 -0
- package/locale/en.json +8 -0
- package/locale/zh.json +8 -0
- package/package.json +125 -4
|
@@ -0,0 +1,1717 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
//#region src/copy-action.ts
|
|
6
|
+
const COPY_THREAD_LABEL = {
|
|
7
|
+
zh: "复制线程 ID",
|
|
8
|
+
en: "Copy thread ID"
|
|
9
|
+
};
|
|
10
|
+
const COPY_THREAD_FAILED = {
|
|
11
|
+
zh: "无法复制线程 ID",
|
|
12
|
+
en: "Could not copy the thread ID"
|
|
13
|
+
};
|
|
14
|
+
function copyLocale(active) {
|
|
15
|
+
return active?.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
16
|
+
}
|
|
17
|
+
/** Copy one session id. Rejection stays with the caller so the menu can show it. */
|
|
18
|
+
async function copyThreadId(sessionId, writeText) {
|
|
19
|
+
await writeText(sessionId);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/contracts.ts
|
|
23
|
+
const COPY_THREAD_MENU_ID = "@klarkxy/dsh-session-manager.copy-thread-id";
|
|
24
|
+
/** Sidebar list id and the matching main panel key. */
|
|
25
|
+
const FREE_PANEL_ID = "free-chat";
|
|
26
|
+
const USER_ACTIONS_SLOT = "conversation.chat.user-actions";
|
|
27
|
+
const USER_ACTIONS_ID = "@klarkxy/dsh-session-manager.user-actions";
|
|
28
|
+
const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/free-list.ts
|
|
31
|
+
/**
|
|
32
|
+
* User-visible free chats. Host list membership is `ids` only.
|
|
33
|
+
* Owned subagents stay out; an independent fork with classification free stays in.
|
|
34
|
+
*/
|
|
35
|
+
function freeChatRows(snapshot) {
|
|
36
|
+
const rows = [];
|
|
37
|
+
for (const id of snapshot.ids) {
|
|
38
|
+
const row = snapshot.byId[id];
|
|
39
|
+
if (row === void 0 || row.classification !== "free" || row.origin === "subagent") continue;
|
|
40
|
+
rows.push(row);
|
|
41
|
+
}
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
/** Match the visible title or id. Directory text is not a class and is not searched. */
|
|
45
|
+
function matchFreeRows(rows, query) {
|
|
46
|
+
const needle = query.trim().toLowerCase();
|
|
47
|
+
if (needle === "") return [...rows];
|
|
48
|
+
return rows.filter((row) => row.displayTitle.toLowerCase().includes(needle) || row.id.toLowerCase().includes(needle));
|
|
49
|
+
}
|
|
50
|
+
/** Quiet clock: time when it is today, month and day otherwise. */
|
|
51
|
+
function formatQuietTime(updatedAt, locale, now = Date.now()) {
|
|
52
|
+
if (!Number.isFinite(updatedAt)) return "";
|
|
53
|
+
const date = new Date(updatedAt);
|
|
54
|
+
const today = new Date(now);
|
|
55
|
+
const sameDay = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
|
|
56
|
+
const tag = locale === "zh" ? "zh-CN" : "en";
|
|
57
|
+
if (sameDay) return new Intl.DateTimeFormat(tag, {
|
|
58
|
+
hour: "2-digit",
|
|
59
|
+
minute: "2-digit"
|
|
60
|
+
}).format(date);
|
|
61
|
+
return new Intl.DateTimeFormat(tag, {
|
|
62
|
+
month: "short",
|
|
63
|
+
day: "numeric"
|
|
64
|
+
}).format(date);
|
|
65
|
+
}
|
|
66
|
+
Object.values({
|
|
67
|
+
/** Surfaces, from the page up to the topmost nested card. */
|
|
68
|
+
surface: [
|
|
69
|
+
"--dsw-alias-bg-base",
|
|
70
|
+
"--dsw-alias-bg-layer-1",
|
|
71
|
+
"--dsw-alias-bg-layer-2",
|
|
72
|
+
"--dsw-alias-bg-layer-3",
|
|
73
|
+
"--dsw-alias-bg-layer-4",
|
|
74
|
+
"--dsw-alias-bg-module-platform",
|
|
75
|
+
"--dsw-alias-bg-overlay"
|
|
76
|
+
],
|
|
77
|
+
/** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
|
|
78
|
+
border: [
|
|
79
|
+
"--dsw-alias-border-l1",
|
|
80
|
+
"--dsw-alias-border-l2",
|
|
81
|
+
"--dsw-alias-border-l3",
|
|
82
|
+
"--dsw-alias-border-l4"
|
|
83
|
+
],
|
|
84
|
+
/** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
|
|
85
|
+
label: [
|
|
86
|
+
"--dsw-alias-label-primary",
|
|
87
|
+
"--dsw-alias-label-secondary",
|
|
88
|
+
"--dsw-alias-label-tertiary",
|
|
89
|
+
"--dsw-alias-label-caption",
|
|
90
|
+
"--dsw-alias-label-dimmed",
|
|
91
|
+
"--dsw-alias-label-error",
|
|
92
|
+
"--dsw-alias-label-primary-foreground"
|
|
93
|
+
],
|
|
94
|
+
/** Interactive fills. The `-danger` hover belongs to destructive rows. */
|
|
95
|
+
interactive: [
|
|
96
|
+
"--dsw-alias-interactive-bg-hover",
|
|
97
|
+
"--dsw-alias-interactive-bg-active",
|
|
98
|
+
"--dsw-alias-interactive-bg-hover-danger"
|
|
99
|
+
],
|
|
100
|
+
/** Accent and status. `business` is the focus ring's default colour. */
|
|
101
|
+
accent: [
|
|
102
|
+
"--dsw-alias-brand-primary",
|
|
103
|
+
"--dsw-alias-button-primary-fill",
|
|
104
|
+
"--dsw-alias-button-primary-hover",
|
|
105
|
+
"--dsw-alias-button-tool-bar-fill",
|
|
106
|
+
"--dsw-alias-button-tool-bar-hover",
|
|
107
|
+
"--dsw-alias-button-ghost-active-fill",
|
|
108
|
+
"--dsw-alias-button-ghost-active-border"
|
|
109
|
+
],
|
|
110
|
+
state: [
|
|
111
|
+
"--dsw-alias-state-business-primary",
|
|
112
|
+
"--dsw-alias-state-success-primary",
|
|
113
|
+
"--dsw-alias-state-warn-primary",
|
|
114
|
+
"--dsw-alias-state-error-primary",
|
|
115
|
+
"--dsw-alias-state-idle-primary"
|
|
116
|
+
],
|
|
117
|
+
/** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
|
|
118
|
+
radius: [
|
|
119
|
+
"--dsw-radius-sm",
|
|
120
|
+
"--dsw-radius-md",
|
|
121
|
+
"--dsw-radius-lg",
|
|
122
|
+
"--dsw-radius-panel"
|
|
123
|
+
],
|
|
124
|
+
/** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
|
|
125
|
+
elevation: [
|
|
126
|
+
"--dsw-elevation-soft",
|
|
127
|
+
"--dsw-elevation-stroke",
|
|
128
|
+
"--dsw-elevation-stroke-color",
|
|
129
|
+
"--dsw-elevation-panel",
|
|
130
|
+
"--dsw-elevation-prominent"
|
|
131
|
+
],
|
|
132
|
+
/** Floats, masks and scrollbars. */
|
|
133
|
+
overlay: [
|
|
134
|
+
"--dsw-menu-surface-fill",
|
|
135
|
+
"--dsw-menu-backdrop-filter",
|
|
136
|
+
"--dsw-mask-blur",
|
|
137
|
+
"--dsw-alias-bg-mask-1",
|
|
138
|
+
"--dsw-alias-menu-icon",
|
|
139
|
+
"--dsw-alias-scrollbar-bg-l2",
|
|
140
|
+
"--dsw-alias-scrollbar-hover-l2",
|
|
141
|
+
"--dsw-shadow-lv3"
|
|
142
|
+
],
|
|
143
|
+
/** Motion, focus and type axis, published by the host rather than by a component. */
|
|
144
|
+
system: [
|
|
145
|
+
"--dsw-focus-ring-width",
|
|
146
|
+
"--dsw-focus-ring-color",
|
|
147
|
+
"--dsw-font-markdown-code-font-family",
|
|
148
|
+
"--ds-transition-duration",
|
|
149
|
+
"--ds-ease-in-out",
|
|
150
|
+
"--ds-font-family-code",
|
|
151
|
+
"--dsh-scrollbar-thumb",
|
|
152
|
+
"--dsh-scrollbar-thumb-hover",
|
|
153
|
+
"--dsh-content-font-size-secondary",
|
|
154
|
+
"--dsh-content-font-delta",
|
|
155
|
+
"--dsh-frame-top-clearance"
|
|
156
|
+
]
|
|
157
|
+
}).flat();
|
|
158
|
+
/**
|
|
159
|
+
* The host's focus ring shorthand, verbatim. Writing this per control is how
|
|
160
|
+
* rings drift apart; a stylesheet that draws its own focus style should use
|
|
161
|
+
* {@link officialFocus} instead. The colour fallback is the host's own default.
|
|
162
|
+
*/
|
|
163
|
+
const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
|
|
164
|
+
/**
|
|
165
|
+
* A complete `outline` declaration at the official geometry. `offset` defaults
|
|
166
|
+
* to the 2px the native controls use; the host's inline field rules sit at 1px
|
|
167
|
+
* and a segmented tab pulls the ring inside itself at -2px.
|
|
168
|
+
*/
|
|
169
|
+
function officialFocus(offset = "2px") {
|
|
170
|
+
return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* An elevated surface. The host draws a raised surface's edge inside its
|
|
174
|
+
* shadow rather than as a border, so a card sets `border: 0` and takes the edge
|
|
175
|
+
* from the elevation — which is also why its border stays off the alias-border
|
|
176
|
+
* set that a flat, non-elevated rule would use.
|
|
177
|
+
*/
|
|
178
|
+
function officialElevation(level) {
|
|
179
|
+
return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The shared stylesheet for a plugin UI, scoped under that plugin's root class.
|
|
183
|
+
*
|
|
184
|
+
* Scoping is what keeps this safe to always-bundle into every client: the rules
|
|
185
|
+
* can only match inside the plugin's own subtree, so two plugins can both mount
|
|
186
|
+
* `dsh-ui-card` without either styling the other's markup.
|
|
187
|
+
*
|
|
188
|
+
* Each recipe matches the root element *and* its descendants, because the
|
|
189
|
+
* commonest arrangement is one element that is both — a surface that carries
|
|
190
|
+
* the root class and `dsh-ui-card`, or a portaled dialog that is the root and
|
|
191
|
+
* the `dsh-ui-surface`. A descendant-only scope would silently leave those
|
|
192
|
+
* unstyled, with no error anywhere.
|
|
193
|
+
*
|
|
194
|
+
* A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
|
|
195
|
+
* content leaves the plugin's subtree entirely. A portaled surface therefore
|
|
196
|
+
* needs its own root class on an element inside the portal.
|
|
197
|
+
*
|
|
198
|
+
* Only patterns that primitives do not provide live here. If a control appears
|
|
199
|
+
* below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
|
|
200
|
+
* bug in this file: use the primitive and delete the recipe.
|
|
201
|
+
*/
|
|
202
|
+
function officialUiCss(roots) {
|
|
203
|
+
const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
|
|
204
|
+
return `
|
|
205
|
+
/* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
|
|
206
|
+
* Do not hand-edit the copy inside a plugin: change this module so every plugin
|
|
207
|
+
* moves together. Scope: ${scope} */
|
|
208
|
+
/* The scope rule below stays open for the whole sheet: every rule after it
|
|
209
|
+
* nests with the parent selector, so one stylesheet can cover several unrelated
|
|
210
|
+
* roots without leaking into a neighbouring plugin. Its closing brace is last. */
|
|
211
|
+
${scope} {
|
|
212
|
+
color: var(--dsw-alias-label-primary);
|
|
213
|
+
font-family: inherit;
|
|
214
|
+
font-size: 14px;
|
|
215
|
+
line-height: 22px;
|
|
216
|
+
min-width: 0;
|
|
217
|
+
|
|
218
|
+
& *, & *::before, & *::after { box-sizing: border-box; }
|
|
219
|
+
& h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
|
|
220
|
+
/* Keyboard focus only: pointer input keeps the native resting appearance. The
|
|
221
|
+
* ring needs no radius of its own — the outline already follows the control's
|
|
222
|
+
* own border-radius, and overriding it here would flatten every control. */
|
|
223
|
+
& :focus-visible { ${officialFocus()}; }
|
|
224
|
+
& [hidden] { display: none !important; }
|
|
225
|
+
|
|
226
|
+
/* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
|
|
227
|
+
& .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
228
|
+
& .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
229
|
+
& .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
|
|
230
|
+
& .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
|
|
231
|
+
& .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
232
|
+
& .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
|
|
233
|
+
& .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
|
|
234
|
+
& .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
|
|
235
|
+
& .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
|
|
236
|
+
& .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
237
|
+
& .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
|
|
238
|
+
|
|
239
|
+
/* --- Layout scaffolding. --- */
|
|
240
|
+
& .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
|
|
241
|
+
& .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
|
|
242
|
+
& .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
243
|
+
& .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
244
|
+
& .dsh-ui-spacer { flex: 1; min-width: 0; }
|
|
245
|
+
/* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
|
|
246
|
+
& .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
|
|
247
|
+
& .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
|
|
248
|
+
|
|
249
|
+
/* --- Card: a raised surface whose edge lives in the shadow. --- */
|
|
250
|
+
& .dsh-ui-card {
|
|
251
|
+
${officialElevation("stroke")}
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 12px;
|
|
255
|
+
padding: 16px;
|
|
256
|
+
border-radius: var(--dsw-radius-lg);
|
|
257
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
258
|
+
color: var(--dsw-alias-label-primary);
|
|
259
|
+
min-width: 0;
|
|
260
|
+
}
|
|
261
|
+
& .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
|
|
262
|
+
& .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
|
|
263
|
+
& .dsh-ui-card--plain { background: none; }
|
|
264
|
+
|
|
265
|
+
/* --- Field: label, control, help, error. The control itself is an
|
|
266
|
+
* official primitive, so this only owns the vertical rhythm. --- */
|
|
267
|
+
& .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
268
|
+
& .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
|
|
269
|
+
& .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
270
|
+
& .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
271
|
+
& .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
|
|
272
|
+
& .dsh-ui-help p { margin: 0; }
|
|
273
|
+
& .dsh-ui-help p + p { margin-top: 8px; }
|
|
274
|
+
& .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
|
|
275
|
+
& .dsh-ui-control { width: 100%; min-width: 0; }
|
|
276
|
+
/* An invalid control is marked on the control, not only in the message. */
|
|
277
|
+
& .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
278
|
+
& .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
279
|
+
& .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
280
|
+
& .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
|
|
281
|
+
|
|
282
|
+
/* --- Buttons row. A no-op container: it exists so actions line up on the
|
|
283
|
+
* trailing edge the way every native panel does. --- */
|
|
284
|
+
& .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
|
|
285
|
+
& .dsh-ui-actions-end { justify-content: flex-end; }
|
|
286
|
+
|
|
287
|
+
/* --- Banners. A banner is a left-rail notice, the shape used for warnings,
|
|
288
|
+
* deprecations and blocking instructions. --- */
|
|
289
|
+
& .dsh-ui-banner {
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
gap: 8px;
|
|
293
|
+
padding: 10px 14px;
|
|
294
|
+
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
295
|
+
border-left: 3px solid var(--dsw-alias-state-warn-primary);
|
|
296
|
+
border-radius: var(--dsw-radius-md);
|
|
297
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
298
|
+
font-size: 13px;
|
|
299
|
+
line-height: 20px;
|
|
300
|
+
color: var(--dsw-alias-label-secondary);
|
|
301
|
+
}
|
|
302
|
+
& .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
|
|
303
|
+
& .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
|
|
304
|
+
& .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
|
|
305
|
+
& .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
|
|
306
|
+
& .dsh-ui-banner p { margin: 0; }
|
|
307
|
+
|
|
308
|
+
/* --- Inline notice: a transient confirmation with no chrome of its own. --- */
|
|
309
|
+
& .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
|
|
310
|
+
& .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
|
|
311
|
+
|
|
312
|
+
/* --- Empty and loading states. Both sit in a dashed well so an empty panel
|
|
313
|
+
* reads as "nothing here yet" rather than as a broken layout. --- */
|
|
314
|
+
& .dsh-ui-empty {
|
|
315
|
+
display: flex;
|
|
316
|
+
flex-direction: column;
|
|
317
|
+
align-items: flex-start;
|
|
318
|
+
gap: 6px;
|
|
319
|
+
padding: 20px 16px;
|
|
320
|
+
border: 0.5px dashed var(--dsw-alias-border-l3);
|
|
321
|
+
border-radius: var(--dsw-radius-md);
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
line-height: 20px;
|
|
324
|
+
color: var(--dsw-alias-label-secondary);
|
|
325
|
+
}
|
|
326
|
+
& .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
|
|
327
|
+
|
|
328
|
+
/* --- Selectable list. A row is a button-sized target with a hover fill and
|
|
329
|
+
* a selected fill; the name carries the weight, the description does not. --- */
|
|
330
|
+
& .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
331
|
+
& .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
|
|
332
|
+
& .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
|
|
333
|
+
& .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
334
|
+
& .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
|
|
335
|
+
& .dsh-ui-list-item {
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-direction: column;
|
|
338
|
+
align-items: flex-start;
|
|
339
|
+
gap: 4px;
|
|
340
|
+
width: 100%;
|
|
341
|
+
min-width: 0;
|
|
342
|
+
padding: 10px 12px;
|
|
343
|
+
border: 0;
|
|
344
|
+
border-radius: var(--dsw-radius-md);
|
|
345
|
+
background: none;
|
|
346
|
+
font: inherit;
|
|
347
|
+
color: inherit;
|
|
348
|
+
text-align: start;
|
|
349
|
+
cursor: pointer;
|
|
350
|
+
}
|
|
351
|
+
& .dsh-ui-list-item:disabled { cursor: default; }
|
|
352
|
+
& .dsh-ui-list-item:disabled:hover { background: none; }
|
|
353
|
+
& .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
|
|
354
|
+
& .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
355
|
+
|
|
356
|
+
/* --- Code and structured output. Wraps rather than scrolls, because a
|
|
357
|
+
* feature panel is read rather than scanned. --- */
|
|
358
|
+
& .dsh-ui-code {
|
|
359
|
+
padding: 10px 12px;
|
|
360
|
+
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
361
|
+
border-radius: var(--dsw-radius-md);
|
|
362
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
363
|
+
font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
|
|
364
|
+
font-size: 12px;
|
|
365
|
+
line-height: 20px;
|
|
366
|
+
color: var(--dsw-alias-label-secondary);
|
|
367
|
+
overflow-wrap: anywhere;
|
|
368
|
+
white-space: pre-wrap;
|
|
369
|
+
}
|
|
370
|
+
& .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
|
|
371
|
+
|
|
372
|
+
/* --- A toggle row: label on the left, switch on the trailing edge. This is
|
|
373
|
+
* layout only; the switch itself is the official primitive. --- */
|
|
374
|
+
& .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
|
|
375
|
+
& .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
|
|
376
|
+
& .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
377
|
+
& .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
378
|
+
|
|
379
|
+
/* --- Floating surface: a panel anchored to a trigger. This is the material
|
|
380
|
+
* a menu card is made of — the translucent fill plus its backdrop blur, with
|
|
381
|
+
* the edge riding inside the prominent elevation. A plugin that hand-rolls this
|
|
382
|
+
* is how tokens like --dsw-specific-menu get invented; use this instead. --- */
|
|
383
|
+
& .dsh-ui-surface {
|
|
384
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
385
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
386
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
387
|
+
position: relative;
|
|
388
|
+
display: flex;
|
|
389
|
+
flex-direction: column;
|
|
390
|
+
border: 0;
|
|
391
|
+
border-radius: var(--dsw-radius-lg);
|
|
392
|
+
background: var(--dsw-menu-surface-fill);
|
|
393
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter);
|
|
394
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
395
|
+
color: var(--dsw-alias-label-primary);
|
|
396
|
+
font-size: 13px;
|
|
397
|
+
line-height: 20px;
|
|
398
|
+
min-width: 0;
|
|
399
|
+
}
|
|
400
|
+
& .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
|
|
401
|
+
& .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
402
|
+
& .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
403
|
+
|
|
404
|
+
/* --- Native select. The primitives ship no select, so a plugin that offers a
|
|
405
|
+
* closed list of choices keeps the platform control and matches the official
|
|
406
|
+
* settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
|
|
407
|
+
* SegmentedControl is the primitive to reach for when the choices are few
|
|
408
|
+
* enough to show at once. --- */
|
|
409
|
+
& .dsh-ui-select {
|
|
410
|
+
box-sizing: border-box;
|
|
411
|
+
width: 100%;
|
|
412
|
+
min-width: 0;
|
|
413
|
+
height: 34px;
|
|
414
|
+
padding: 0 12px;
|
|
415
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
416
|
+
border-radius: var(--dsw-radius-md);
|
|
417
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
418
|
+
font: inherit;
|
|
419
|
+
font-size: 13px;
|
|
420
|
+
line-height: 1.5;
|
|
421
|
+
color: var(--dsw-alias-label-primary);
|
|
422
|
+
cursor: pointer;
|
|
423
|
+
}
|
|
424
|
+
& .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
|
|
425
|
+
& .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
426
|
+
& .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
427
|
+
|
|
428
|
+
/* Long panels scroll inside themselves instead of growing the host page. */
|
|
429
|
+
& .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
430
|
+
& .dsh-ui-panel-wide { max-width: 100%; }
|
|
431
|
+
|
|
432
|
+
@media (prefers-reduced-motion: reduce) {
|
|
433
|
+
& *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/styles.ts
|
|
440
|
+
const FREE_CHAT_ROOT = "dsh-free-chat";
|
|
441
|
+
/** Host tokens and official scaffolding. The panel stays a flat list; the modal reuses the root class. */
|
|
442
|
+
const freeChatCss = `${officialUiCss(FREE_CHAT_ROOT)}
|
|
443
|
+
.${FREE_CHAT_ROOT}.dsh-free-panel {
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-direction: column;
|
|
446
|
+
gap: 12px;
|
|
447
|
+
min-width: 0;
|
|
448
|
+
height: 100%;
|
|
449
|
+
padding: 16px;
|
|
450
|
+
background: var(--dsw-alias-bg-base);
|
|
451
|
+
}
|
|
452
|
+
.${FREE_CHAT_ROOT} .dsh-free-head {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 12px;
|
|
456
|
+
min-width: 0;
|
|
457
|
+
}
|
|
458
|
+
.${FREE_CHAT_ROOT} .dsh-free-head .dsh-ui-title { flex: 1; min-width: 0; }
|
|
459
|
+
.${FREE_CHAT_ROOT} .dsh-free-search { width: 100%; }
|
|
460
|
+
.${FREE_CHAT_ROOT} .dsh-free-line {
|
|
461
|
+
display: flex;
|
|
462
|
+
align-items: center;
|
|
463
|
+
gap: 4px;
|
|
464
|
+
min-width: 0;
|
|
465
|
+
}
|
|
466
|
+
.${FREE_CHAT_ROOT} .dsh-free-open {
|
|
467
|
+
display: flex;
|
|
468
|
+
align-items: center;
|
|
469
|
+
gap: 8px;
|
|
470
|
+
flex: 1;
|
|
471
|
+
min-width: 0;
|
|
472
|
+
min-height: 36px;
|
|
473
|
+
padding: 4px 8px;
|
|
474
|
+
border: 0;
|
|
475
|
+
border-radius: var(--dsw-radius-sm);
|
|
476
|
+
background: none;
|
|
477
|
+
color: inherit;
|
|
478
|
+
font: inherit;
|
|
479
|
+
text-align: left;
|
|
480
|
+
}
|
|
481
|
+
.${FREE_CHAT_ROOT} .dsh-free-open:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
482
|
+
.${FREE_CHAT_ROOT} .dsh-free-title {
|
|
483
|
+
flex: 1;
|
|
484
|
+
min-width: 0;
|
|
485
|
+
overflow: hidden;
|
|
486
|
+
text-overflow: ellipsis;
|
|
487
|
+
white-space: nowrap;
|
|
488
|
+
}
|
|
489
|
+
.${FREE_CHAT_ROOT} .dsh-free-time {
|
|
490
|
+
flex: none;
|
|
491
|
+
color: var(--dsw-alias-label-tertiary);
|
|
492
|
+
font-size: 12px;
|
|
493
|
+
line-height: 18px;
|
|
494
|
+
}
|
|
495
|
+
.${FREE_CHAT_ROOT} .dsh-free-draft {
|
|
496
|
+
box-sizing: border-box;
|
|
497
|
+
width: 100%;
|
|
498
|
+
min-height: 8em;
|
|
499
|
+
padding: 8px 12px;
|
|
500
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
501
|
+
border-radius: var(--dsw-radius-md);
|
|
502
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
503
|
+
color: var(--dsw-alias-label-primary);
|
|
504
|
+
font: inherit;
|
|
505
|
+
font-size: 14px;
|
|
506
|
+
line-height: 22px;
|
|
507
|
+
resize: vertical;
|
|
508
|
+
}
|
|
509
|
+
.${FREE_CHAT_ROOT} .dsh-free-actions {
|
|
510
|
+
display: inline-flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
gap: 4px;
|
|
513
|
+
min-width: 0;
|
|
514
|
+
}
|
|
515
|
+
.${FREE_CHAT_ROOT} .dsh-free-alert { margin: 0; }
|
|
516
|
+
.${FREE_CHAT_ROOT} .dsh-free-sr {
|
|
517
|
+
position: absolute;
|
|
518
|
+
width: 1px;
|
|
519
|
+
height: 1px;
|
|
520
|
+
padding: 0;
|
|
521
|
+
margin: -1px;
|
|
522
|
+
overflow: hidden;
|
|
523
|
+
clip: rect(0, 0, 0, 0);
|
|
524
|
+
white-space: nowrap;
|
|
525
|
+
border: 0;
|
|
526
|
+
}
|
|
527
|
+
@media (max-width: 640px) {
|
|
528
|
+
.${FREE_CHAT_ROOT}.dsh-free-panel { padding: 12px; }
|
|
529
|
+
.${FREE_CHAT_ROOT} .dsh-free-head { flex-wrap: wrap; }
|
|
530
|
+
}
|
|
531
|
+
@media (prefers-reduced-motion: reduce) {
|
|
532
|
+
.${FREE_CHAT_ROOT} .dsh-free-open { transition: none; }
|
|
533
|
+
}
|
|
534
|
+
`;
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/ui-copy.ts
|
|
537
|
+
const reasons = {
|
|
538
|
+
zh: {
|
|
539
|
+
modified: "后续操作可能涉及修改,暂时不能重试。",
|
|
540
|
+
unknown: "无法确认这个操作的结果。",
|
|
541
|
+
pending: "这条消息还在等待处理。",
|
|
542
|
+
stale: "这个聊天已经更新,请再试一次。",
|
|
543
|
+
running: "这个聊天仍在运行。请等待当前工作结束后再试。",
|
|
544
|
+
cleanup: "清理没有完成。",
|
|
545
|
+
unsupported: "当前版本不能完成这个操作。"
|
|
546
|
+
},
|
|
547
|
+
en: {
|
|
548
|
+
modified: "Later operations may have made changes. Retry is unavailable.",
|
|
549
|
+
unknown: "This action's result could not be confirmed.",
|
|
550
|
+
pending: "This message is still pending.",
|
|
551
|
+
stale: "This chat has changed. Try again.",
|
|
552
|
+
running: "This chat is still running. Wait for the current work to finish, then try again.",
|
|
553
|
+
cleanup: "Cleanup did not finish.",
|
|
554
|
+
unsupported: "This runtime cannot complete that action."
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const text = {
|
|
558
|
+
zh: {
|
|
559
|
+
panel: "自由聊天",
|
|
560
|
+
newChat: "新建聊天",
|
|
561
|
+
empty: "还没有自由聊天。",
|
|
562
|
+
loading: "正在读取聊天",
|
|
563
|
+
noMatch: "没有匹配的聊天。",
|
|
564
|
+
listError: "聊天列表没有读出来。",
|
|
565
|
+
retryList: "重试",
|
|
566
|
+
search: "搜索聊天",
|
|
567
|
+
running: "运行中",
|
|
568
|
+
idle: "空闲",
|
|
569
|
+
menu: "聊天操作",
|
|
570
|
+
fork: "分叉",
|
|
571
|
+
deleteChat: "删除聊天",
|
|
572
|
+
deleting: "正在删除",
|
|
573
|
+
deleteFree: "将删除这个聊天及其拥有的文件。",
|
|
574
|
+
deleteOrdinary: "将删除这个聊天。项目文件会保留。",
|
|
575
|
+
cancel: "取消",
|
|
576
|
+
close: "关闭",
|
|
577
|
+
retry: "重试",
|
|
578
|
+
edit: "编辑并重发",
|
|
579
|
+
send: "发送",
|
|
580
|
+
editTitle: "编辑并重发",
|
|
581
|
+
draft: "消息文本"
|
|
582
|
+
},
|
|
583
|
+
en: {
|
|
584
|
+
panel: "Free chat",
|
|
585
|
+
newChat: "New chat",
|
|
586
|
+
empty: "No free chats yet.",
|
|
587
|
+
loading: "Reading chats",
|
|
588
|
+
noMatch: "No matching chats.",
|
|
589
|
+
listError: "The chat list could not be read.",
|
|
590
|
+
retryList: "Retry",
|
|
591
|
+
search: "Search chats",
|
|
592
|
+
running: "Running",
|
|
593
|
+
idle: "Idle",
|
|
594
|
+
menu: "Chat actions",
|
|
595
|
+
fork: "Fork",
|
|
596
|
+
deleteChat: "Delete chat",
|
|
597
|
+
deleting: "Deleting",
|
|
598
|
+
deleteFree: "This deletes this chat and its owned files.",
|
|
599
|
+
deleteOrdinary: "This deletes this chat. Project files remain.",
|
|
600
|
+
cancel: "Cancel",
|
|
601
|
+
close: "Close",
|
|
602
|
+
retry: "Retry",
|
|
603
|
+
edit: "Edit and resend",
|
|
604
|
+
send: "Send",
|
|
605
|
+
editTitle: "Edit and resend",
|
|
606
|
+
draft: "Message text"
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
function uiText(locale, key) {
|
|
610
|
+
return text[locale][key];
|
|
611
|
+
}
|
|
612
|
+
function failureReason(failure) {
|
|
613
|
+
if (failure.code === "session/lifecycle-unavailable") return "unsupported";
|
|
614
|
+
if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
|
|
615
|
+
if (failure.code === "session/delete-pending") return "cleanup";
|
|
616
|
+
if (failure.code === "session/agent-busy" && failure.reason === "free-session-work-active") return "running";
|
|
617
|
+
if (failure.reason === "modified" || failure.reason === "pending" || failure.reason === "running" || failure.reason === "stale") return failure.reason;
|
|
618
|
+
if (failure.reason === "stop-failed" || failure.reason === "not-quiet") return "running";
|
|
619
|
+
if (failure.reason === "not-human-prompt" || failure.reason === "missing-evidence") return "unknown";
|
|
620
|
+
return "unknown";
|
|
621
|
+
}
|
|
622
|
+
function failureText(locale, failure) {
|
|
623
|
+
return reasons[locale][failureReason(failure)];
|
|
624
|
+
}
|
|
625
|
+
function deleteBody(locale, classification) {
|
|
626
|
+
return classification === "free" ? text[locale].deleteFree : text[locale].deleteOrdinary;
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/free-panel.tsx
|
|
630
|
+
function FreeChatIcon({ size }) {
|
|
631
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
632
|
+
width: size,
|
|
633
|
+
height: size,
|
|
634
|
+
viewBox: "0 0 16 16",
|
|
635
|
+
"aria-hidden": "true",
|
|
636
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
637
|
+
d: "M2.5 3.5h11v7.5h-4.2L6 13.5V11H2.5z",
|
|
638
|
+
fill: "none",
|
|
639
|
+
stroke: "currentColor",
|
|
640
|
+
strokeWidth: "1.2"
|
|
641
|
+
})
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function FreeChatPanel(props) {
|
|
645
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
|
|
646
|
+
const titleRef = (0, react.useRef)(null);
|
|
647
|
+
const searchId = (0, react.useId)();
|
|
648
|
+
(0, react.useEffect)(() => {
|
|
649
|
+
if (snapshot.focusNonce === 0) return;
|
|
650
|
+
titleRef.current?.focus();
|
|
651
|
+
}, [snapshot.focusNonce]);
|
|
652
|
+
const copy = (key) => uiText(props.locale, key);
|
|
653
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
654
|
+
className: `${FREE_CHAT_ROOT} dsh-free-panel`,
|
|
655
|
+
children: [
|
|
656
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: freeChatCss }),
|
|
657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
658
|
+
className: "dsh-free-head",
|
|
659
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
660
|
+
ref: titleRef,
|
|
661
|
+
tabIndex: -1,
|
|
662
|
+
className: "dsh-ui-title",
|
|
663
|
+
children: copy("panel")
|
|
664
|
+
}), snapshot.controls.create ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
665
|
+
variant: "primary",
|
|
666
|
+
size: "sm",
|
|
667
|
+
disabled: snapshot.creating,
|
|
668
|
+
onClick: () => {
|
|
669
|
+
props.model.create();
|
|
670
|
+
},
|
|
671
|
+
children: copy("newChat")
|
|
672
|
+
}) : null]
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
675
|
+
id: searchId,
|
|
676
|
+
className: "dsh-free-search",
|
|
677
|
+
type: "search",
|
|
678
|
+
value: snapshot.query,
|
|
679
|
+
"aria-label": copy("search"),
|
|
680
|
+
onChange: (event) => {
|
|
681
|
+
props.model.setQuery(event.target.value);
|
|
682
|
+
}
|
|
683
|
+
}),
|
|
684
|
+
snapshot.notice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
685
|
+
className: "dsh-ui-error dsh-free-alert",
|
|
686
|
+
role: "alert",
|
|
687
|
+
children: failureText(props.locale, snapshot.notice)
|
|
688
|
+
}) : null,
|
|
689
|
+
snapshot.listStatus === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
690
|
+
className: "dsh-ui-loading",
|
|
691
|
+
children: copy("loading")
|
|
692
|
+
}) : null,
|
|
693
|
+
snapshot.listStatus === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
694
|
+
className: "dsh-ui-empty",
|
|
695
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
696
|
+
role: "alert",
|
|
697
|
+
children: snapshot.listFailure?.reason && snapshot.listFailure.reason !== "unknown" ? snapshot.listFailure.reason : copy("listError")
|
|
698
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
699
|
+
size: "sm",
|
|
700
|
+
onClick: () => {
|
|
701
|
+
props.model.refreshList();
|
|
702
|
+
},
|
|
703
|
+
children: copy("retryList")
|
|
704
|
+
})]
|
|
705
|
+
}) : null,
|
|
706
|
+
snapshot.listStatus === "empty" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
707
|
+
className: "dsh-ui-empty",
|
|
708
|
+
children: copy("empty")
|
|
709
|
+
}) : null,
|
|
710
|
+
snapshot.listStatus === "nomatch" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
711
|
+
className: "dsh-ui-empty",
|
|
712
|
+
children: copy("noMatch")
|
|
713
|
+
}) : null,
|
|
714
|
+
snapshot.rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
715
|
+
className: "dsh-ui-list",
|
|
716
|
+
"aria-label": copy("panel"),
|
|
717
|
+
children: snapshot.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatRow, {
|
|
718
|
+
model: props.model,
|
|
719
|
+
locale: props.locale,
|
|
720
|
+
row
|
|
721
|
+
}, row.id))
|
|
722
|
+
}) : null
|
|
723
|
+
]
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function FreeChatRow(props) {
|
|
727
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
728
|
+
const [copyFailed, setCopyFailed] = (0, react.useState)(false);
|
|
729
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
|
|
730
|
+
const copy = (key) => uiText(props.locale, key);
|
|
731
|
+
const items = [];
|
|
732
|
+
if (snapshot.controls.fork) items.push({
|
|
733
|
+
id: "fork",
|
|
734
|
+
label: copy("fork")
|
|
735
|
+
});
|
|
736
|
+
items.push({
|
|
737
|
+
id: "copy",
|
|
738
|
+
label: copyFailed ? COPY_THREAD_FAILED[props.locale] : COPY_THREAD_LABEL[props.locale]
|
|
739
|
+
});
|
|
740
|
+
if (snapshot.controls.delete) items.push({
|
|
741
|
+
id: "delete",
|
|
742
|
+
label: copy("deleteChat"),
|
|
743
|
+
danger: true
|
|
744
|
+
});
|
|
745
|
+
const onSelect = (id) => {
|
|
746
|
+
if (id === "copy") {
|
|
747
|
+
copyThreadId(props.row.id, writeClipboard$1).then(() => {
|
|
748
|
+
setCopyFailed(false);
|
|
749
|
+
setOpen(false);
|
|
750
|
+
}, () => {
|
|
751
|
+
setCopyFailed(true);
|
|
752
|
+
setOpen(true);
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
setOpen(false);
|
|
757
|
+
if (id === "fork") props.model.fork(props.row.id);
|
|
758
|
+
if (id === "delete") props.model.requestDelete(props.row.id);
|
|
759
|
+
};
|
|
760
|
+
const state = props.row.running ? copy("running") : copy("idle");
|
|
761
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
762
|
+
className: "dsh-ui-list-row dsh-free-line",
|
|
763
|
+
children: [
|
|
764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
765
|
+
type: "button",
|
|
766
|
+
className: "dsh-free-open",
|
|
767
|
+
onClick: () => {
|
|
768
|
+
props.model.open(props.row.id);
|
|
769
|
+
},
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
|
|
772
|
+
state: props.row.running ? "ongoing" : "idle",
|
|
773
|
+
size: 8
|
|
774
|
+
}),
|
|
775
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
776
|
+
className: "dsh-free-sr",
|
|
777
|
+
children: state
|
|
778
|
+
}),
|
|
779
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
780
|
+
className: "dsh-free-title",
|
|
781
|
+
children: props.row.displayTitle
|
|
782
|
+
}),
|
|
783
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
|
|
784
|
+
className: "dsh-free-time",
|
|
785
|
+
dateTime: new Date(props.row.updatedAt).toISOString(),
|
|
786
|
+
children: formatQuietTime(props.row.updatedAt, props.locale)
|
|
787
|
+
})
|
|
788
|
+
]
|
|
789
|
+
}),
|
|
790
|
+
copyFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
791
|
+
className: "dsh-ui-error dsh-free-alert",
|
|
792
|
+
role: "alert",
|
|
793
|
+
children: COPY_THREAD_FAILED[props.locale]
|
|
794
|
+
}) : null,
|
|
795
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
796
|
+
open,
|
|
797
|
+
onClose: () => {
|
|
798
|
+
setOpen(false);
|
|
799
|
+
},
|
|
800
|
+
portal: true,
|
|
801
|
+
listClassName: FREE_CHAT_ROOT,
|
|
802
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
803
|
+
size: "sm",
|
|
804
|
+
variant: "ghost",
|
|
805
|
+
"aria-label": copy("menu"),
|
|
806
|
+
"aria-haspopup": "menu",
|
|
807
|
+
"aria-expanded": open,
|
|
808
|
+
onClick: () => {
|
|
809
|
+
setOpen((value) => !value);
|
|
810
|
+
},
|
|
811
|
+
children: "···"
|
|
812
|
+
}),
|
|
813
|
+
items,
|
|
814
|
+
onSelect
|
|
815
|
+
})
|
|
816
|
+
]
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function DeleteChatDialog(props) {
|
|
820
|
+
const request = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot).deleteRequest;
|
|
821
|
+
const confirmRef = (0, react.useRef)(null);
|
|
822
|
+
(0, react.useEffect)(() => {
|
|
823
|
+
if (request?.failure) confirmRef.current?.focus();
|
|
824
|
+
}, [request?.failure]);
|
|
825
|
+
if (request === void 0) return null;
|
|
826
|
+
const copy = (key) => uiText(props.locale, key);
|
|
827
|
+
const label = request.busy ? copy("deleting") : request.failure ? copy("retryList") : copy("deleteChat");
|
|
828
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
829
|
+
open: true,
|
|
830
|
+
className: FREE_CHAT_ROOT,
|
|
831
|
+
title: copy("deleteChat"),
|
|
832
|
+
closeLabel: copy("close"),
|
|
833
|
+
description: deleteBody(props.locale, request.classification),
|
|
834
|
+
onClose: () => {
|
|
835
|
+
props.model.cancelDelete();
|
|
836
|
+
},
|
|
837
|
+
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, {
|
|
838
|
+
variant: "ghost",
|
|
839
|
+
disabled: request.busy,
|
|
840
|
+
onClick: () => {
|
|
841
|
+
props.model.cancelDelete();
|
|
842
|
+
},
|
|
843
|
+
children: copy("cancel")
|
|
844
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
845
|
+
ref: confirmRef,
|
|
846
|
+
variant: "primary",
|
|
847
|
+
"data-modal-autofocus": true,
|
|
848
|
+
disabled: request.busy,
|
|
849
|
+
onClick: () => {
|
|
850
|
+
props.model.confirmDelete();
|
|
851
|
+
},
|
|
852
|
+
children: label
|
|
853
|
+
})] }),
|
|
854
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: request.title }), request.failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
855
|
+
className: "dsh-ui-error dsh-free-alert",
|
|
856
|
+
role: "alert",
|
|
857
|
+
children: failureText(props.locale, request.failure)
|
|
858
|
+
}) : null]
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
async function writeClipboard$1(value) {
|
|
862
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
863
|
+
if (clipboard === void 0) throw new Error("clipboard unavailable");
|
|
864
|
+
await clipboard.writeText(value);
|
|
865
|
+
}
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/session-actions.ts
|
|
868
|
+
const UNSUPPORTED_CAPABILITIES = {
|
|
869
|
+
retry: false,
|
|
870
|
+
freeSession: false,
|
|
871
|
+
deleteSession: false
|
|
872
|
+
};
|
|
873
|
+
function isRecoverable(failure) {
|
|
874
|
+
return failure.code === "transport" || failure.code === "session/delete-pending";
|
|
875
|
+
}
|
|
876
|
+
function readFailure(error) {
|
|
877
|
+
if (isFailure(error)) return {
|
|
878
|
+
code: error.code,
|
|
879
|
+
...error.reason === void 0 ? {} : { reason: error.reason }
|
|
880
|
+
};
|
|
881
|
+
if (typeof error === "object" && error !== null) {
|
|
882
|
+
const record = error;
|
|
883
|
+
const rpc = record.rpcError;
|
|
884
|
+
if (typeof rpc?.code === "string") return {
|
|
885
|
+
code: rpc.code,
|
|
886
|
+
...reasonOf(rpc) === void 0 ? {} : { reason: reasonOf(rpc) }
|
|
887
|
+
};
|
|
888
|
+
if (typeof record.code === "string") {
|
|
889
|
+
const reason = typeof record.reason === "string" ? record.reason : void 0;
|
|
890
|
+
return {
|
|
891
|
+
code: record.code,
|
|
892
|
+
...reason === void 0 ? {} : { reason }
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
code: "transport",
|
|
898
|
+
reason: "unknown"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function isFailure(error) {
|
|
902
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && error.name === "HostOperationError";
|
|
903
|
+
}
|
|
904
|
+
var HostOperationError = class extends Error {
|
|
905
|
+
code;
|
|
906
|
+
name = "HostOperationError";
|
|
907
|
+
reason;
|
|
908
|
+
constructor(code, reason) {
|
|
909
|
+
super(code);
|
|
910
|
+
this.code = code;
|
|
911
|
+
if (reason !== void 0) this.reason = reason;
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
function reasonOf(error) {
|
|
915
|
+
return typeof error?.details?.reason === "string" ? error.details.reason : void 0;
|
|
916
|
+
}
|
|
917
|
+
function isRemoteResult(value) {
|
|
918
|
+
if (typeof value !== "object" || value === null || !("ok" in value)) return false;
|
|
919
|
+
return typeof value.ok === "boolean" && ("value" in value || "error" in value);
|
|
920
|
+
}
|
|
921
|
+
async function unwrap(value) {
|
|
922
|
+
const resolved = await value;
|
|
923
|
+
if (!isRemoteResult(resolved)) return resolved;
|
|
924
|
+
if (!resolved.ok) throw new HostOperationError(resolved.error?.code ?? "transport", reasonOf(resolved.error));
|
|
925
|
+
return resolved.value;
|
|
926
|
+
}
|
|
927
|
+
async function readCapabilities(host) {
|
|
928
|
+
if (typeof host.capabilities !== "function") return UNSUPPORTED_CAPABILITIES;
|
|
929
|
+
try {
|
|
930
|
+
const value = await unwrap(host.capabilities());
|
|
931
|
+
if (typeof value !== "object" || value === null) return UNSUPPORTED_CAPABILITIES;
|
|
932
|
+
const record = value;
|
|
933
|
+
return {
|
|
934
|
+
retry: record.retry === true,
|
|
935
|
+
freeSession: record.freeSession === true,
|
|
936
|
+
deleteSession: record.deleteSession === true
|
|
937
|
+
};
|
|
938
|
+
} catch {
|
|
939
|
+
return UNSUPPORTED_CAPABILITIES;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function controlsFor(capabilities, ready, host) {
|
|
943
|
+
return {
|
|
944
|
+
create: ready && capabilities.freeSession && typeof host.create === "function",
|
|
945
|
+
fork: ready && capabilities.freeSession && (typeof host.forkSession === "function" || typeof host.fork === "function"),
|
|
946
|
+
delete: ready && capabilities.deleteSession && typeof host.deleteSession === "function",
|
|
947
|
+
retry: ready && capabilities.retry && typeof host.previewRetry === "function" && typeof host.retry === "function"
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function sessionIdOf(value) {
|
|
951
|
+
if (typeof value === "string" && value !== "") return value;
|
|
952
|
+
if (typeof value === "object" && value !== null && typeof value.sessionId === "string") return value.sessionId;
|
|
953
|
+
throw new HostOperationError("session/lifecycle-unavailable");
|
|
954
|
+
}
|
|
955
|
+
/** Pass only `{ kind: 'free' }`. A cwd or workspace is rejected by free create, and must not be invented. */
|
|
956
|
+
async function createFreeSession(host) {
|
|
957
|
+
if (typeof host.create !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
958
|
+
return sessionIdOf(await unwrap(host.create({ kind: "free" })));
|
|
959
|
+
}
|
|
960
|
+
/** Returns the child id. The caller opens it only while its generation is still current. */
|
|
961
|
+
async function forkOwnedChild(host, sessionId) {
|
|
962
|
+
if (typeof host.forkSession === "function") return sessionIdOf(await unwrap(host.forkSession(sessionId)));
|
|
963
|
+
if (typeof host.fork === "function") return sessionIdOf(await unwrap(host.fork({
|
|
964
|
+
sessionId,
|
|
965
|
+
increaseTitle: true
|
|
966
|
+
})));
|
|
967
|
+
throw new HostOperationError("session/fork-unavailable");
|
|
968
|
+
}
|
|
969
|
+
async function deleteOwnedSession(host, sessionId, requestId) {
|
|
970
|
+
if (typeof host.deleteSession !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
971
|
+
const value = await unwrap(host.deleteSession(sessionId, requestId));
|
|
972
|
+
if (typeof value !== "object" || value === null) throw new HostOperationError("session/delete-pending", "missing-outcome");
|
|
973
|
+
const phase = value.phase;
|
|
974
|
+
if (phase !== "deleted" && phase !== "cleanup-pending") throw new HostOperationError("session/delete-pending", "missing-outcome");
|
|
975
|
+
return {
|
|
976
|
+
phase,
|
|
977
|
+
releasedSelection: value.releasedSelection === true
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function directRetryRequest(input) {
|
|
981
|
+
return {
|
|
982
|
+
sessionId: input.sessionId,
|
|
983
|
+
messageId: input.messageId,
|
|
984
|
+
expectedRevision: input.expectedRevision,
|
|
985
|
+
expectedInputRevision: input.expectedInputRevision,
|
|
986
|
+
requestId: input.requestId
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
function editRetryRequest(input) {
|
|
990
|
+
return {
|
|
991
|
+
...directRetryRequest(input),
|
|
992
|
+
text: input.text
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
var SubmissionLedger = class {
|
|
996
|
+
active = false;
|
|
997
|
+
unresolved;
|
|
998
|
+
get busy() {
|
|
999
|
+
return this.active;
|
|
1000
|
+
}
|
|
1001
|
+
pending() {
|
|
1002
|
+
return this.unresolved;
|
|
1003
|
+
}
|
|
1004
|
+
/** Resend the stored body. A non-recoverable result drops it; a lost reply keeps it. */
|
|
1005
|
+
async replay(execute) {
|
|
1006
|
+
const body = this.unresolved;
|
|
1007
|
+
if (body === void 0) return;
|
|
1008
|
+
if (this.active) throw new HostOperationError("busy", "running");
|
|
1009
|
+
this.active = true;
|
|
1010
|
+
try {
|
|
1011
|
+
await execute(body);
|
|
1012
|
+
this.unresolved = void 0;
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
const failure = readFailure(error);
|
|
1015
|
+
if (!isRecoverable(failure) && failure.reason !== "aborted") this.unresolved = void 0;
|
|
1016
|
+
throw error instanceof HostOperationError ? error : new HostOperationError(failure.code, failure.reason);
|
|
1017
|
+
} finally {
|
|
1018
|
+
this.active = false;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/** First submission of a body built from the current preview. */
|
|
1022
|
+
async submit(body, execute) {
|
|
1023
|
+
if (this.active || this.unresolved !== void 0) throw new HostOperationError("busy", "running");
|
|
1024
|
+
this.active = true;
|
|
1025
|
+
try {
|
|
1026
|
+
await execute();
|
|
1027
|
+
this.unresolved = void 0;
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
const failure = readFailure(error);
|
|
1030
|
+
this.unresolved = isRecoverable(failure) ? body : void 0;
|
|
1031
|
+
throw error instanceof HostOperationError ? error : new HostOperationError(failure.code, failure.reason);
|
|
1032
|
+
} finally {
|
|
1033
|
+
this.active = false;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
function store() {
|
|
1038
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1039
|
+
return {
|
|
1040
|
+
subscribe(listener) {
|
|
1041
|
+
listeners.add(listener);
|
|
1042
|
+
return () => {
|
|
1043
|
+
listeners.delete(listener);
|
|
1044
|
+
};
|
|
1045
|
+
},
|
|
1046
|
+
emit() {
|
|
1047
|
+
for (const listener of listeners) listener();
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
|
|
1052
|
+
const events = store();
|
|
1053
|
+
let capabilities = UNSUPPORTED_CAPABILITIES;
|
|
1054
|
+
let capabilitiesReady = false;
|
|
1055
|
+
let query = "";
|
|
1056
|
+
let creating = false;
|
|
1057
|
+
let notice;
|
|
1058
|
+
let deleteRequest;
|
|
1059
|
+
let focusNonce = 0;
|
|
1060
|
+
let listFailure;
|
|
1061
|
+
let generation = 0;
|
|
1062
|
+
let snapshot = build();
|
|
1063
|
+
const stopList = host.list.subscribe(() => publish());
|
|
1064
|
+
function listError(listed) {
|
|
1065
|
+
const code = typeof listed.error?.code === "string" && listed.error.code !== "" ? listed.error.code : "transport";
|
|
1066
|
+
const message = listed.error?.message;
|
|
1067
|
+
return {
|
|
1068
|
+
code,
|
|
1069
|
+
...typeof message === "string" && message !== "" ? { reason: message } : {}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function build() {
|
|
1073
|
+
let status = "ready";
|
|
1074
|
+
let rows = [];
|
|
1075
|
+
try {
|
|
1076
|
+
const listed = host.list.getSnapshot();
|
|
1077
|
+
if (listed.phase === "error") {
|
|
1078
|
+
status = "error";
|
|
1079
|
+
listFailure = listError(listed);
|
|
1080
|
+
rows = [];
|
|
1081
|
+
} else {
|
|
1082
|
+
listFailure = void 0;
|
|
1083
|
+
const matched = matchFreeRows(freeChatRows(listed), query);
|
|
1084
|
+
if (listed.phase === "pending" && matched.length === 0) status = "loading";
|
|
1085
|
+
else if (listed.phase === "ready" && matched.length === 0 && query.trim() === "") status = "empty";
|
|
1086
|
+
else if (matched.length === 0) status = "nomatch";
|
|
1087
|
+
else status = "ready";
|
|
1088
|
+
rows = matched;
|
|
1089
|
+
}
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
status = "error";
|
|
1092
|
+
listFailure = readFailure(error);
|
|
1093
|
+
rows = [];
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
rows,
|
|
1097
|
+
listStatus: status,
|
|
1098
|
+
...listFailure === void 0 ? {} : { listFailure },
|
|
1099
|
+
query,
|
|
1100
|
+
controls: controlsFor(capabilities, capabilitiesReady, host),
|
|
1101
|
+
capabilitiesReady,
|
|
1102
|
+
creating,
|
|
1103
|
+
...notice === void 0 ? {} : { notice },
|
|
1104
|
+
...deleteRequest === void 0 ? {} : { deleteRequest },
|
|
1105
|
+
focusNonce
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function publish() {
|
|
1109
|
+
snapshot = build();
|
|
1110
|
+
events.emit();
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
subscribe: events.subscribe,
|
|
1114
|
+
getSnapshot: () => snapshot,
|
|
1115
|
+
async reloadCapabilities() {
|
|
1116
|
+
capabilities = await readCapabilities(host);
|
|
1117
|
+
capabilitiesReady = true;
|
|
1118
|
+
publish();
|
|
1119
|
+
},
|
|
1120
|
+
setQuery(next) {
|
|
1121
|
+
query = next;
|
|
1122
|
+
publish();
|
|
1123
|
+
},
|
|
1124
|
+
async refreshList() {
|
|
1125
|
+
if (typeof host.refresh !== "function") return;
|
|
1126
|
+
await unwrap(host.refresh());
|
|
1127
|
+
publish();
|
|
1128
|
+
},
|
|
1129
|
+
async create() {
|
|
1130
|
+
if (!controlsFor(capabilities, capabilitiesReady, host).create || creating) return;
|
|
1131
|
+
const ticket = generation;
|
|
1132
|
+
creating = true;
|
|
1133
|
+
notice = void 0;
|
|
1134
|
+
publish();
|
|
1135
|
+
try {
|
|
1136
|
+
const id = await createFreeSession(host);
|
|
1137
|
+
if (ticket !== generation) return;
|
|
1138
|
+
if (typeof host.openSession !== "function") throw new HostOperationError("session/lifecycle-unavailable", "open-session");
|
|
1139
|
+
host.openSession(id);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (ticket !== generation) return;
|
|
1142
|
+
notice = readFailure(error);
|
|
1143
|
+
} finally {
|
|
1144
|
+
if (ticket === generation) {
|
|
1145
|
+
creating = false;
|
|
1146
|
+
publish();
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
open(sessionId) {
|
|
1151
|
+
host.openSession?.(sessionId);
|
|
1152
|
+
},
|
|
1153
|
+
async fork(sessionId) {
|
|
1154
|
+
const ticket = generation;
|
|
1155
|
+
notice = void 0;
|
|
1156
|
+
try {
|
|
1157
|
+
const child = await forkOwnedChild(host, sessionId);
|
|
1158
|
+
if (ticket !== generation) return;
|
|
1159
|
+
if (typeof host.openSession !== "function") throw new HostOperationError("session/lifecycle-unavailable", "open-session");
|
|
1160
|
+
host.openSession(child);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
if (ticket !== generation) return;
|
|
1163
|
+
notice = readFailure(error);
|
|
1164
|
+
}
|
|
1165
|
+
if (ticket === generation) publish();
|
|
1166
|
+
},
|
|
1167
|
+
requestDelete(sessionId) {
|
|
1168
|
+
if (!controlsFor(capabilities, capabilitiesReady, host).delete) return;
|
|
1169
|
+
const row = host.list.getSnapshot().byId[sessionId];
|
|
1170
|
+
deleteRequest = {
|
|
1171
|
+
sessionId,
|
|
1172
|
+
title: row?.displayTitle ?? row?.title ?? sessionId,
|
|
1173
|
+
classification: row?.classification === "free" ? "free" : "ordinary",
|
|
1174
|
+
requestId: ids(),
|
|
1175
|
+
busy: false
|
|
1176
|
+
};
|
|
1177
|
+
publish();
|
|
1178
|
+
},
|
|
1179
|
+
cancelDelete() {
|
|
1180
|
+
if (deleteRequest?.busy) return;
|
|
1181
|
+
deleteRequest = void 0;
|
|
1182
|
+
publish();
|
|
1183
|
+
},
|
|
1184
|
+
async confirmDelete() {
|
|
1185
|
+
const current = deleteRequest;
|
|
1186
|
+
if (current === void 0 || current.busy) return;
|
|
1187
|
+
const ticket = generation;
|
|
1188
|
+
deleteRequest = {
|
|
1189
|
+
...current,
|
|
1190
|
+
busy: true,
|
|
1191
|
+
failure: void 0
|
|
1192
|
+
};
|
|
1193
|
+
publish();
|
|
1194
|
+
try {
|
|
1195
|
+
const outcome = await deleteOwnedSession(host, current.sessionId, current.requestId);
|
|
1196
|
+
if (ticket !== generation) return;
|
|
1197
|
+
if (outcome.phase !== "deleted") {
|
|
1198
|
+
deleteRequest = {
|
|
1199
|
+
...current,
|
|
1200
|
+
busy: false,
|
|
1201
|
+
failure: {
|
|
1202
|
+
code: "session/delete-pending",
|
|
1203
|
+
reason: "pending"
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
publish();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
deleteRequest = void 0;
|
|
1210
|
+
if (outcome.releasedSelection) {
|
|
1211
|
+
host.selectPanel?.(FREE_PANEL_ID);
|
|
1212
|
+
focusNonce += 1;
|
|
1213
|
+
}
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
if (ticket !== generation) return;
|
|
1216
|
+
const failure = readFailure(error);
|
|
1217
|
+
deleteRequest = {
|
|
1218
|
+
...current,
|
|
1219
|
+
busy: false,
|
|
1220
|
+
failure
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
if (ticket === generation) publish();
|
|
1224
|
+
},
|
|
1225
|
+
cancel() {
|
|
1226
|
+
generation += 1;
|
|
1227
|
+
creating = false;
|
|
1228
|
+
notice = void 0;
|
|
1229
|
+
deleteRequest = void 0;
|
|
1230
|
+
stopList();
|
|
1231
|
+
publish();
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function createRetryModel(host, ids = () => crypto.randomUUID()) {
|
|
1236
|
+
const events = store();
|
|
1237
|
+
const ledger = new SubmissionLedger();
|
|
1238
|
+
let capabilities = UNSUPPORTED_CAPABILITIES;
|
|
1239
|
+
let ready = false;
|
|
1240
|
+
let busy = false;
|
|
1241
|
+
let editing = false;
|
|
1242
|
+
let draft = "";
|
|
1243
|
+
let failure;
|
|
1244
|
+
let expectedRevision = 0;
|
|
1245
|
+
let expectedInputRevision = "";
|
|
1246
|
+
let ownerSession = "";
|
|
1247
|
+
let ownerMessage = "";
|
|
1248
|
+
let call = 0;
|
|
1249
|
+
let snapshot = build();
|
|
1250
|
+
function build() {
|
|
1251
|
+
return {
|
|
1252
|
+
controlsRetry: controlsFor(capabilities, ready, host).retry,
|
|
1253
|
+
busy,
|
|
1254
|
+
editing,
|
|
1255
|
+
draft,
|
|
1256
|
+
sessionId: ownerSession,
|
|
1257
|
+
messageId: ownerMessage,
|
|
1258
|
+
...failure === void 0 ? {} : { failure }
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
function publish() {
|
|
1262
|
+
snapshot = build();
|
|
1263
|
+
events.emit();
|
|
1264
|
+
}
|
|
1265
|
+
function readPreview(value) {
|
|
1266
|
+
if (typeof value !== "object" || value === null || typeof value.branchRevision !== "number") throw new HostOperationError("session/lifecycle-unavailable");
|
|
1267
|
+
const record = value;
|
|
1268
|
+
const inputRevision = typeof record.inputRevision === "string" ? record.inputRevision : "";
|
|
1269
|
+
const eligible = record.eligible === true;
|
|
1270
|
+
if (eligible && inputRevision === "") throw new HostOperationError("session/lifecycle-unavailable", "input-revision");
|
|
1271
|
+
return {
|
|
1272
|
+
branchRevision: record.branchRevision,
|
|
1273
|
+
inputRevision,
|
|
1274
|
+
eligible,
|
|
1275
|
+
...typeof record.reason === "string" ? { reason: record.reason } : {}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
async function preview(sessionId, id, signal, ticket) {
|
|
1279
|
+
if (ticket !== call || signal.aborted) return void 0;
|
|
1280
|
+
if (typeof host.previewRetry !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
1281
|
+
const value = await unwrap(host.previewRetry({
|
|
1282
|
+
sessionId,
|
|
1283
|
+
messageId: id
|
|
1284
|
+
}));
|
|
1285
|
+
if (ticket !== call || signal.aborted) return void 0;
|
|
1286
|
+
return readPreview(value);
|
|
1287
|
+
}
|
|
1288
|
+
async function callRetry(body, signal, ticket) {
|
|
1289
|
+
if (signal.aborted || ticket !== call) throw new HostOperationError("busy", "aborted");
|
|
1290
|
+
if (typeof host.retry !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
1291
|
+
await unwrap(host.retry(body));
|
|
1292
|
+
}
|
|
1293
|
+
function settle() {
|
|
1294
|
+
if (!ledger.busy) busy = false;
|
|
1295
|
+
publish();
|
|
1296
|
+
}
|
|
1297
|
+
async function replaySame(signal) {
|
|
1298
|
+
if (signal.aborted) return;
|
|
1299
|
+
const pending = ledger.pending();
|
|
1300
|
+
const ticket = ++call;
|
|
1301
|
+
busy = true;
|
|
1302
|
+
failure = void 0;
|
|
1303
|
+
publish();
|
|
1304
|
+
try {
|
|
1305
|
+
await ledger.replay(async (body) => {
|
|
1306
|
+
await callRetry(body, signal, ticket);
|
|
1307
|
+
});
|
|
1308
|
+
if (pending !== void 0 && ticket === call && !signal.aborted && ownerSession === pending.sessionId && ownerMessage === pending.messageId) editing = false;
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (ticket === call && !signal.aborted) failure = readFailure(error);
|
|
1311
|
+
} finally {
|
|
1312
|
+
settle();
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function samePending(sessionId, id) {
|
|
1316
|
+
const pending = ledger.pending();
|
|
1317
|
+
return pending !== void 0 && pending.sessionId === sessionId && pending.messageId === id;
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
subscribe: events.subscribe,
|
|
1321
|
+
getSnapshot: () => snapshot,
|
|
1322
|
+
async load() {
|
|
1323
|
+
capabilities = await readCapabilities(host);
|
|
1324
|
+
ready = true;
|
|
1325
|
+
publish();
|
|
1326
|
+
},
|
|
1327
|
+
async retry(sessionId, id, signal) {
|
|
1328
|
+
if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
|
|
1329
|
+
if (ledger.pending() !== void 0) {
|
|
1330
|
+
if (!samePending(sessionId, id)) return;
|
|
1331
|
+
await replaySame(signal);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const ticket = ++call;
|
|
1335
|
+
busy = true;
|
|
1336
|
+
failure = void 0;
|
|
1337
|
+
editing = false;
|
|
1338
|
+
ownerSession = sessionId;
|
|
1339
|
+
ownerMessage = id;
|
|
1340
|
+
publish();
|
|
1341
|
+
try {
|
|
1342
|
+
const seen = await preview(sessionId, id, signal, ticket);
|
|
1343
|
+
if (seen === void 0) return;
|
|
1344
|
+
if (!seen.eligible) {
|
|
1345
|
+
failure = {
|
|
1346
|
+
code: "session/retry-blocked",
|
|
1347
|
+
reason: seen.reason ?? "unknown"
|
|
1348
|
+
};
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (signal.aborted || ticket !== call) return;
|
|
1352
|
+
expectedRevision = seen.branchRevision;
|
|
1353
|
+
expectedInputRevision = seen.inputRevision;
|
|
1354
|
+
const body = directRetryRequest({
|
|
1355
|
+
sessionId,
|
|
1356
|
+
messageId: id,
|
|
1357
|
+
expectedRevision: seen.branchRevision,
|
|
1358
|
+
expectedInputRevision: seen.inputRevision,
|
|
1359
|
+
requestId: ids()
|
|
1360
|
+
});
|
|
1361
|
+
await ledger.submit(body, () => callRetry(body, signal, ticket));
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (ticket === call && !signal.aborted) failure = readFailure(error);
|
|
1364
|
+
} finally {
|
|
1365
|
+
settle();
|
|
1366
|
+
}
|
|
1367
|
+
},
|
|
1368
|
+
async beginEdit(sessionId, id, text, signal) {
|
|
1369
|
+
if (busy || !controlsFor(capabilities, ready, host).retry || signal.aborted) return;
|
|
1370
|
+
if (ledger.pending() !== void 0) {
|
|
1371
|
+
if (!samePending(sessionId, id)) return;
|
|
1372
|
+
await replaySame(signal);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const ticket = ++call;
|
|
1376
|
+
busy = true;
|
|
1377
|
+
failure = void 0;
|
|
1378
|
+
ownerSession = sessionId;
|
|
1379
|
+
ownerMessage = id;
|
|
1380
|
+
publish();
|
|
1381
|
+
try {
|
|
1382
|
+
const seen = await preview(sessionId, id, signal, ticket);
|
|
1383
|
+
if (seen === void 0) return;
|
|
1384
|
+
if (!seen.eligible) {
|
|
1385
|
+
failure = {
|
|
1386
|
+
code: "session/retry-blocked",
|
|
1387
|
+
reason: seen.reason ?? "unknown"
|
|
1388
|
+
};
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (signal.aborted || ticket !== call) return;
|
|
1392
|
+
expectedRevision = seen.branchRevision;
|
|
1393
|
+
expectedInputRevision = seen.inputRevision;
|
|
1394
|
+
draft = text;
|
|
1395
|
+
editing = true;
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (ticket === call && !signal.aborted) failure = readFailure(error);
|
|
1398
|
+
} finally {
|
|
1399
|
+
settle();
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
setDraft(text) {
|
|
1403
|
+
if (!editing || ownerSession === "" || ownerMessage === "") return;
|
|
1404
|
+
draft = text;
|
|
1405
|
+
publish();
|
|
1406
|
+
},
|
|
1407
|
+
cancelEdit() {
|
|
1408
|
+
if (busy) return;
|
|
1409
|
+
editing = false;
|
|
1410
|
+
publish();
|
|
1411
|
+
},
|
|
1412
|
+
async sendEdit(sessionId, id, signal) {
|
|
1413
|
+
if (signal.aborted || busy || !controlsFor(capabilities, ready, host).retry) return;
|
|
1414
|
+
if (!editing || ownerSession !== sessionId || ownerMessage !== id) return;
|
|
1415
|
+
if (ledger.pending() !== void 0) {
|
|
1416
|
+
if (!samePending(sessionId, id)) return;
|
|
1417
|
+
await replaySame(signal);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const ticket = ++call;
|
|
1421
|
+
busy = true;
|
|
1422
|
+
failure = void 0;
|
|
1423
|
+
const text = draft;
|
|
1424
|
+
publish();
|
|
1425
|
+
try {
|
|
1426
|
+
if (signal.aborted || ticket !== call) return;
|
|
1427
|
+
const body = editRetryRequest({
|
|
1428
|
+
sessionId,
|
|
1429
|
+
messageId: id,
|
|
1430
|
+
expectedRevision,
|
|
1431
|
+
expectedInputRevision,
|
|
1432
|
+
requestId: ids(),
|
|
1433
|
+
text
|
|
1434
|
+
});
|
|
1435
|
+
await ledger.submit(body, () => callRetry(body, signal, ticket));
|
|
1436
|
+
if (ticket === call && !signal.aborted) editing = false;
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
if (ticket === call && !signal.aborted) failure = readFailure(error);
|
|
1439
|
+
} finally {
|
|
1440
|
+
settle();
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
retire(sessionId, messageId) {
|
|
1444
|
+
if (ownerSession !== sessionId || ownerMessage !== messageId) return;
|
|
1445
|
+
call += 1;
|
|
1446
|
+
editing = false;
|
|
1447
|
+
failure = void 0;
|
|
1448
|
+
draft = "";
|
|
1449
|
+
if (!ledger.busy) busy = false;
|
|
1450
|
+
publish();
|
|
1451
|
+
},
|
|
1452
|
+
cancel() {
|
|
1453
|
+
call += 1;
|
|
1454
|
+
editing = false;
|
|
1455
|
+
failure = void 0;
|
|
1456
|
+
if (!ledger.busy) busy = false;
|
|
1457
|
+
publish();
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function bindHost(ctx) {
|
|
1462
|
+
const sessions = ctx.sessions;
|
|
1463
|
+
const workspace = ctx.uiWorkspace;
|
|
1464
|
+
return {
|
|
1465
|
+
list: sessions?.list ?? {
|
|
1466
|
+
getSnapshot: () => ({
|
|
1467
|
+
ids: [],
|
|
1468
|
+
byId: {},
|
|
1469
|
+
phase: "ready"
|
|
1470
|
+
}),
|
|
1471
|
+
subscribe: () => () => {}
|
|
1472
|
+
},
|
|
1473
|
+
...typeof sessions?.capabilities === "function" ? { capabilities: () => sessions.capabilities() } : {},
|
|
1474
|
+
...typeof sessions?.create === "function" ? { create: (opts) => sessions.create(opts) } : {},
|
|
1475
|
+
...typeof sessions?.fork === "function" ? { fork: (opts) => sessions.fork(opts) } : {},
|
|
1476
|
+
...typeof sessions?.refresh === "function" ? { refresh: () => sessions.refresh() } : {},
|
|
1477
|
+
...typeof sessions?.previewRetry === "function" ? { previewRetry: (request) => sessions.previewRetry(request) } : {},
|
|
1478
|
+
...typeof sessions?.retry === "function" ? { retry: (request) => sessions.retry(request) } : {},
|
|
1479
|
+
...typeof workspace?.openSession === "function" ? { openSession: (sessionId) => workspace.openSession(sessionId) } : {},
|
|
1480
|
+
...typeof workspace?.forkSession === "function" ? { forkSession: (sessionId) => workspace.forkSession(sessionId) } : {},
|
|
1481
|
+
...typeof workspace?.deleteSession === "function" ? { deleteSession: (sessionId, requestId) => workspace.deleteSession(sessionId, requestId) } : {},
|
|
1482
|
+
...typeof ctx.layout?.selectPanel === "function" ? { selectPanel: (id) => ctx.layout.selectPanel(id) } : {}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
//#endregion
|
|
1486
|
+
//#region src/user-actions.tsx
|
|
1487
|
+
function UserMessageActions(props) {
|
|
1488
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
|
|
1489
|
+
const signal = (0, react.useRef)(new AbortController());
|
|
1490
|
+
(0, react.useEffect)(() => {
|
|
1491
|
+
const controller = new AbortController();
|
|
1492
|
+
signal.current = controller;
|
|
1493
|
+
return () => {
|
|
1494
|
+
controller.abort();
|
|
1495
|
+
props.model.retire(props.sessionId, props.messageId);
|
|
1496
|
+
};
|
|
1497
|
+
}, [
|
|
1498
|
+
props.model,
|
|
1499
|
+
props.sessionId,
|
|
1500
|
+
props.messageId
|
|
1501
|
+
]);
|
|
1502
|
+
if (props.sessionId === "" || !snapshot.controlsRetry) return null;
|
|
1503
|
+
const mine = snapshot.sessionId === props.sessionId && snapshot.messageId === props.messageId;
|
|
1504
|
+
const copy = (key) => uiText(props.locale, key);
|
|
1505
|
+
const failure = mine ? snapshot.failure : void 0;
|
|
1506
|
+
const editing = mine && snapshot.editing;
|
|
1507
|
+
const draft = mine ? snapshot.draft : props.text;
|
|
1508
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1509
|
+
className: `${FREE_CHAT_ROOT} dsh-free-actions`,
|
|
1510
|
+
children: [
|
|
1511
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1512
|
+
label: copy("retry"),
|
|
1513
|
+
side: "bottom",
|
|
1514
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1515
|
+
size: "sm",
|
|
1516
|
+
variant: "ghost",
|
|
1517
|
+
"aria-label": copy("retry"),
|
|
1518
|
+
disabled: snapshot.busy,
|
|
1519
|
+
onClick: () => {
|
|
1520
|
+
props.model.retry(props.sessionId, props.messageId, signal.current.signal);
|
|
1521
|
+
},
|
|
1522
|
+
children: copy("retry")
|
|
1523
|
+
})
|
|
1524
|
+
}),
|
|
1525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1526
|
+
label: copy("edit"),
|
|
1527
|
+
side: "bottom",
|
|
1528
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1529
|
+
size: "sm",
|
|
1530
|
+
variant: "ghost",
|
|
1531
|
+
"aria-label": copy("edit"),
|
|
1532
|
+
disabled: snapshot.busy,
|
|
1533
|
+
onClick: () => {
|
|
1534
|
+
props.model.beginEdit(props.sessionId, props.messageId, props.text, signal.current.signal);
|
|
1535
|
+
},
|
|
1536
|
+
children: copy("edit")
|
|
1537
|
+
})
|
|
1538
|
+
}),
|
|
1539
|
+
failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1540
|
+
className: "dsh-ui-error dsh-free-alert",
|
|
1541
|
+
role: "alert",
|
|
1542
|
+
children: failureText(props.locale, failure)
|
|
1543
|
+
}) : null,
|
|
1544
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1545
|
+
open: editing,
|
|
1546
|
+
className: FREE_CHAT_ROOT,
|
|
1547
|
+
title: copy("editTitle"),
|
|
1548
|
+
closeLabel: copy("close"),
|
|
1549
|
+
onClose: () => {
|
|
1550
|
+
props.model.cancelEdit();
|
|
1551
|
+
},
|
|
1552
|
+
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, {
|
|
1553
|
+
variant: "ghost",
|
|
1554
|
+
disabled: snapshot.busy,
|
|
1555
|
+
onClick: () => {
|
|
1556
|
+
props.model.cancelEdit();
|
|
1557
|
+
},
|
|
1558
|
+
children: copy("cancel")
|
|
1559
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1560
|
+
variant: "primary",
|
|
1561
|
+
"data-modal-autofocus": true,
|
|
1562
|
+
disabled: snapshot.busy,
|
|
1563
|
+
onClick: () => {
|
|
1564
|
+
props.model.sendEdit(props.sessionId, props.messageId, signal.current.signal);
|
|
1565
|
+
},
|
|
1566
|
+
children: copy("send")
|
|
1567
|
+
})] }),
|
|
1568
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1569
|
+
className: "dsh-free-draft",
|
|
1570
|
+
"aria-label": copy("draft"),
|
|
1571
|
+
value: draft,
|
|
1572
|
+
onChange: (event) => {
|
|
1573
|
+
props.model.setDraft(event.target.value);
|
|
1574
|
+
}
|
|
1575
|
+
})
|
|
1576
|
+
})
|
|
1577
|
+
]
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
//#endregion
|
|
1581
|
+
//#region src/client.tsx
|
|
1582
|
+
const name = "dsh-session-manager-client";
|
|
1583
|
+
const inject = [
|
|
1584
|
+
"slots",
|
|
1585
|
+
"locale",
|
|
1586
|
+
"sessions",
|
|
1587
|
+
"uiWorkspace",
|
|
1588
|
+
"layout"
|
|
1589
|
+
];
|
|
1590
|
+
function CopyThreadMenuItem(props) {
|
|
1591
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
1592
|
+
const [, setMenuOpen] = props.useMenuOpenState();
|
|
1593
|
+
const label = failed ? COPY_THREAD_FAILED[props.locale] : COPY_THREAD_LABEL[props.locale];
|
|
1594
|
+
const onSelect = () => copyThreadId(props.sessionId, props.writeText ?? writeClipboard).then(() => {
|
|
1595
|
+
setFailed(false);
|
|
1596
|
+
setMenuOpen(false);
|
|
1597
|
+
}, () => {
|
|
1598
|
+
setFailed(true);
|
|
1599
|
+
});
|
|
1600
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
|
|
1601
|
+
separatorBefore: true,
|
|
1602
|
+
onSelect,
|
|
1603
|
+
children: label
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
function apply(ctx) {
|
|
1607
|
+
const client = ctx;
|
|
1608
|
+
const host = bindHost(client);
|
|
1609
|
+
const chats = createFreeChatModel(host);
|
|
1610
|
+
const retries = createRetryModel(host);
|
|
1611
|
+
client.effect(() => {
|
|
1612
|
+
const stops = [];
|
|
1613
|
+
const localeStop = client.locale?.register?.("sessionManager", {
|
|
1614
|
+
zh: { panel: uiText("zh", "panel") },
|
|
1615
|
+
en: { panel: uiText("en", "panel") }
|
|
1616
|
+
});
|
|
1617
|
+
if (typeof localeStop === "function") stops.push(localeStop);
|
|
1618
|
+
stops.push(client.slots.inject("sidebar.workspaces.session.menu.item", () => client.slots.register({
|
|
1619
|
+
name: "sidebar.workspaces.session.menu.item",
|
|
1620
|
+
id: COPY_THREAD_MENU_ID,
|
|
1621
|
+
order: 500
|
|
1622
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadSlot, {
|
|
1623
|
+
locale: client.locale,
|
|
1624
|
+
...props
|
|
1625
|
+
}))));
|
|
1626
|
+
stops.push(client.slots.inject("sidebar.workspaces.session.menu.item", () => client.slots.register({
|
|
1627
|
+
name: "sidebar.workspaces.session.menu.item",
|
|
1628
|
+
id: DELETE_DIALOG_ID,
|
|
1629
|
+
order: 510
|
|
1630
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeleteMenuItem, {
|
|
1631
|
+
locale: client.locale,
|
|
1632
|
+
model: chats,
|
|
1633
|
+
...props
|
|
1634
|
+
}))));
|
|
1635
|
+
stops.push(client.slots.inject("main", () => client.slots.register({
|
|
1636
|
+
name: "main",
|
|
1637
|
+
key: FREE_PANEL_ID
|
|
1638
|
+
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatPanel, {
|
|
1639
|
+
model: chats,
|
|
1640
|
+
locale: useLiveLocale(client.locale)
|
|
1641
|
+
}))));
|
|
1642
|
+
stops.push(client.slots.inject("sidebar.panellist", () => client.slots.register({
|
|
1643
|
+
name: "sidebar.panellist",
|
|
1644
|
+
id: FREE_PANEL_ID,
|
|
1645
|
+
order: 20,
|
|
1646
|
+
label: () => uiText(copyLocale(client.locale?.getSnapshot().active), "panel")
|
|
1647
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatIcon, { size: props.size ?? 16 }))));
|
|
1648
|
+
stops.push(client.slots.inject(USER_ACTIONS_SLOT, () => client.slots.register({
|
|
1649
|
+
name: USER_ACTIONS_SLOT,
|
|
1650
|
+
id: USER_ACTIONS_ID,
|
|
1651
|
+
order: 10,
|
|
1652
|
+
inject: (sessionId) => ({ sessionId })
|
|
1653
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMessageActions, {
|
|
1654
|
+
messageId: props.messageId,
|
|
1655
|
+
text: props.text,
|
|
1656
|
+
sessionId: props.sessionId ?? "",
|
|
1657
|
+
locale: useLiveLocale(client.locale),
|
|
1658
|
+
model: retries
|
|
1659
|
+
}))));
|
|
1660
|
+
stops.push(client.slots.inject("shell.overlay", () => client.slots.register({
|
|
1661
|
+
name: "shell.overlay",
|
|
1662
|
+
id: DELETE_DIALOG_ID,
|
|
1663
|
+
order: 40
|
|
1664
|
+
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: freeChatCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeleteChatDialog, {
|
|
1665
|
+
model: chats,
|
|
1666
|
+
locale: useLiveLocale(client.locale)
|
|
1667
|
+
})] }))));
|
|
1668
|
+
const reset = client.on?.("connection/reset", () => {
|
|
1669
|
+
chats.reloadCapabilities();
|
|
1670
|
+
retries.load();
|
|
1671
|
+
});
|
|
1672
|
+
if (typeof reset === "function") stops.push(reset);
|
|
1673
|
+
chats.reloadCapabilities();
|
|
1674
|
+
retries.load();
|
|
1675
|
+
return () => {
|
|
1676
|
+
chats.cancel();
|
|
1677
|
+
retries.cancel();
|
|
1678
|
+
for (const stop of stops) stop();
|
|
1679
|
+
};
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
function CopyThreadSlot(props) {
|
|
1683
|
+
const locale = useLiveLocale(props.locale);
|
|
1684
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadMenuItem, {
|
|
1685
|
+
sessionId: props.sessionId,
|
|
1686
|
+
useMenuOpenState: props.useMenuOpenState,
|
|
1687
|
+
locale
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
function DeleteMenuItem(props) {
|
|
1691
|
+
const locale = useLiveLocale(props.locale);
|
|
1692
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
|
|
1693
|
+
const [, setMenuOpen] = props.useMenuOpenState();
|
|
1694
|
+
if (!snapshot.controls.delete) return null;
|
|
1695
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
|
|
1696
|
+
onSelect: () => {
|
|
1697
|
+
props.model.requestDelete(props.sessionId);
|
|
1698
|
+
setMenuOpen(false);
|
|
1699
|
+
},
|
|
1700
|
+
children: uiText(locale, "deleteChat")
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
function useLiveLocale(locale) {
|
|
1704
|
+
return copyLocale((0, react.useSyncExternalStore)((listener) => locale?.subscribe(listener) ?? (() => {}), () => locale?.getSnapshot().active ?? "zh", () => "zh"));
|
|
1705
|
+
}
|
|
1706
|
+
async function writeClipboard(value) {
|
|
1707
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
1708
|
+
if (clipboard === void 0) throw new Error("clipboard unavailable");
|
|
1709
|
+
await clipboard.writeText(value);
|
|
1710
|
+
}
|
|
1711
|
+
//#endregion
|
|
1712
|
+
exports.CopyThreadMenuItem = CopyThreadMenuItem;
|
|
1713
|
+
exports.apply = apply;
|
|
1714
|
+
exports.inject = inject;
|
|
1715
|
+
exports.name = name;
|
|
1716
|
+
|
|
1717
|
+
//# sourceMappingURL=client.inner.cjs.map
|