@sparkelf/dsh-group-chat 0.0.0-stage → 0.1.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/lib/client.js ADDED
@@ -0,0 +1,4238 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@sparkelf/dsh-group-chat",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_dom = require("react-dom");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region src/client/api.ts
12
+ /**
13
+ * Room-page transport.
14
+ *
15
+ * The browser talks only to the Host plugin's own route. The Host holds the
16
+ * workspace credential and forwards to the server under the workspace identity, so
17
+ * this module never sees a token and cannot disagree with the agent about who the
18
+ * caller is.
19
+ *
20
+ * @module @sparkelf/dsh-group-chat/client/api
21
+ */
22
+ /** The Host route registered by this plugin's own Host half. */
23
+ const GC_API = "/dsh-group-chat/api";
24
+ /** A failure the page renders as copy rather than as an exception. */
25
+ var RoomError = class extends Error {
26
+ status;
27
+ constructor(message, status) {
28
+ super(message);
29
+ this.status = status;
30
+ this.name = "RoomError";
31
+ }
32
+ };
33
+ const call = async (path, init) => {
34
+ let response;
35
+ try {
36
+ response = await fetch(`${GC_API}${path}`, {
37
+ ...init,
38
+ headers: {
39
+ ...init?.body === void 0 ? {} : { "content-type": "application/json" },
40
+ ...init?.headers
41
+ },
42
+ credentials: "same-origin"
43
+ });
44
+ } catch (cause) {
45
+ throw new RoomError("unreachable", 0);
46
+ }
47
+ if (!response.ok) {
48
+ const status = response.status;
49
+ if (status === 503) throw new RoomError("credentialMissing", status);
50
+ if (status === 401 || status === 403) throw new RoomError("credentialRejected", status);
51
+ if (status === 404) throw new RoomError("notConfigured", status);
52
+ if (status === 409) throw new RoomError("ownerCannotLeave", status);
53
+ throw new RoomError("unreachable", status);
54
+ }
55
+ if (response.status === 204) return void 0;
56
+ return await response.json();
57
+ };
58
+ /**
59
+ * The account this workspace acts as.
60
+ *
61
+ * The message renderer needs it to tell the operator's own messages from another member's,
62
+ * and the browser holds no credential of its own to ask DataOps directly — the Host resolves
63
+ * it from the same identity that decides message routing, so the two cannot disagree.
64
+ *
65
+ * @returns the account id, or `undefined` when the Host cannot identify this workspace.
66
+ */
67
+ const readSelf = async () => {
68
+ try {
69
+ const me = await call("/me");
70
+ if (typeof me?.id !== "string" || me.id.length === 0) return void 0;
71
+ const administrator = Array.isArray(me.roleCodes) && me.roleCodes.some((code) => code === "管理员" || code === "超级管理员");
72
+ return {
73
+ id: me.id,
74
+ administrator
75
+ };
76
+ } catch {
77
+ return;
78
+ }
79
+ };
80
+ /** Rooms this workspace has joined. */
81
+ const listRooms = () => call("/rooms");
82
+ /**
83
+ * Rooms this account could join, as opposed to the ones it is already in.
84
+ *
85
+ * Kept separate from {@link listRooms} because the two answer different questions: the sidebar
86
+ * shows what you belong to, while this is consulted only when the operator is looking for
87
+ * something to join. Merging them would make every list read ask for rooms the account has no
88
+ * relationship with.
89
+ *
90
+ * @param query - optional narrowing keyword, applied by the server.
91
+ * @returns matching rooms, each carrying `joined` so the caller can tell them apart.
92
+ */
93
+ const listDiscoverableRooms = (query) => {
94
+ const suffix = query === void 0 || query.trim().length === 0 ? "" : `?query=${encodeURIComponent(query.trim())}`;
95
+ return call(`/discover${suffix}`);
96
+ };
97
+ /**
98
+ * Take a seat in a room the server allows this account to join.
99
+ *
100
+ * The decision to allow it is the server's: it re-reads discoverability, invite policy and
101
+ * membership at the moment of the press, so a room offered a minute ago may still be refused.
102
+ * That refusal arrives as a `RoomError` and is shown as-is rather than being pre-empted here.
103
+ *
104
+ * @param roomId - the room to join.
105
+ * @returns the server's answer, including whether the account was already a member.
106
+ */
107
+ const joinRoom = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}/join`, { method: "POST" });
108
+ const listMembers = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}/members`);
109
+ /**
110
+ * Accounts that match `query` and are not yet in the room.
111
+ *
112
+ * The search runs on the Host, which holds the credential: the browser has no DataOps token
113
+ * of its own, so a client-side filter over a full directory is not an option here. An empty
114
+ * query is valid and returns a browsable first page rather than nothing, which is what makes
115
+ * the picker usable before the operator has typed anything.
116
+ */
117
+ const searchInviteCandidates = (roomId, query) => {
118
+ const suffix = query.length === 0 ? "" : `?q=${encodeURIComponent(query)}`;
119
+ return call(`/rooms/${encodeURIComponent(roomId)}/invite-candidates${suffix}`);
120
+ };
121
+ /** Preset names this deployment offers. An empty list means the picker falls back to typing. */
122
+ const readOptions = () => call("/options");
123
+ /** Child directories of `path`, for the working-directory picker. */
124
+ const listDirectories = (path) => {
125
+ const suffix = path === void 0 || path.length === 0 ? "" : `?path=${encodeURIComponent(path)}`;
126
+ return call(`/directories${suffix}`);
127
+ };
128
+ /**
129
+ * Create a room.
130
+ *
131
+ * `cwd` and the preset are sent at creation because they decide the scope every member
132
+ * will share; the server does not accept them afterwards.
133
+ */
134
+ const createRoom = (name, execution) => call("/rooms", {
135
+ method: "POST",
136
+ body: JSON.stringify({
137
+ name,
138
+ ...execution
139
+ })
140
+ });
141
+ /**
142
+ * Set or clear a room's announcement.
143
+ *
144
+ * Only the room owner may write it; the server decides that, so a refusal is surfaced as-is
145
+ * rather than pre-empted here. An empty string clears the announcement — the server stores
146
+ * that as no announcement, which is why the panel treats `''` and `null` alike when reading.
147
+ *
148
+ * @param roomId - the room whose announcement to write.
149
+ * @param announcement - the new text, or an empty string to clear it.
150
+ */
151
+ const setAnnouncement = (roomId, announcement) => call(`/rooms/${encodeURIComponent(roomId)}/announcement`, {
152
+ method: "POST",
153
+ body: JSON.stringify({ announcement })
154
+ });
155
+ const updateRoom = (roomId, patch) => call(`/rooms/${encodeURIComponent(roomId)}/update`, {
156
+ method: "POST",
157
+ body: JSON.stringify(patch)
158
+ });
159
+ /**
160
+ * Hand ownership of a room to another member.
161
+ *
162
+ * The reason this exists at all is that a room's owner cannot leave it: leaving would strand the
163
+ * room with nobody able to change its settings. Transferring is the way out, so the server owns
164
+ * the rule (owner only, target must be a member) and this only carries the request.
165
+ */
166
+ const transferOwnership = (roomId, userId) => call(`/rooms/${encodeURIComponent(roomId)}/transfer`, {
167
+ method: "POST",
168
+ body: JSON.stringify({ userId })
169
+ });
170
+ const inviteMembers = (roomId, userIds) => call(`/rooms/${encodeURIComponent(roomId)}/members`, {
171
+ method: "POST",
172
+ body: JSON.stringify({ userIds })
173
+ });
174
+ const leaveRoom = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}/members/me`, { method: "DELETE" });
175
+ /** Reads the stored preferences. Never fails: an unreadable file reads as "nothing saved". */
176
+ const readPreferences = async () => {
177
+ try {
178
+ return await call("/preferences");
179
+ } catch {
180
+ return {};
181
+ }
182
+ };
183
+ /** Stores the preferences and returns what the host settled on, clamped. */
184
+ const savePreferences = (patch) => call("/preferences", {
185
+ method: "PATCH",
186
+ body: JSON.stringify(patch)
187
+ });
188
+ /**
189
+ * List a room's shared folder.
190
+ *
191
+ * An empty `path` means this deployment has no shared tree, which the panel renders as
192
+ * "not available" rather than as an empty folder — the two read differently to a member.
193
+ */
194
+ const listRoomFiles = (roomId, path) => call(`/rooms/${encodeURIComponent(roomId)}/files${path === void 0 || path.length === 0 ? "" : `?path=${encodeURIComponent(path)}`}`);
195
+ /** Read one file out of a room's shared folder. */
196
+ const getRoomFile = (roomId, path) => call(`/rooms/${encodeURIComponent(roomId)}/file-content?path=${encodeURIComponent(path)}`);
197
+ /**
198
+ * Upload one file into a room's shared folder.
199
+ *
200
+ * Base64 in JSON rather than multipart: the plugin's request path reads one JSON envelope for
201
+ * every operation, and a second body format would need its own parser, limits, and error
202
+ * mapping for no gain.
203
+ *
204
+ * @param roomId - the room whose folder receives the file.
205
+ * @param file - the browser's File object.
206
+ * @returns the written file.
207
+ */
208
+ const uploadRoomFile = async (roomId, file) => {
209
+ const buffer = new Uint8Array(await file.arrayBuffer());
210
+ let binary = "";
211
+ for (let index = 0; index < buffer.length; index += 1) binary += String.fromCharCode(buffer[index]);
212
+ return call(`/rooms/${encodeURIComponent(roomId)}/files`, {
213
+ method: "POST",
214
+ body: JSON.stringify({
215
+ name: file.name,
216
+ data: btoa(binary)
217
+ })
218
+ });
219
+ };
220
+ /**
221
+ * Remove one member from a room.
222
+ *
223
+ * The reserved word `me` keeps its meaning: removal always names another account, and the
224
+ * server rejects the owner as a target. Leaving therefore never goes through this call.
225
+ */
226
+ const removeMember = (roomId, userId) => call(`/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(userId)}`, { method: "DELETE" });
227
+ /**
228
+ * Dissolve a room outright.
229
+ *
230
+ * Distinct from {@link leaveRoom}: leaving removes one membership and the room keeps existing
231
+ * for everyone else, while dissolving ends the room for every member. The server decides who
232
+ * may do it (the owner, or an administrator), so a refusal surfaces as copy rather than being
233
+ * pre-empted here.
234
+ */
235
+ const dissolveRoom = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}`, { method: "DELETE" });
236
+ //#endregion
237
+ //#region src/client/rows.ts
238
+ /**
239
+ * The decisions the plugin's components make without a browser.
240
+ *
241
+ * They live outside their components on purpose. The components import the official
242
+ * primitives, which bring CSS modules that this repository's test runner cannot transform,
243
+ * so anything worth asserting has to be reachable without mounting a component. That
244
+ * constraint turns out to match the risk: what an operator sees — which rows survive a
245
+ * query, what a picture falls back to, who carries the owner tag, whether an empty
246
+ * announcement area means "write one" or "this deployment cannot store one" — is exactly
247
+ * what goes wrong quietly, and it is all decidable from plain values.
248
+ *
249
+ * @module @sparkelf/dsh-group-chat/client/rows
250
+ */
251
+ /**
252
+ * The locale namespace the shipped presets publish their names in.
253
+ *
254
+ * Read as a foreign namespace rather than one this plugin registers: `standard` and the other
255
+ * three are declared by the platform, and their copy is owned there. Reprinting that copy here
256
+ * would freeze today's wording and drift from the hero chip the moment the platform changes it.
257
+ */
258
+ const PRESET_LOCALE_NS = "settings.agentPreset";
259
+ /** Locale keys the shipped presets use, keyed by the id the registry publishes. */
260
+ const BUILT_IN_PRESET_KEYS = {
261
+ standard: {
262
+ name: "presetStandardName",
263
+ description: "presetStandardDescription"
264
+ },
265
+ ptc: {
266
+ name: "presetPtcName",
267
+ description: "presetPtcDescription"
268
+ },
269
+ minimal: {
270
+ name: "presetMinimalName",
271
+ description: "presetMinimalDescription"
272
+ },
273
+ cordis: {
274
+ name: "presetCordisName",
275
+ description: "presetCordisDescription"
276
+ }
277
+ };
278
+ /**
279
+ * A preset's display copy, following the platform's own rule.
280
+ *
281
+ * A shipped preset publishes no `name`, so its copy is a locale lookup; a preset that names
282
+ * itself in YAML owns its own words and is never translated. The split matters because the two
283
+ * arrive as the same `{ id, name? }` shape — treating a published name as a locale key would
284
+ * render the key itself, and treating a shipped id as self-named would render `standard`.
285
+ *
286
+ * @param preset - one roster row.
287
+ * @param t - lookup for the preset locale namespace; a missing key resolves to the key, so the
288
+ * fallback below is what keeps a renamed platform key from reaching the operator.
289
+ * @returns the name and optional description to draw.
290
+ */
291
+ const presetDisplay = (preset, t) => {
292
+ const keys = preset.name === void 0 ? BUILT_IN_PRESET_KEYS[preset.id] : void 0;
293
+ if (keys === void 0) return {
294
+ name: preset.name ?? preset.id,
295
+ ...preset.description === void 0 ? {} : { description: preset.description }
296
+ };
297
+ const name = t(keys.name);
298
+ const description = t(keys.description);
299
+ return {
300
+ name: name === keys.name ? preset.id : name,
301
+ ...description === keys.description ? {} : { description }
302
+ };
303
+ };
304
+ /**
305
+ * Filter rows by what the operator typed.
306
+ *
307
+ * Matching happens here rather than only at the source because the source's answer arrives a
308
+ * debounce behind the keystroke; re-filtering the rows already held narrows the list on the
309
+ * same frame instead of one request later. `secondary` participates because a member's handle
310
+ * is often what the operator knows them by.
311
+ *
312
+ * @param options - the rows currently held.
313
+ * @param query - the raw input.
314
+ * @returns the rows to draw, in their original order.
315
+ */
316
+ const filterOptions = (options, query) => {
317
+ const needle = query.trim().toLowerCase();
318
+ if (needle.length === 0) return options;
319
+ return options.filter((option) => option.label.toLowerCase().includes(needle) || (option.secondary ?? "").toLowerCase().includes(needle));
320
+ };
321
+ /**
322
+ * Add or remove one person from the invite selection.
323
+ *
324
+ * The selection is kept apart from the search result rows because the two answer different
325
+ * questions: the rows are "what the current query found", while the selection is "who will be
326
+ * invited". A narrowed query legitimately hides a person already picked, and deriving the
327
+ * selection from the visible rows would drop them the moment the operator typed another name.
328
+ *
329
+ * The row is stored whole, not by id, so a chip can still draw the name and picture after the
330
+ * search that produced it has been replaced.
331
+ *
332
+ * @param chosen - the current selection, in pick order.
333
+ * @param candidate - the row the operator pressed.
334
+ * @returns the new selection; a second press on the same id removes it, and identity is stable
335
+ * when the press changed nothing.
336
+ */
337
+ const toggleChosen = (chosen, candidate) => chosen.some((member) => member.userId === candidate.userId) ? chosen.filter((member) => member.userId !== candidate.userId) : [...chosen, candidate];
338
+ /**
339
+ * Narrow the invite candidates to what the operator typed.
340
+ *
341
+ * The same rule the server applies, applied again to the rows already held, for one reason:
342
+ * the server's answer arrives a debounce behind the keystroke, and the list would otherwise
343
+ * sit still while the operator types. Re-filtering locally makes the narrowing immediate; the
344
+ * server request still happens, and its answer replaces these rows a moment later.
345
+ *
346
+ * Both the display name and the handle participate, because the handle is often what a
347
+ * colleague is known by — the server's search matches the same pair.
348
+ *
349
+ * @param candidates - the rows currently held, from the last answer.
350
+ * @param query - the raw input.
351
+ * @returns the rows to draw, in their original order.
352
+ */
353
+ const filterCandidates = (candidates, query) => {
354
+ const needle = query.trim().toLowerCase();
355
+ if (needle.length === 0) return candidates;
356
+ return candidates.filter((candidate) => candidate.displayName.toLowerCase().includes(needle) || candidate.username.toLowerCase().includes(needle));
357
+ };
358
+ /**
359
+ * Whether one candidate row is part of the selection.
360
+ *
361
+ * @param chosen - the current selection.
362
+ * @param userId - the row's account id.
363
+ * @returns whether the row draws as selected.
364
+ */
365
+ const isChosen = (chosen, userId) => chosen.some((member) => member.userId === userId);
366
+ /**
367
+ * Up to two initials from a display name.
368
+ *
369
+ * Names arriving here are DataOps display names, which are as often Chinese as Latin. A CJK
370
+ * name has no spaces to split on, so the first two characters are taken as-is; a Latin name
371
+ * takes the first letter of its first two words. Both paths return at most two glyphs, which
372
+ * is what keeps the circle from overflowing at either size.
373
+ *
374
+ * @param name - the display name, possibly empty.
375
+ * @returns one or two characters, or `'?'` when the name carries nothing to draw.
376
+ */
377
+ const initialsOf = (name) => {
378
+ const trimmed = name.trim();
379
+ if (trimmed.length === 0) return "?";
380
+ if (/^[\u3400-\u9fff\uf900-\ufaff]/u.test(trimmed)) return [...trimmed].slice(0, 2).join("");
381
+ const words = trimmed.split(/\s+/u).filter((word) => word.length > 0);
382
+ if (words.length === 1) return [...words[0]].slice(0, 2).join("").toUpperCase();
383
+ return (words[0][0] + words[1][0]).toUpperCase();
384
+ };
385
+ /**
386
+ * The rooms worth offering as joinable.
387
+ *
388
+ * Two filters, for two different failures. `joined` is belt-and-braces: the Host already strips
389
+ * joined rooms from discovery, but this list arrives from a request the sidebar did not wait
390
+ * for, so a room the operator joined a moment ago can still be in the response.
391
+ *
392
+ * The member-id check covers the other window: the discovery read and the sidebar's own list
393
+ * come from separate requests, so a room can be in both for a moment. Without it the same room
394
+ * would appear twice, and the copy under the discovery heading would offer a Join button that
395
+ * can only fail.
396
+ *
397
+ * @param discoverable - what the discovery read returned.
398
+ * @param memberIds - ids of the rooms the sidebar is already showing.
399
+ * @returns the rooms to render under the discovery heading.
400
+ */
401
+ const joinableRooms = (discoverable, memberIds) => discoverable.filter((room) => !room.joined && !memberIds.has(room.id));
402
+ /**
403
+ * Whether a member owns the room.
404
+ *
405
+ * `owner` is the field every server has ever sent, `role` is the one the announcement work
406
+ * added. Preferring `role` and falling back to `owner` keeps the tag correct against either
407
+ * server, and costs nothing: the two carry the same fact.
408
+ *
409
+ * @param member - one roster row.
410
+ * @returns whether the row is the room's owner.
411
+ */
412
+ const memberIsOwner = (member) => member.role === void 0 ? member.owner === true : member.role === "owner";
413
+ /**
414
+ * Roster rows for the panel, owner pinned first.
415
+ *
416
+ * Only the owner is moved. Everyone else keeps the order the server sent, which is join
417
+ * order — a roster re-sorted alphabetically would scramble the one fact a reader can
418
+ * actually see in it (who has been here longest), and would do so differently for a CJK
419
+ * name than for a Latin one.
420
+ *
421
+ * @param members - the roster as the store holds it.
422
+ * @param selfId - this workspace's account id, `undefined` while identity is still unknown.
423
+ * @returns rows to draw.
424
+ */
425
+ const memberRows = (members, selfId) => {
426
+ return members.map((member) => ({
427
+ userId: member.userId,
428
+ name: member.displayName.length > 0 ? member.displayName : member.userId,
429
+ avatarUrl: member.avatarUrl,
430
+ owner: memberIsOwner(member),
431
+ self: selfId !== void 0 && member.userId === selfId
432
+ })).slice().sort((left, right) => Number(right.owner) - Number(left.owner));
433
+ };
434
+ /**
435
+ * Narrow the roster to what the operator typed.
436
+ *
437
+ * The account id participates because it is what another member is often known by — the
438
+ * invite picker searches the handle for the same reason.
439
+ *
440
+ * @param rows - the roster rows currently held.
441
+ * @param query - the raw input.
442
+ * @returns rows to draw, in their order.
443
+ */
444
+ const filterMemberRows = (rows, query) => {
445
+ const needle = query.trim().toLowerCase();
446
+ if (needle.length === 0) return rows;
447
+ return rows.filter((row) => row.name.toLowerCase().includes(needle) || row.userId.toLowerCase().includes(needle));
448
+ };
449
+ /**
450
+ * Read one room's announcement into the state its area draws.
451
+ *
452
+ * @param announcement - the room summary's field.
453
+ * @returns the state to render.
454
+ */
455
+ const announcementView = (announcement) => {
456
+ if (announcement === void 0) return { state: "unavailable" };
457
+ const text = (announcement ?? "").trim();
458
+ return text.length === 0 ? { state: "empty" } : {
459
+ state: "text",
460
+ text
461
+ };
462
+ };
463
+ /**
464
+ * Whether a failed write means this deployment has no announcement feature.
465
+ *
466
+ * A rolled-back deployment answers the route with 404 (no route) or 501/405 (a route that
467
+ * refuses the method). Those are the only failures that should switch the area to
468
+ * "unavailable": a 401 or a 5xx is this request's problem, and reporting it as a missing
469
+ * feature would send the operator looking at the wrong thing.
470
+ *
471
+ * @param error - whatever the write threw.
472
+ * @returns whether the feature should be regarded as absent.
473
+ */
474
+ const announcementMissing = (error) => error instanceof RoomError && (error.status === 404 || error.status === 405 || error.status === 501);
475
+ //#endregion
476
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/Avatar.module.css.mjs
477
+ const css$8 = ".Ohch6W_avatar{letter-spacing:.02em;color:#fff;background:var(--dsw-static-blue-600,#2563eb);user-select:none;border-radius:50%;flex:none;justify-content:center;align-items:center;font-weight:600;display:inline-flex;overflow:hidden}.Ohch6W_md{width:30px;height:30px;font-size:12px;line-height:30px}.Ohch6W_sm{width:24px;height:24px;font-size:10px;line-height:24px}.Ohch6W_stack{flex:none;display:inline-flex;position:relative}.Ohch6W_over{background:0 0;position:absolute;inset:0}img.Ohch6W_avatar{object-fit:cover;background:0 0}";
478
+ const tagId$8 = "@sparkelf/dsh-group-chat/Avatar.module.css";
479
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$8) + "]") === null) {
480
+ const tag = document.createElement("style");
481
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
482
+ tag.dataset.pluginCss = tagId$8;
483
+ tag.textContent = css$8;
484
+ document.head.appendChild(tag);
485
+ }
486
+ var Avatar_module_css_default = {
487
+ "sm": "Ohch6W_sm",
488
+ "over": "Ohch6W_over",
489
+ "md": "Ohch6W_md",
490
+ "avatar": "Ohch6W_avatar",
491
+ "stack": "Ohch6W_stack"
492
+ };
493
+ //#endregion
494
+ //#region src/client/Avatar.tsx
495
+ /** One member's picture, or their initials when there is no picture to show. */
496
+ const Avatar = (props) => {
497
+ const { name, src, size = "md" } = props;
498
+ const className = `${Avatar_module_css_default["avatar"]} ${size === "sm" ? Avatar_module_css_default["sm"] : Avatar_module_css_default["md"]}`;
499
+ const fallback = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
500
+ className,
501
+ role: "img",
502
+ "aria-label": name,
503
+ children: initialsOf(name)
504
+ });
505
+ if (typeof src !== "string" || src.length === 0) return fallback;
506
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
507
+ className: Avatar_module_css_default["stack"],
508
+ children: [fallback, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
509
+ className: `${Avatar_module_css_default["avatar"]} ${size === "sm" ? Avatar_module_css_default["sm"] : Avatar_module_css_default["md"]} ${Avatar_module_css_default["over"]}`,
510
+ src,
511
+ alt: "",
512
+ "aria-hidden": "true",
513
+ onError: (event) => {
514
+ event.currentTarget.style.display = "none";
515
+ }
516
+ })]
517
+ });
518
+ };
519
+ //#endregion
520
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/Picker.module.css.mjs
521
+ const css$7 = ".sKjJ-a_box{flex-direction:column;min-width:0;display:flex;position:relative}.sKjJ-a_list{z-index:10;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-base,var(--dsw-alias-interactive-bg-hover));max-height:240px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;box-shadow:0 8px 24px #0000003d}.sKjJ-a_row,.sKjJ-a_rowActive{border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:6px 8px;font-size:13px;line-height:18px;display:flex}.sKjJ-a_rowActive{background:var(--dsw-alias-interactive-bg-hover)}.sKjJ-a_label{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.sKjJ-a_secondary{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px}.sKjJ-a_note{color:var(--dsw-alias-label-tertiary);margin:6px 8px;font-size:12px;line-height:17px}";
522
+ const tagId$7 = "@sparkelf/dsh-group-chat/Picker.module.css";
523
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$7) + "]") === null) {
524
+ const tag = document.createElement("style");
525
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
526
+ tag.dataset.pluginCss = tagId$7;
527
+ tag.textContent = css$7;
528
+ document.head.appendChild(tag);
529
+ }
530
+ var Picker_module_css_default = {
531
+ "note": "sKjJ-a_note",
532
+ "secondary": "sKjJ-a_secondary",
533
+ "list": "sKjJ-a_list",
534
+ "row": "sKjJ-a_row",
535
+ "box": "sKjJ-a_box",
536
+ "rowActive": "sKjJ-a_rowActive",
537
+ "label": "sKjJ-a_label"
538
+ };
539
+ //#endregion
540
+ //#region src/client/Picker.tsx
541
+ /**
542
+ * A searchable list of choices, for the fields that used to be free text.
543
+ *
544
+ * Three of the room dialogs ask for values the deployment already knows — who can be
545
+ * invited, which preset to run, which directory to work in — and each used to be an empty
546
+ * box that only accepted the answer if it was spelled exactly right. A wrong userId failed
547
+ * only after the invite was submitted, and a wrong directory silently became a room the
548
+ * agent could not work in. This component is the shared answer so all three behave alike
549
+ * rather than each growing its own behaviour.
550
+ *
551
+ * It is deliberately not a `<datalist>`: the options come from an async source, the row
552
+ * needs a picture and a secondary line, and a datalist cannot group or report why it is
553
+ * empty. It is also not built on the official primitives because none of them is a
554
+ * combobox — the package ships Button, Input, Menu and friends, but no searchable select.
555
+ *
556
+ * @module @sparkelf/dsh-group-chat/client/Picker
557
+ */
558
+ /**
559
+ * Sentinel id for the "use what I typed" row.
560
+ *
561
+ * A real id cannot collide with it because account ids, preset names and paths are all
562
+ * printable, so a NUL is outside every id space this component is handed.
563
+ */
564
+ const TYPED_ROW_ID = "\0typed";
565
+ /**
566
+ * A text field with a filtered list beneath it.
567
+ *
568
+ * Keyboard support is the reason this is not a plain list of buttons: ↑/↓ move the highlight
569
+ * without leaving the field, Enter takes the highlighted row, and Escape first closes the
570
+ * list and only then reaches the dialog, so a stray Escape while searching does not discard
571
+ * the whole dialog.
572
+ */
573
+ const Picker = (props) => {
574
+ const { options, placeholder, emptyHint, loadingHint, loading = false, onQueryChange, onTypedCommit, typedCommitLabel, onPick, value, autoFocus = false } = props;
575
+ const [query, setQuery] = (0, react.useState)(value ?? "");
576
+ const [open, setOpen] = (0, react.useState)(autoFocus);
577
+ const [active, setActive] = (0, react.useState)(0);
578
+ const boxRef = (0, react.useRef)(null);
579
+ (0, react.useEffect)(() => {
580
+ setQuery(value ?? "");
581
+ }, [value]);
582
+ const visible = [...onTypedCommit !== void 0 && query.trim().length > 0 ? [{
583
+ id: TYPED_ROW_ID,
584
+ label: typedCommitLabel?.(query.trim()) ?? query.trim()
585
+ }] : [], ...filterOptions(options, query)];
586
+ (0, react.useEffect)(() => {
587
+ setActive(0);
588
+ }, [query]);
589
+ const pick = (0, react.useCallback)((option) => {
590
+ if (option === void 0) return;
591
+ if (option.id === TYPED_ROW_ID) {
592
+ onTypedCommit?.(query.trim());
593
+ return;
594
+ }
595
+ onPick(option);
596
+ }, [
597
+ onPick,
598
+ onTypedCommit,
599
+ query
600
+ ]);
601
+ (0, react.useEffect)(() => {
602
+ if (!open) return;
603
+ const onPointerDown = (event) => {
604
+ if (boxRef.current !== null && !boxRef.current.contains(event.target)) setOpen(false);
605
+ };
606
+ document.addEventListener("pointerdown", onPointerDown);
607
+ return () => {
608
+ document.removeEventListener("pointerdown", onPointerDown);
609
+ };
610
+ }, [open]);
611
+ const onKeyDown = (event) => {
612
+ if (event.key === "ArrowDown") {
613
+ event.preventDefault();
614
+ setOpen(true);
615
+ setActive((current) => visible.length === 0 ? 0 : (current + 1) % visible.length);
616
+ return;
617
+ }
618
+ if (event.key === "ArrowUp") {
619
+ event.preventDefault();
620
+ setActive((current) => visible.length === 0 ? 0 : (current - 1 + visible.length) % visible.length);
621
+ return;
622
+ }
623
+ if (event.key === "Enter") {
624
+ if (open && visible.length > 0) {
625
+ event.preventDefault();
626
+ pick(visible[active]);
627
+ }
628
+ return;
629
+ }
630
+ if (event.key === "Escape" && open) {
631
+ event.preventDefault();
632
+ event.stopPropagation();
633
+ setOpen(false);
634
+ }
635
+ };
636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
637
+ className: Picker_module_css_default["box"],
638
+ ref: boxRef,
639
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
640
+ value: query,
641
+ autoFocus,
642
+ placeholder,
643
+ role: "combobox",
644
+ "aria-expanded": open,
645
+ "aria-autocomplete": "list",
646
+ onFocus: () => {
647
+ setOpen(true);
648
+ },
649
+ onChange: (event) => {
650
+ setQuery(event.target.value);
651
+ setOpen(true);
652
+ onQueryChange?.(event.target.value);
653
+ },
654
+ onKeyDown
655
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
656
+ className: Picker_module_css_default["list"],
657
+ role: "listbox",
658
+ children: [
659
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
660
+ className: Picker_module_css_default["note"],
661
+ children: loadingHint ?? ""
662
+ }),
663
+ !loading && visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
664
+ className: Picker_module_css_default["note"],
665
+ children: emptyHint ?? ""
666
+ }),
667
+ !loading && visible.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
668
+ type: "button",
669
+ role: "option",
670
+ "aria-selected": index === active,
671
+ className: index === active ? Picker_module_css_default["rowActive"] : Picker_module_css_default["row"],
672
+ onMouseEnter: () => {
673
+ setActive(index);
674
+ },
675
+ onClick: () => {
676
+ pick(option);
677
+ },
678
+ children: [
679
+ option.avatarId !== void 0 || option.avatarUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
680
+ name: option.label,
681
+ src: option.avatarUrl,
682
+ size: "sm"
683
+ }) : null,
684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
685
+ className: Picker_module_css_default["label"],
686
+ children: option.label
687
+ }),
688
+ option.secondary !== void 0 && option.secondary.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
689
+ className: Picker_module_css_default["secondary"],
690
+ children: option.secondary
691
+ })
692
+ ]
693
+ }, option.id))
694
+ ]
695
+ })]
696
+ });
697
+ };
698
+ //#endregion
699
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/PresetSelect.module.css.mjs
700
+ const css$6 = ".xjnmxG_trigger{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:18px;display:flex}.xjnmxG_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.xjnmxG_triggerLabel{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.xjnmxG_trigger:after{border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);content:\"\";flex:none;width:6px;height:6px;margin-left:2px;transform:translateY(-1px)rotate(45deg)}.xjnmxG_row{flex-direction:column;gap:2px;min-width:0;display:flex}.xjnmxG_name{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.xjnmxG_description{color:var(--dsw-alias-label-tertiary);white-space:normal;font-size:12px;line-height:17px}";
701
+ const tagId$6 = "@sparkelf/dsh-group-chat/PresetSelect.module.css";
702
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
703
+ const tag = document.createElement("style");
704
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
705
+ tag.dataset.pluginCss = tagId$6;
706
+ tag.textContent = css$6;
707
+ document.head.appendChild(tag);
708
+ }
709
+ var PresetSelect_module_css_default = {
710
+ "description": "xjnmxG_description",
711
+ "triggerLabel": "xjnmxG_triggerLabel",
712
+ "row": "xjnmxG_row",
713
+ "name": "xjnmxG_name",
714
+ "trigger": "xjnmxG_trigger"
715
+ };
716
+ //#endregion
717
+ //#region src/client/PresetSelect.tsx
718
+ /**
719
+ * The agent-preset field, built on the platform's own menu.
720
+ *
721
+ * This is the same control the new-session screen shows beside the workspace picker: the
722
+ * platform paints that one with `Menu` and draws each row as a name over its description.
723
+ * Reproducing the choice as a text box — or as a differently-shaped list — would make the same
724
+ * concept look like two things depending on where the operator met it, so the dialog borrows
725
+ * the platform's materials instead:
726
+ *
727
+ * - `Menu` for the dropdown, so the keyboard walk, outside-click dismissal and the portaled
728
+ * list behave exactly as they do on the hero chip.
729
+ * - The platform's `settings.agentPreset` locale namespace for the shipped presets' names and
730
+ * descriptions. That copy is owned by the platform; reprinting it here would freeze today's
731
+ * wording and drift the moment the platform rewords it.
732
+ *
733
+ * What this deliberately does not do is import the platform's preset component: its props are
734
+ * bound to the `conversation.hero.agentPreset` slot and carry a session plus a staged-selection
735
+ * store this dialog has neither of. The roster comes from the Host instead, which reads the
736
+ * same registry that slot renders.
737
+ *
738
+ * @module @sparkelf/dsh-group-chat/client/PresetSelect
739
+ */
740
+ /**
741
+ * Render the preset trigger and its menu.
742
+ * @param props - roster, current choice, and the platform copy lookup.
743
+ * @returns the anchored dropdown.
744
+ */
745
+ const PresetSelect = (props) => {
746
+ const { presets, value, t, defaultLabel, onChange } = props;
747
+ const [open, setOpen] = (0, react.useState)(false);
748
+ const chosen = presets.find((preset) => preset.id === value);
749
+ const trigger = chosen === void 0 ? defaultLabel : presetDisplay(chosen, t).name;
750
+ const entryOf = (preset) => {
751
+ const { name, description } = presetDisplay(preset, t);
752
+ return {
753
+ id: preset.id,
754
+ disabled: preset.broken !== void 0,
755
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
756
+ className: PresetSelect_module_css_default["row"],
757
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
758
+ className: PresetSelect_module_css_default["name"],
759
+ children: name
760
+ }), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
761
+ className: PresetSelect_module_css_default["description"],
762
+ children: description
763
+ })]
764
+ })
765
+ };
766
+ };
767
+ const items = [...presets.filter((preset) => preset.name === void 0).map(entryOf), ...presets.filter((preset) => preset.name !== void 0).map(entryOf)];
768
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
769
+ open,
770
+ portal: true,
771
+ side: "bottom",
772
+ align: "start",
773
+ items,
774
+ selectedId: value.length === 0 ? void 0 : value,
775
+ onSelect: (id) => {
776
+ onChange(id);
777
+ setOpen(false);
778
+ },
779
+ onClose: () => {
780
+ setOpen(false);
781
+ },
782
+ footer: [{
783
+ id: "",
784
+ label: defaultLabel
785
+ }],
786
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
787
+ type: "button",
788
+ className: PresetSelect_module_css_default["trigger"],
789
+ "aria-haspopup": "menu",
790
+ "aria-expanded": open,
791
+ onClick: () => {
792
+ setOpen((current) => !current);
793
+ },
794
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
795
+ className: PresetSelect_module_css_default["triggerLabel"],
796
+ children: trigger
797
+ })
798
+ })
799
+ });
800
+ };
801
+ //#endregion
802
+ //#region src/client/store.ts
803
+ /**
804
+ * The room list both client surfaces read.
805
+ *
806
+ * It lives here rather than inside either component because the sidebar region and the
807
+ * room page must never show different lists. It is a plain observable, not a framework
808
+ * store: both consumers are components this plugin already owns.
809
+ *
810
+ * The slot caches the value its `inject` factory returns, so a snapshot taken at inject
811
+ * time would freeze at the first read. Consumers therefore subscribe through
812
+ * `useSyncExternalStore` and re-read on every change.
813
+ *
814
+ * @module @sparkelf/dsh-group-chat/client/store
815
+ */
816
+ /**
817
+ * Create the shared store.
818
+ * @returns a store whose `getState` returns a stable reference between changes, which is
819
+ * what `useSyncExternalStore` requires to avoid a render loop.
820
+ */
821
+ function createRoomStore() {
822
+ let state = {
823
+ rooms: void 0,
824
+ activeRoomId: void 0,
825
+ failure: void 0,
826
+ searchQuery: "",
827
+ selfId: void 0,
828
+ administrator: false,
829
+ members: {},
830
+ pendingCreate: false,
831
+ discoverable: void 0,
832
+ dialog: void 0
833
+ };
834
+ const listeners = /* @__PURE__ */ new Set();
835
+ const set = (patch) => {
836
+ state = {
837
+ ...state,
838
+ ...patch
839
+ };
840
+ for (const listener of listeners) listener();
841
+ };
842
+ /**
843
+ * Read one room's roster into the store.
844
+ *
845
+ * A failure leaves the previous roster in place rather than clearing it: the room is still
846
+ * readable without names (the renderer falls back to an unlabelled bubble), and a stale name is
847
+ * still the right person far more often than no name at all.
848
+ *
849
+ * @param roomId - the room whose roster to read.
850
+ */
851
+ const loadMembersInto = async (roomId) => {
852
+ try {
853
+ const roster = await listMembers(roomId);
854
+ set({ members: {
855
+ ...state.members,
856
+ [roomId]: roster
857
+ } });
858
+ } catch {}
859
+ };
860
+ return {
861
+ getState: () => state,
862
+ subscribe: (listener) => {
863
+ listeners.add(listener);
864
+ return () => {
865
+ listeners.delete(listener);
866
+ };
867
+ },
868
+ refresh: async () => {
869
+ readSelf().then((self) => {
870
+ if (self !== void 0) set({
871
+ selfId: self.id,
872
+ administrator: self.administrator
873
+ });
874
+ });
875
+ try {
876
+ const rooms = await listRooms();
877
+ set({
878
+ rooms,
879
+ failure: void 0
880
+ });
881
+ for (const room of rooms) {
882
+ if (!room.joined || state.members[room.id] !== void 0) continue;
883
+ loadMembersInto(room.id);
884
+ }
885
+ } catch (error) {
886
+ set({ failure: failureOf(error) });
887
+ }
888
+ },
889
+ open: (roomId) => {
890
+ set({ activeRoomId: roomId });
891
+ loadMembersInto(roomId);
892
+ },
893
+ discover: async (query) => {
894
+ try {
895
+ set({ discoverable: await listDiscoverableRooms(query) });
896
+ } catch (error) {
897
+ set({
898
+ discoverable: [],
899
+ failure: failureOf(error)
900
+ });
901
+ }
902
+ },
903
+ join: async (roomId) => {
904
+ await joinRoom(roomId);
905
+ set({
906
+ rooms: await listRooms(),
907
+ discoverable: await listDiscoverableRooms()
908
+ });
909
+ },
910
+ selfId: () => state.selfId,
911
+ administrator: () => state.administrator,
912
+ setFailure: (failure) => set({ failure }),
913
+ setSearch: (searchQuery) => set({ searchQuery }),
914
+ loadMembers: loadMembersInto,
915
+ setPolicy: async (roomId, patch) => {
916
+ await updateRoom(roomId, patch);
917
+ set({ rooms: await listRooms() });
918
+ },
919
+ openCreate: () => set({ pendingCreate: true }),
920
+ closeCreate: () => set({ pendingCreate: false }),
921
+ setDialog: (dialog) => set({ dialog }),
922
+ openMembers: void 0,
923
+ rename: async (roomId, name) => {
924
+ await updateRoom(roomId, { name });
925
+ set({ rooms: await listRooms() });
926
+ },
927
+ leave: async (roomId) => {
928
+ await leaveRoom(roomId);
929
+ set({
930
+ rooms: await listRooms(),
931
+ activeRoomId: void 0
932
+ });
933
+ },
934
+ removeMember: async (roomId, userId) => {
935
+ await removeMember(roomId, userId);
936
+ set({ rooms: await listRooms() });
937
+ },
938
+ dissolve: async (roomId) => {
939
+ await dissolveRoom(roomId);
940
+ set({
941
+ rooms: await listRooms(),
942
+ activeRoomId: void 0
943
+ });
944
+ },
945
+ transfer: async (roomId, userId) => {
946
+ await transferOwnership(roomId, userId);
947
+ set({ rooms: await listRooms() });
948
+ }
949
+ };
950
+ }
951
+ /** Map a transport failure onto the copy key the surfaces render. */
952
+ const failureOf = (error) => {
953
+ const message = error instanceof Error ? error.message : "";
954
+ if (message === "credentialMissing") return "credentialMissing";
955
+ if (message === "credentialRejected") return "credentialRejected";
956
+ if (message === "notConfigured") return "notConfigured";
957
+ if (message === "ownerCannotLeave") return "ownerCannotLeave";
958
+ return "unreachable";
959
+ };
960
+ //#endregion
961
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/CreateRoomDialog.module.css.mjs
962
+ const css$5 = ".Z3w8WW_body{flex-direction:column;gap:14px;min-width:0;display:flex}.Z3w8WW_field{flex-direction:column;gap:5px;min-width:0;display:flex}.Z3w8WW_label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.Z3w8WW_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}.Z3w8WW_warn{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-warning,#d299221f);color:var(--dsw-alias-state-warning-primary,#d29922);padding:6px 8px;font-size:12px;line-height:17px;display:block}.Z3w8WW_switches{border-top:1px solid var(--dsw-alias-border-l3);flex-direction:column;gap:8px;padding:10px 0 0;display:flex}.Z3w8WW_triggerExtra{flex-wrap:wrap;gap:6px;padding-left:0;display:flex}.Z3w8WW_pill,.Z3w8WW_pillActive{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:4px 10px;font-size:12px}.Z3w8WW_pillActive{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}.Z3w8WW_notice{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-2,#80808014);color:var(--dsw-alias-label-secondary);margin:0;padding:8px 10px;font-size:12px;line-height:17px}.Z3w8WW_error{color:var(--dsw-alias-state-error-primary,#f85149);margin:0;font-size:12px;line-height:17px}.Z3w8WW_footer{justify-content:flex-end;gap:8px;display:flex}.Z3w8WW_policyRow{flex-direction:column;gap:5px;display:flex}.Z3w8WW_memberList{flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;display:flex}.Z3w8WW_memberRow{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;padding:6px 8px;font-size:13px;display:flex}.Z3w8WW_memberRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.Z3w8WW_memberName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Z3w8WW_memberBadge{background:var(--dsw-alias-bg-layer-2,#8080801f);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:0 6px;font-size:11px;line-height:16px}";
963
+ const tagId$5 = "@sparkelf/dsh-group-chat/CreateRoomDialog.module.css";
964
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
965
+ const tag = document.createElement("style");
966
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
967
+ tag.dataset.pluginCss = tagId$5;
968
+ tag.textContent = css$5;
969
+ document.head.appendChild(tag);
970
+ }
971
+ var CreateRoomDialog_module_css_default = {
972
+ "label": "Z3w8WW_label",
973
+ "pillActive": "Z3w8WW_pillActive",
974
+ "notice": "Z3w8WW_notice",
975
+ "hint": "Z3w8WW_hint",
976
+ "policyRow": "Z3w8WW_policyRow",
977
+ "pill": "Z3w8WW_pill",
978
+ "field": "Z3w8WW_field",
979
+ "memberList": "Z3w8WW_memberList",
980
+ "memberName": "Z3w8WW_memberName",
981
+ "memberBadge": "Z3w8WW_memberBadge",
982
+ "warn": "Z3w8WW_warn",
983
+ "error": "Z3w8WW_error",
984
+ "triggerExtra": "Z3w8WW_triggerExtra",
985
+ "memberRow": "Z3w8WW_memberRow",
986
+ "body": "Z3w8WW_body",
987
+ "footer": "Z3w8WW_footer",
988
+ "switches": "Z3w8WW_switches"
989
+ };
990
+ //#endregion
991
+ //#region src/client/CreateRoomDialog.tsx
992
+ /**
993
+ * The create-room dialog.
994
+ *
995
+ * Three fields are required because each one decides something the room cannot change
996
+ * later — the name, the working directory, and the agent preset. `cwd` is the real
997
+ * security boundary: every member of the room can ask the agent to modify anything under
998
+ * it, so the field carries a visible warning rather than a hint.
999
+ *
1000
+ * The three policy switches are the room's settings, offered at creation so a room is not
1001
+ * born with defaults the creator would have chosen differently. The notice at the foot is
1002
+ * required wording: these settings control where a room appears, not who may reach it.
1003
+ *
1004
+ * @module @sparkelf/dsh-group-chat/client/CreateRoomDialog
1005
+ */
1006
+ /**
1007
+ * Render the create-room dialog.
1008
+ * @param props - the copy resolver and shared store whose `pendingCreate` opens this.
1009
+ * @returns the dialog, or null while closed.
1010
+ */
1011
+ function CreateRoomDialog(props) {
1012
+ const { t, tPreset, store } = props;
1013
+ const open = store.getState().pendingCreate;
1014
+ const [name, setName] = (0, react.useState)("");
1015
+ const [cwd, setCwd] = (0, react.useState)("");
1016
+ const [preset, setPreset] = (0, react.useState)("");
1017
+ const [invitePolicy, setInvitePolicy] = (0, react.useState)("anyone");
1018
+ const [discoverability, setDiscoverability] = (0, react.useState)("discoverable");
1019
+ const [agentTrigger, setAgentTrigger] = (0, react.useState)("mention");
1020
+ const [busy, setBusy] = (0, react.useState)(false);
1021
+ const [error, setError] = (0, react.useState)(void 0);
1022
+ const [presets, setPresets] = (0, react.useState)([]);
1023
+ const [directories, setDirectories] = (0, react.useState)([]);
1024
+ const [dirPath, setDirPath] = (0, react.useState)("");
1025
+ (0, react.useEffect)(() => {
1026
+ if (!open) return;
1027
+ let cancelled = false;
1028
+ (async () => {
1029
+ try {
1030
+ const options = await readOptions();
1031
+ if (!cancelled) setPresets(options.presets);
1032
+ } catch {
1033
+ if (!cancelled) setPresets([]);
1034
+ }
1035
+ })();
1036
+ return () => {
1037
+ cancelled = true;
1038
+ };
1039
+ }, [open]);
1040
+ (0, react.useEffect)(() => {
1041
+ if (!open) return;
1042
+ let cancelled = false;
1043
+ (async () => {
1044
+ try {
1045
+ const listing = await listDirectories(dirPath.length === 0 ? void 0 : dirPath);
1046
+ if (cancelled) return;
1047
+ const entries = listing.entries.map((entry) => ({
1048
+ id: entry.path,
1049
+ label: entry.name,
1050
+ secondary: entry.path
1051
+ }));
1052
+ setDirectories([
1053
+ {
1054
+ id: listing.path,
1055
+ label: listing.path,
1056
+ secondary: t("pickThisDirectory")
1057
+ },
1058
+ ...listing.parent === null ? [] : [{
1059
+ id: listing.parent,
1060
+ label: "..",
1061
+ secondary: listing.parent
1062
+ }],
1063
+ ...entries
1064
+ ]);
1065
+ } catch {
1066
+ if (!cancelled) setDirectories([]);
1067
+ }
1068
+ })();
1069
+ return () => {
1070
+ cancelled = true;
1071
+ };
1072
+ }, [
1073
+ open,
1074
+ dirPath,
1075
+ t
1076
+ ]);
1077
+ (0, react.useEffect)(() => {
1078
+ if (!open) {
1079
+ setName("");
1080
+ setBusy(false);
1081
+ setError(void 0);
1082
+ }
1083
+ }, [open]);
1084
+ const close = () => store.closeCreate();
1085
+ const submit = async () => {
1086
+ if (name.trim().length === 0 || busy) return;
1087
+ setBusy(true);
1088
+ try {
1089
+ const room = await createRoom(name.trim(), {
1090
+ ...cwd.trim().length === 0 ? {} : { cwd: cwd.trim() },
1091
+ ...preset.trim().length === 0 ? {} : { agentPreset: preset.trim() }
1092
+ });
1093
+ await Promise.all([
1094
+ invitePolicy === "anyone" ? Promise.resolve() : store.setPolicy(room.id, { invitePolicy }),
1095
+ discoverability === "discoverable" ? Promise.resolve() : store.setPolicy(room.id, { discoverability }),
1096
+ agentTrigger === "mention" ? Promise.resolve() : store.setPolicy(room.id, { agentTrigger })
1097
+ ]);
1098
+ await store.refresh();
1099
+ store.open(room.id);
1100
+ close();
1101
+ } catch (cause) {
1102
+ setError(failureOf(cause));
1103
+ } finally {
1104
+ setBusy(false);
1105
+ }
1106
+ };
1107
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1108
+ open,
1109
+ onClose: close,
1110
+ title: t("createTitle"),
1111
+ closeLabel: t("cancel"),
1112
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
+ className: CreateRoomDialog_module_css_default.footer,
1114
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1115
+ variant: "ghost",
1116
+ size: "md",
1117
+ onClick: close,
1118
+ children: t("cancel")
1119
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1120
+ variant: "primary",
1121
+ size: "md",
1122
+ disabled: busy || name.trim().length === 0,
1123
+ onClick: () => void submit(),
1124
+ children: t("createSubmit")
1125
+ })]
1126
+ }),
1127
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1128
+ className: CreateRoomDialog_module_css_default.body,
1129
+ children: [
1130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1131
+ className: CreateRoomDialog_module_css_default.field,
1132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1133
+ className: CreateRoomDialog_module_css_default.label,
1134
+ children: t("createName")
1135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1136
+ value: name,
1137
+ autoFocus: true,
1138
+ onChange: (event) => setName(event.target.value),
1139
+ onKeyDown: (event) => {
1140
+ if (event.key === "Enter") submit();
1141
+ }
1142
+ })]
1143
+ }),
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1145
+ className: CreateRoomDialog_module_css_default.field,
1146
+ children: [
1147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1148
+ className: CreateRoomDialog_module_css_default.label,
1149
+ children: t("createCwd")
1150
+ }),
1151
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
1152
+ value: cwd,
1153
+ options: directories,
1154
+ placeholder: "/workspace",
1155
+ emptyHint: t("optionEmpty"),
1156
+ onQueryChange: (next) => {
1157
+ setCwd(next);
1158
+ },
1159
+ onTypedCommit: (text) => {
1160
+ setCwd(text);
1161
+ setDirPath(text);
1162
+ },
1163
+ typedCommitLabel: (text) => text,
1164
+ onPick: (option) => {
1165
+ setCwd(option.id);
1166
+ setDirPath(option.id);
1167
+ }
1168
+ }),
1169
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1170
+ className: CreateRoomDialog_module_css_default.warn,
1171
+ children: t("createCwdHint")
1172
+ })
1173
+ ]
1174
+ }),
1175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1176
+ className: CreateRoomDialog_module_css_default.field,
1177
+ children: [
1178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1179
+ className: CreateRoomDialog_module_css_default.label,
1180
+ children: t("createPreset")
1181
+ }),
1182
+ presets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1183
+ className: CreateRoomDialog_module_css_default.hint,
1184
+ children: t("createPresetUnavailable")
1185
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PresetSelect, {
1186
+ presets,
1187
+ value: preset,
1188
+ t: tPreset,
1189
+ defaultLabel: t("createPresetDefault"),
1190
+ onChange: setPreset
1191
+ }),
1192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1193
+ className: CreateRoomDialog_module_css_default.hint,
1194
+ children: t("createPresetHint")
1195
+ })
1196
+ ]
1197
+ }),
1198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1199
+ className: CreateRoomDialog_module_css_default.switches,
1200
+ children: [
1201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1202
+ checked: invitePolicy === "anyone",
1203
+ onChange: (next) => setInvitePolicy(next ? "anyone" : "admin"),
1204
+ label: t("createPolicy")
1205
+ }),
1206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1207
+ checked: discoverability === "discoverable",
1208
+ onChange: (next) => setDiscoverability(next ? "discoverable" : "invite_only"),
1209
+ label: t("createVisibility")
1210
+ }),
1211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1212
+ checked: agentTrigger === "mention",
1213
+ onChange: (next) => setAgentTrigger(next ? "mention" : "always"),
1214
+ label: t("createTrigger")
1215
+ }),
1216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1217
+ className: CreateRoomDialog_module_css_default.triggerExtra,
1218
+ children: [
1219
+ "mention",
1220
+ "always",
1221
+ "never"
1222
+ ].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1223
+ type: "button",
1224
+ className: agentTrigger === mode ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
1225
+ onClick: () => setAgentTrigger(mode),
1226
+ children: mode === "mention" ? t("triggerMention") : mode === "always" ? t("triggerAlways") : t("triggerNever")
1227
+ }, mode))
1228
+ })
1229
+ ]
1230
+ }),
1231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1232
+ className: CreateRoomDialog_module_css_default.notice,
1233
+ children: t("createNotice")
1234
+ }),
1235
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1236
+ className: CreateRoomDialog_module_css_default.error,
1237
+ children: t(error)
1238
+ })
1239
+ ]
1240
+ })
1241
+ });
1242
+ }
1243
+ //#endregion
1244
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/InviteDialog.module.css.mjs
1245
+ const css$4 = ".r4uCba_dialog.r4uCba_dialog{width:min(760px,100%);max-height:100%}.r4uCba_content{overscroll-behavior:contain;min-height:0;overflow-y:auto}.r4uCba_searchRow{align-items:center;gap:8px;min-width:0;display:flex;position:relative}.r4uCba_search{flex:1;min-width:0}.r4uCba_spinner{color:var(--dsw-alias-label-tertiary);flex:none;animation:.9s linear infinite r4uCba_inviteSpin}@keyframes r4uCba_inviteSpin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.r4uCba_spinner{animation:none}}.r4uCba_visuallyHidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.r4uCba_field{flex-direction:column;gap:5px;min-width:0;display:flex}.r4uCba_chips{flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none;display:flex}.r4uCba_chip{border:1px solid var(--dsw-alias-border-l3);border-radius:999px;align-items:center;gap:6px;max-width:100%;padding:3px 6px 3px 3px;font-size:12px;line-height:18px;display:inline-flex}.r4uCba_chipName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.r4uCba_chipRemove{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;flex:none;padding:0 2px;font-size:14px;line-height:14px}.r4uCba_chipRemove:hover{color:var(--dsw-alias-label-primary)}.r4uCba_chipRemove:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px;border-radius:var(--dsw-radius-sm)}.r4uCba_list{scrollbar-gutter:stable;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);max-height:min(46vh,420px);margin:0;padding:4px;list-style:none;overflow-y:auto}.r4uCba_list>li+li{margin-top:2px}.r4uCba_row,.r4uCba_rowSelected{box-sizing:border-box;border-radius:var(--dsw-radius-sm);width:100%;min-height:38px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;align-items:center;gap:8px;padding:5px 8px;font-size:13px;line-height:18px;display:flex}.r4uCba_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.r4uCba_rowSelected{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-active)}.r4uCba_rowSelected:hover{background:var(--dsw-alias-interactive-bg-hover-accent)}.r4uCba_row:focus-visible,.r4uCba_rowSelected:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}.r4uCba_rowName{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.r4uCba_rowHandle{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px}.r4uCba_check,.r4uCba_checkHidden{width:14px;color:var(--dsw-alias-state-business-primary);flex:none;justify-content:center;align-items:center;display:inline-flex}.r4uCba_checkHidden{visibility:hidden}.r4uCba_note{color:var(--dsw-alias-label-tertiary);margin:0;padding:14px 10px;font-size:12px;line-height:17px}.r4uCba_errorRow{align-items:center;gap:8px;min-width:0;display:flex}.r4uCba_errorRow>span{flex:1;min-width:0}.r4uCba_error{color:var(--dsw-alias-state-error-primary,#f85149);margin:0;font-size:12px;line-height:17px}";
1246
+ const tagId$4 = "@sparkelf/dsh-group-chat/InviteDialog.module.css";
1247
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
1248
+ const tag = document.createElement("style");
1249
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
1250
+ tag.dataset.pluginCss = tagId$4;
1251
+ tag.textContent = css$4;
1252
+ document.head.appendChild(tag);
1253
+ }
1254
+ var InviteDialog_module_css_default = {
1255
+ "inviteSpin": "r4uCba_inviteSpin",
1256
+ "rowName": "r4uCba_rowName",
1257
+ "search": "r4uCba_search",
1258
+ "rowSelected": "r4uCba_rowSelected",
1259
+ "note": "r4uCba_note",
1260
+ "chipName": "r4uCba_chipName",
1261
+ "spinner": "r4uCba_spinner",
1262
+ "errorRow": "r4uCba_errorRow",
1263
+ "rowHandle": "r4uCba_rowHandle",
1264
+ "error": "r4uCba_error",
1265
+ "dialog": "r4uCba_dialog",
1266
+ "check": "r4uCba_check",
1267
+ "chip": "r4uCba_chip",
1268
+ "checkHidden": "r4uCba_checkHidden",
1269
+ "chipRemove": "r4uCba_chipRemove",
1270
+ "row": "r4uCba_row",
1271
+ "list": "r4uCba_list",
1272
+ "visuallyHidden": "r4uCba_visuallyHidden",
1273
+ "content": "r4uCba_content",
1274
+ "searchRow": "r4uCba_searchRow",
1275
+ "field": "r4uCba_field",
1276
+ "chips": "r4uCba_chips"
1277
+ };
1278
+ //#endregion
1279
+ //#region src/client/RoomDialogs.tsx
1280
+ /**
1281
+ * The three per-room dialogs: invite, members, and settings.
1282
+ *
1283
+ * They share one module because they are one interaction family — each is opened from a
1284
+ * room row's context menu, each operates on exactly one room, and each writes through the
1285
+ * same store. Splitting them would triple the plumbing without separating anything.
1286
+ *
1287
+ * The settings dialog carries the required notice: its three switches change where a room
1288
+ * appears, not who may reach it. The server enforces permissions; these are preferences.
1289
+ *
1290
+ * @module @sparkelf/dsh-group-chat/client/RoomDialogs
1291
+ */
1292
+ /**
1293
+ * Render whichever room dialog is open.
1294
+ * @param props - the copy resolver and the store holding the open dialog.
1295
+ * @returns the dialog, or null when none is open.
1296
+ */
1297
+ function RoomDialogs(props) {
1298
+ const { t, store } = props;
1299
+ const dialog = store.getState().dialog;
1300
+ if (dialog === void 0) return null;
1301
+ const close = () => store.setDialog(void 0);
1302
+ if (dialog.kind === "invite") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InviteDialog, {
1303
+ t,
1304
+ store,
1305
+ roomId: dialog.roomId,
1306
+ onClose: close
1307
+ });
1308
+ if (dialog.kind === "settings") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsDialog, {
1309
+ t,
1310
+ store,
1311
+ roomId: dialog.roomId,
1312
+ onClose: close
1313
+ });
1314
+ if (dialog.kind === "transfer") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferDialog, {
1315
+ t,
1316
+ store,
1317
+ roomId: dialog.roomId,
1318
+ onClose: close
1319
+ });
1320
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MembersDialog, {
1321
+ t,
1322
+ roomId: dialog.roomId,
1323
+ onClose: close
1324
+ });
1325
+ }
1326
+ /**
1327
+ * Add existing DataOps accounts to a room.
1328
+ *
1329
+ * The list is multi-select rather than one-pick-at-a-time: a room is usually started with
1330
+ * several colleagues, and a dialog that closed after each person made that a repeated
1331
+ * errand. Rows are native buttons with `aria-pressed`, so Tab, Enter and Space all work and
1332
+ * a screen reader hears the selected state; ↑/↓ move between rows without leaving the list.
1333
+ *
1334
+ * What is selected lives in `chosen` and not in the search result rows. A query can hide an
1335
+ * already-picked person — the server search returns only what matches — and keeping the two
1336
+ * separate is what stops the selection from silently emptying as the operator narrows the
1337
+ * list.
1338
+ */
1339
+ function InviteDialog(props) {
1340
+ const { t, store, roomId, onClose } = props;
1341
+ const [candidates, setCandidates] = (0, react.useState)([]);
1342
+ const [query, setQuery] = (0, react.useState)("");
1343
+ const [chosen, setChosen] = (0, react.useState)([]);
1344
+ const [searching, setSearching] = (0, react.useState)(true);
1345
+ const [busy, setBusy] = (0, react.useState)(false);
1346
+ const [searchError, setSearchError] = (0, react.useState)(void 0);
1347
+ const [submitError, setSubmitError] = (0, react.useState)(void 0);
1348
+ const [done, setDone] = (0, react.useState)(false);
1349
+ const [attempt, setAttempt] = (0, react.useState)(0);
1350
+ const listRef = (0, react.useRef)(null);
1351
+ (0, react.useEffect)(() => {
1352
+ let cancelled = false;
1353
+ setSearching(true);
1354
+ const timer = setTimeout(() => {
1355
+ (async () => {
1356
+ try {
1357
+ const next = await searchInviteCandidates(roomId, query);
1358
+ if (!cancelled) {
1359
+ setCandidates(next);
1360
+ setSearchError(void 0);
1361
+ }
1362
+ } catch (cause) {
1363
+ if (!cancelled) setSearchError(failureOf(cause));
1364
+ } finally {
1365
+ if (!cancelled) setSearching(false);
1366
+ }
1367
+ })();
1368
+ }, 220);
1369
+ return () => {
1370
+ cancelled = true;
1371
+ clearTimeout(timer);
1372
+ };
1373
+ }, [
1374
+ roomId,
1375
+ query,
1376
+ attempt
1377
+ ]);
1378
+ const toggle = (candidate) => {
1379
+ setDone(false);
1380
+ setSubmitError(void 0);
1381
+ setChosen((current) => toggleChosen(current, candidate));
1382
+ };
1383
+ const submit = async () => {
1384
+ if (chosen.length === 0 || busy) return;
1385
+ setBusy(true);
1386
+ setSubmitError(void 0);
1387
+ try {
1388
+ await inviteMembers(roomId, chosen.map((member) => member.userId));
1389
+ await store.refresh();
1390
+ setChosen([]);
1391
+ setDone(true);
1392
+ if (query.length === 0) setAttempt((current) => current + 1);
1393
+ else setQuery("");
1394
+ } catch (cause) {
1395
+ setSubmitError(failureOf(cause));
1396
+ } finally {
1397
+ setBusy(false);
1398
+ }
1399
+ };
1400
+ const onListKeyDown = (event) => {
1401
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1402
+ const rows = [...listRef.current?.querySelectorAll("button[data-candidate]") ?? []];
1403
+ if (rows.length === 0) return;
1404
+ event.preventDefault();
1405
+ const index = rows.indexOf(document.activeElement);
1406
+ const step = event.key === "ArrowDown" ? 1 : -1;
1407
+ rows[index < 0 ? 0 : (index + step + rows.length) % rows.length]?.focus();
1408
+ };
1409
+ const moveToList = () => {
1410
+ listRef.current?.querySelector("button[data-candidate]")?.focus();
1411
+ };
1412
+ const visible = filterCandidates(candidates, query);
1413
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1414
+ open: true,
1415
+ onClose,
1416
+ title: t("inviteTitle"),
1417
+ closeLabel: t("cancel"),
1418
+ className: InviteDialog_module_css_default.dialog,
1419
+ contentClassName: InviteDialog_module_css_default.content,
1420
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1421
+ className: CreateRoomDialog_module_css_default.footer,
1422
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1423
+ variant: "ghost",
1424
+ size: "md",
1425
+ onClick: onClose,
1426
+ children: t("cancel")
1427
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1428
+ variant: "primary",
1429
+ size: "md",
1430
+ disabled: busy || chosen.length === 0,
1431
+ onClick: () => void submit(),
1432
+ children: chosen.length === 0 ? t("inviteSubmit") : t("inviteSubmitCount").replace("{count}", String(chosen.length))
1433
+ })]
1434
+ }),
1435
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1436
+ className: CreateRoomDialog_module_css_default.body,
1437
+ children: [
1438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1439
+ className: InviteDialog_module_css_default.field,
1440
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1441
+ className: InviteDialog_module_css_default.searchRow,
1442
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1443
+ className: InviteDialog_module_css_default.search,
1444
+ value: query,
1445
+ "data-modal-autofocus": true,
1446
+ placeholder: t("invitePlaceholder"),
1447
+ "aria-label": t("inviteSearchLabel"),
1448
+ onChange: (event) => {
1449
+ setQuery(event.target.value);
1450
+ },
1451
+ onKeyDown: (event) => {
1452
+ if (event.key !== "ArrowDown") return;
1453
+ event.preventDefault();
1454
+ moveToList();
1455
+ }
1456
+ }), searching && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutlineRegular, {
1457
+ className: InviteDialog_module_css_default.spinner,
1458
+ size: 14
1459
+ })]
1460
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1461
+ className: CreateRoomDialog_module_css_default.hint,
1462
+ children: t("inviteHint")
1463
+ })]
1464
+ }),
1465
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1466
+ className: InviteDialog_module_css_default.visuallyHidden,
1467
+ role: "status",
1468
+ children: chosen.length === 0 ? "" : t("inviteSelectedCount").replace("{count}", String(chosen.length))
1469
+ }),
1470
+ chosen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1471
+ className: CreateRoomDialog_module_css_default.field,
1472
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1473
+ className: CreateRoomDialog_module_css_default.label,
1474
+ children: t("inviteSelectedCount").replace("{count}", String(chosen.length))
1475
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1476
+ className: InviteDialog_module_css_default.chips,
1477
+ children: chosen.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1478
+ className: InviteDialog_module_css_default.chip,
1479
+ children: [
1480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1481
+ "aria-hidden": "true",
1482
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1483
+ name: member.displayName || member.username,
1484
+ src: member.avatarUrl,
1485
+ size: "sm"
1486
+ })
1487
+ }),
1488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1489
+ className: InviteDialog_module_css_default.chipName,
1490
+ children: member.displayName || member.username
1491
+ }),
1492
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1493
+ type: "button",
1494
+ className: InviteDialog_module_css_default.chipRemove,
1495
+ "aria-label": `${t("inviteRemove")} ${member.displayName || member.username}`,
1496
+ onClick: () => {
1497
+ setChosen((current) => current.filter((row) => row.userId !== member.userId));
1498
+ },
1499
+ children: "×"
1500
+ })
1501
+ ]
1502
+ }, member.userId))
1503
+ })]
1504
+ }),
1505
+ searchError !== void 0 && !searching ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1506
+ className: InviteDialog_module_css_default.errorRow,
1507
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1508
+ className: InviteDialog_module_css_default.error,
1509
+ children: t(searchError)
1510
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1511
+ size: "sm",
1512
+ variant: "ghost",
1513
+ onClick: () => {
1514
+ setAttempt((current) => current + 1);
1515
+ },
1516
+ children: t("retry")
1517
+ })]
1518
+ }) : searching && visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1519
+ className: InviteDialog_module_css_default.note,
1520
+ children: t("inviteSearching")
1521
+ }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1522
+ className: InviteDialog_module_css_default.note,
1523
+ children: t("inviteEmpty")
1524
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1525
+ ref: listRef,
1526
+ className: InviteDialog_module_css_default.list,
1527
+ onKeyDown: onListKeyDown,
1528
+ children: visible.map((candidate) => {
1529
+ const selected = isChosen(chosen, candidate.userId);
1530
+ const name = candidate.displayName.length > 0 ? candidate.displayName : candidate.username;
1531
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1532
+ type: "button",
1533
+ "data-candidate": true,
1534
+ className: selected ? InviteDialog_module_css_default.rowSelected : InviteDialog_module_css_default.row,
1535
+ "aria-pressed": selected,
1536
+ onClick: () => {
1537
+ toggle(candidate);
1538
+ },
1539
+ children: [
1540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1541
+ "aria-hidden": "true",
1542
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1543
+ name,
1544
+ src: candidate.avatarUrl,
1545
+ size: "sm"
1546
+ })
1547
+ }),
1548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1549
+ className: InviteDialog_module_css_default.rowName,
1550
+ children: name
1551
+ }),
1552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1553
+ className: InviteDialog_module_css_default.rowHandle,
1554
+ children: candidate.username
1555
+ }),
1556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1557
+ className: selected ? InviteDialog_module_css_default.check : InviteDialog_module_css_default.checkHidden,
1558
+ "aria-hidden": "true",
1559
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 })
1560
+ })
1561
+ ]
1562
+ }) }, candidate.userId);
1563
+ })
1564
+ }),
1565
+ done && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1566
+ className: CreateRoomDialog_module_css_default.hint,
1567
+ children: t("saved")
1568
+ }),
1569
+ submitError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1570
+ className: InviteDialog_module_css_default.error,
1571
+ children: t(submitError)
1572
+ })
1573
+ ]
1574
+ })
1575
+ });
1576
+ }
1577
+ /** List a room's members. */
1578
+ function MembersDialog(props) {
1579
+ const { t, roomId, onClose } = props;
1580
+ const [members, setMembers] = (0, react.useState)(void 0);
1581
+ const [error, setError] = (0, react.useState)(void 0);
1582
+ (0, react.useEffect)(() => {
1583
+ let cancelled = false;
1584
+ (async () => {
1585
+ try {
1586
+ const next = await listMembers(roomId);
1587
+ if (!cancelled) setMembers(next);
1588
+ } catch (cause) {
1589
+ if (!cancelled) setError(failureOf(cause));
1590
+ }
1591
+ })();
1592
+ return () => {
1593
+ cancelled = true;
1594
+ };
1595
+ }, [roomId]);
1596
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1597
+ open: true,
1598
+ onClose,
1599
+ title: t("membersTitle"),
1600
+ closeLabel: t("cancel"),
1601
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1602
+ className: CreateRoomDialog_module_css_default.footer,
1603
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1604
+ variant: "ghost",
1605
+ size: "md",
1606
+ onClick: onClose,
1607
+ children: t("cancel")
1608
+ })
1609
+ }),
1610
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1611
+ className: CreateRoomDialog_module_css_default.body,
1612
+ children: [
1613
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1614
+ className: CreateRoomDialog_module_css_default.error,
1615
+ children: t(error)
1616
+ }),
1617
+ error === void 0 && members === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1618
+ className: CreateRoomDialog_module_css_default.hint,
1619
+ children: t("loading")
1620
+ }),
1621
+ members !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1622
+ className: CreateRoomDialog_module_css_default.memberList,
1623
+ children: members.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1624
+ className: CreateRoomDialog_module_css_default.memberRow,
1625
+ children: [
1626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1627
+ name: member.displayName,
1628
+ src: member.avatarUrl
1629
+ }),
1630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1631
+ className: CreateRoomDialog_module_css_default.memberName,
1632
+ children: member.displayName
1633
+ }),
1634
+ member.owner && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1635
+ className: CreateRoomDialog_module_css_default.memberBadge,
1636
+ children: t("ownerBadge")
1637
+ })
1638
+ ]
1639
+ }, member.userId))
1640
+ })
1641
+ ]
1642
+ })
1643
+ });
1644
+ }
1645
+ /** The room's three policy dimensions. */
1646
+ /**
1647
+ * Hand ownership of this room to another member.
1648
+ *
1649
+ * Exists because an owner cannot leave: a room without an owner has nobody who may change its
1650
+ * settings, so the server refuses the leave. Transferring is that way out, and this dialog is
1651
+ * where the owner names the successor.
1652
+ *
1653
+ * Only current members are offered, and the owner is excluded — the server rejects both a
1654
+ * non-member and a self-transfer, so listing them would only invite a refusal.
1655
+ */
1656
+ function TransferDialog(props) {
1657
+ const { t, store, roomId, onClose } = props;
1658
+ const room = store.getState().rooms?.find((candidate) => candidate.id === roomId);
1659
+ const [members, setMembers] = (0, react.useState)(void 0);
1660
+ const [chosen, setChosen] = (0, react.useState)(void 0);
1661
+ const [busy, setBusy] = (0, react.useState)(false);
1662
+ const [failure, setFailure] = (0, react.useState)(void 0);
1663
+ (0, react.useEffect)(() => {
1664
+ let cancelled = false;
1665
+ listMembers(roomId).then((rows) => {
1666
+ if (!cancelled) setMembers(rows);
1667
+ }).catch(() => {
1668
+ if (!cancelled) setMembers([]);
1669
+ });
1670
+ return () => {
1671
+ cancelled = true;
1672
+ };
1673
+ }, [roomId]);
1674
+ const candidates = (members ?? []).filter((member) => member.userId !== store.selfId());
1675
+ const submit = async () => {
1676
+ if (busy || chosen === void 0) return;
1677
+ setBusy(true);
1678
+ setFailure(void 0);
1679
+ try {
1680
+ await store.transfer(roomId, chosen);
1681
+ onClose();
1682
+ } catch (cause) {
1683
+ setFailure(failureOf(cause));
1684
+ } finally {
1685
+ setBusy(false);
1686
+ }
1687
+ };
1688
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1689
+ open: true,
1690
+ onClose: busy ? () => {} : onClose,
1691
+ title: t("menuTransferTitle"),
1692
+ closeLabel: t("cancel"),
1693
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1694
+ className: CreateRoomDialog_module_css_default.footer,
1695
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1696
+ variant: "ghost",
1697
+ size: "md",
1698
+ disabled: busy,
1699
+ onClick: onClose,
1700
+ children: t("cancel")
1701
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1702
+ variant: "primary",
1703
+ size: "md",
1704
+ disabled: busy || chosen === void 0,
1705
+ onClick: () => void submit(),
1706
+ children: t("menuTransferConfirm")
1707
+ })]
1708
+ }),
1709
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1710
+ className: InviteDialog_module_css_default.content,
1711
+ children: [
1712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1713
+ className: InviteDialog_module_css_default.note,
1714
+ children: t("menuTransferHint")
1715
+ }),
1716
+ room !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1717
+ className: InviteDialog_module_css_default.note,
1718
+ children: room.name
1719
+ }),
1720
+ members === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1721
+ className: InviteDialog_module_css_default.note,
1722
+ children: "…"
1723
+ }),
1724
+ members !== void 0 && candidates.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1725
+ className: InviteDialog_module_css_default.note,
1726
+ children: t("menuTransferEmpty")
1727
+ }),
1728
+ candidates.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1729
+ className: InviteDialog_module_css_default.list,
1730
+ role: "listbox",
1731
+ "aria-label": t("menuTransferTitle"),
1732
+ children: candidates.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1733
+ type: "button",
1734
+ role: "option",
1735
+ "aria-selected": chosen === member.userId,
1736
+ className: chosen === member.userId ? InviteDialog_module_css_default.rowSelected : InviteDialog_module_css_default.row,
1737
+ onClick: () => setChosen(member.userId),
1738
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1739
+ size: "sm",
1740
+ name: member.displayName,
1741
+ src: member.avatarUrl ?? void 0
1742
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1743
+ className: InviteDialog_module_css_default.rowName,
1744
+ children: member.displayName
1745
+ })]
1746
+ }) }, member.userId))
1747
+ }),
1748
+ failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1749
+ className: InviteDialog_module_css_default.error,
1750
+ role: "alert",
1751
+ children: t(failure)
1752
+ })
1753
+ ]
1754
+ })
1755
+ });
1756
+ }
1757
+ function SettingsDialog(props) {
1758
+ const { t, store, roomId, onClose } = props;
1759
+ const room = store.getState().rooms?.find((candidate) => candidate.id === roomId);
1760
+ const [invitePolicy, setInvitePolicy] = (0, react.useState)(room?.invitePolicy ?? "anyone");
1761
+ const [discoverability, setDiscoverability] = (0, react.useState)(room?.discoverability ?? "discoverable");
1762
+ const [agentTrigger, setAgentTrigger] = (0, react.useState)(room?.agentTrigger ?? "mention");
1763
+ const [busy, setBusy] = (0, react.useState)(false);
1764
+ const [error, setError] = (0, react.useState)(void 0);
1765
+ const save = async () => {
1766
+ setBusy(true);
1767
+ try {
1768
+ await updateRoom(roomId, {
1769
+ invitePolicy,
1770
+ discoverability,
1771
+ agentTrigger
1772
+ });
1773
+ await store.refresh();
1774
+ onClose();
1775
+ } catch (cause) {
1776
+ setError(failureOf(cause));
1777
+ } finally {
1778
+ setBusy(false);
1779
+ }
1780
+ };
1781
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1782
+ open: true,
1783
+ onClose,
1784
+ title: t("settingsTitle"),
1785
+ closeLabel: t("cancel"),
1786
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1787
+ className: CreateRoomDialog_module_css_default.footer,
1788
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1789
+ variant: "ghost",
1790
+ size: "md",
1791
+ onClick: onClose,
1792
+ children: t("cancel")
1793
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1794
+ variant: "primary",
1795
+ size: "md",
1796
+ disabled: busy,
1797
+ onClick: () => void save(),
1798
+ children: t("save")
1799
+ })]
1800
+ }),
1801
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1802
+ className: CreateRoomDialog_module_css_default.body,
1803
+ children: [
1804
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1805
+ className: CreateRoomDialog_module_css_default.switches,
1806
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicyRow, {
1807
+ t,
1808
+ label: t("createPolicy"),
1809
+ value: invitePolicy === "anyone",
1810
+ onChange: (next) => setInvitePolicy(next ? "anyone" : "admin"),
1811
+ onLabel: t("policyAnyone"),
1812
+ offLabel: t("policyAdmin")
1813
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicyRow, {
1814
+ t,
1815
+ label: t("createVisibility"),
1816
+ value: discoverability === "discoverable",
1817
+ onChange: (next) => setDiscoverability(next ? "discoverable" : "invite_only"),
1818
+ onLabel: t("discoverable"),
1819
+ offLabel: t("inviteOnly")
1820
+ })]
1821
+ }),
1822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1823
+ className: CreateRoomDialog_module_css_default.switches,
1824
+ style: {
1825
+ borderTop: "none",
1826
+ paddingTop: 0
1827
+ },
1828
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1829
+ className: CreateRoomDialog_module_css_default.label,
1830
+ children: t("createTrigger")
1831
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1832
+ className: CreateRoomDialog_module_css_default.triggerExtra,
1833
+ children: [
1834
+ "mention",
1835
+ "always",
1836
+ "never"
1837
+ ].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1838
+ type: "button",
1839
+ className: agentTrigger === mode ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
1840
+ onClick: () => setAgentTrigger(mode),
1841
+ children: mode === "mention" ? t("triggerMention") : mode === "always" ? t("triggerAlways") : t("triggerNever")
1842
+ }, mode))
1843
+ })]
1844
+ }),
1845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1846
+ className: CreateRoomDialog_module_css_default.notice,
1847
+ children: t("createNotice")
1848
+ }),
1849
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1850
+ className: CreateRoomDialog_module_css_default.error,
1851
+ children: t(error)
1852
+ })
1853
+ ]
1854
+ })
1855
+ });
1856
+ }
1857
+ /** One two-state policy row, labelled with what each side means. */
1858
+ function PolicyRow(props) {
1859
+ const { label, value, onChange, onLabel, offLabel } = props;
1860
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1861
+ className: CreateRoomDialog_module_css_default.policyRow,
1862
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1863
+ className: CreateRoomDialog_module_css_default.label,
1864
+ children: label
1865
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1866
+ className: CreateRoomDialog_module_css_default.triggerExtra,
1867
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1868
+ type: "button",
1869
+ className: value ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
1870
+ onClick: () => onChange(true),
1871
+ children: onLabel
1872
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1873
+ type: "button",
1874
+ className: !value ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
1875
+ onClick: () => onChange(false),
1876
+ children: offLabel
1877
+ })]
1878
+ })]
1879
+ });
1880
+ }
1881
+ //#endregion
1882
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/SidebarRooms.module.css.mjs
1883
+ const css$3 = ".s0QgKq_root{--dsh-session-list-edge-inset:var(--dsh-sidebar-inline-padding,12px);--dsh-session-list-scrollbar-width:5px;--dsh-session-list-scrollbar-offset:2px;box-sizing:border-box;flex-direction:column;flex:auto;width:100%;min-width:0;min-height:0;max-height:20vh;display:flex;position:relative}.s0QgKq_resizer{z-index:1;cursor:row-resize;touch-action:none;background:0 0;height:6px;position:absolute;top:-3px;left:0;right:0}.s0QgKq_resizer:after{content:\"\";background:var(--dsw-alias-border-l1,var(--dsw-alias-separator-primary));opacity:0;border-radius:1px;height:1px;transition:opacity .12s;position:absolute;top:3px;left:8px;right:8px}.s0QgKq_root:hover .s0QgKq_resizer:after,.s0QgKq_resizer:focus-visible:after{opacity:1}.s0QgKq_resizer:focus-visible{outline:none}.s0QgKq_resizer:focus-visible:after{background:var(--dsw-alias-brand-primary,var(--dsw-alias-label-primary));height:2px}.s0QgKq_sectionHeader{box-sizing:border-box;border-radius:var(--dsw-radius-md);height:36px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;margin-right:-4px;padding-left:4px;display:flex;overflow:hidden}.s0QgKq_sectionLabel{white-space:nowrap;flex:none;min-width:0;max-width:45%;line-height:20px;overflow:hidden}.s0QgKq_searchSlot{box-sizing:border-box;flex:1;align-items:center;min-width:0;max-width:28px;margin-left:auto;padding-left:0;display:flex}.s0QgKq_searchSlotExpanded{box-sizing:border-box;flex:1;align-items:center;min-width:0;max-width:100%;margin-left:auto;display:flex}.s0QgKq_search,.s0QgKq_searchExpanded{box-sizing:border-box;border-radius:var(--dsw-radius-sm);width:100%;height:28px;color:var(--dsw-alias-label-secondary);cursor:text;background:0 0;border:none;flex:none;align-items:center;margin:0;padding:0;display:flex;overflow:hidden}.s0QgKq_searchExpanded{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);width:calc(100% + 4px);height:30px;color:var(--dsw-alias-label-caption);margin-inline:-2px;padding:0 4px 0 0}.s0QgKq_iconButton{border-radius:var(--dsw-radius-sm);width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.s0QgKq_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_iconButton:focus-visible,.s0QgKq_searchButton:focus-visible,.s0QgKq_clearButton:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}.s0QgKq_searchButton{border-radius:var(--dsw-radius-sm);width:28px;height:28px;color:inherit;cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.s0QgKq_searchExpanded .s0QgKq_searchButton{width:28px;height:30px}.s0QgKq_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_searchExpanded .s0QgKq_searchButton:hover{background:0 0}.s0QgKq_searchInput{width:0;min-width:0;color:var(--dsw-alias-label-primary);opacity:0;pointer-events:none;transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;padding:0;font-size:13px;line-height:18px}.s0QgKq_searchExpanded .s0QgKq_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.s0QgKq_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.s0QgKq_clearButton{border-radius:var(--dsw-radius-sm);width:24px;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.s0QgKq_clearButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_headerActions{flex:none;align-items:center;gap:4px;display:flex}.s0QgKq_viewWrap{display:flex;position:relative}.s0QgKq_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--dsh-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;position:relative}.s0QgKq_list{min-height:0;max-height:100%;margin-left:-4px;margin-right:var(--dsh-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width) - var(--dsh-session-list-scrollbar-offset));scrollbar-gutter:stable;padding-bottom:8px;overflow-y:auto}.s0QgKq_flatList{margin:0;padding:0;list-style:none}.s0QgKq_flatList>*+*{margin-top:2px}.s0QgKq_sessionRow,.s0QgKq_sessionRowSelected{box-sizing:border-box;border-radius:var(--dsw-radius-sm);width:100%;height:32px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:0 8px;font-size:14px;display:flex}.s0QgKq_sessionRow:hover,.s0QgKq_sessionRowSelected{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_rowIcon{width:16px;color:var(--dsw-alias-label-secondary);flex:none;justify-content:center;align-items:center;display:flex}.s0QgKq_title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.s0QgKq_time{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:12px;line-height:17px}.s0QgKq_unread{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-interactive-bg-hover));min-width:18px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-align:center;border-radius:9px;flex:none;padding:0 5px;font-size:11px;font-weight:600;line-height:17px}.s0QgKq_unreadMentioned{background:var(--dsw-alias-state-business-primary);min-width:18px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-align:center;border-radius:9px;flex:none;padding:0 5px;font-size:11px;font-weight:600;line-height:17px}.s0QgKq_empty{color:var(--dsw-alias-label-tertiary);margin:0;padding:16px 12px;font-size:13px}.s0QgKq_emptyState{color:var(--dsw-alias-label-tertiary);text-align:center;flex-direction:column;align-items:center;gap:8px;margin-top:80px;padding:0 12px;font-size:13px;line-height:20px;display:flex}.s0QgKq_emptyState>svg{color:var(--dsw-alias-label-caption);margin-bottom:4px}.s0QgKq_fade{left:0;right:var(--dsh-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}.s0QgKq_menuLayer{z-index:60;max-height:calc(100vh - 16px);position:fixed;overflow-y:auto}.s0QgKq_menuInline{z-index:30;position:absolute;top:32px;right:0}.s0QgKq_menu{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1,var(--dsw-specific-sidebar-fill));min-width:200px;padding:4px;position:relative;box-shadow:0 6px 20px #0000002e}.s0QgKq_menuHeading{color:var(--dsw-alias-label-tertiary);padding:4px 8px 2px;font-size:11px;line-height:16px;display:block}.s0QgKq_menuItem{border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:6px 8px;font-size:13px;display:flex}.s0QgKq_menuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_check{color:var(--dsw-alias-state-business-primary);margin-left:auto}.s0QgKq_visuallyHidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.s0QgKq_rowWrap{position:relative}.s0QgKq_menuRename{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1,var(--dsw-specific-sidebar-fill));min-width:240px;padding:8px;box-shadow:0 6px 20px #0000002e}.s0QgKq_menuRename .s0QgKq_menuHeading{padding:0 0 6px}.s0QgKq_menuInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);appearance:none;background:var(--dsw-alias-bg-base);width:100%;color:var(--dsw-alias-label-primary);font:inherit;outline:none;margin:2px 0 6px;padding:6px 8px;font-size:13px;line-height:20px}.s0QgKq_menuInput:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.s0QgKq_menuInput::placeholder{color:var(--dsw-alias-label-tertiary)}.s0QgKq_menuActions{justify-content:flex-end;gap:6px;padding-top:2px;display:flex}.s0QgKq_menuConfirm{box-sizing:border-box;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border:1px solid #0000;padding:5px 12px;font-size:12px;line-height:18px}.s0QgKq_menuConfirm:hover:not(:disabled){filter:brightness(1.06)}.s0QgKq_menuConfirm:disabled{opacity:.5;cursor:default}.s0QgKq_menuCancel{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;padding:5px 12px;font-size:12px;line-height:18px}.s0QgKq_menuCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_menuItem:disabled{opacity:.5;cursor:default}.s0QgKq_discoverSection{border-top:1px solid var(--dsw-alias-border-l3);margin-top:8px;padding-top:8px}.s0QgKq_discoverHeading{color:var(--dsw-alias-label-tertiary);margin:0 0 4px;padding:0 12px;font-size:11px;line-height:16px}.s0QgKq_discoverRow{border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-primary);align-items:center;gap:8px;padding:4px 12px;font-size:13px;line-height:18px;display:flex}.s0QgKq_discoverRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_discoverName{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.s0QgKq_discoverMeta{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11px}.s0QgKq_joinButton{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-state-business-primary);font:inherit;cursor:pointer;background:0 0;flex:none;padding:2px 10px;font-size:12px}.s0QgKq_joinButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_joinButton:disabled{opacity:.5;cursor:default}.s0QgKq_leaveBody{flex-direction:column;gap:8px;min-width:0;display:flex}.s0QgKq_leaveCopy{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}.s0QgKq_leaveRoomName{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:14px;line-height:22px}.s0QgKq_leaveRoomName strong{font-weight:600}.s0QgKq_leaveError{color:var(--dsw-alias-state-error-primary,#f85149);margin:0;font-size:12px;line-height:17px}.s0QgKq_footer{justify-content:flex-end;gap:8px;display:flex}.s0QgKq_leaveConfirmDanger{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary)}.s0QgKq_leaveConfirmDanger.s0QgKq_leaveConfirmDanger{color:var(--dsw-alias-label-primary)}.s0QgKq_leaveConfirmDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);filter:brightness(1.08)}.s0QgKq_leaveConfirmDanger:disabled{opacity:.5}";
1884
+ const tagId$3 = "@sparkelf/dsh-group-chat/SidebarRooms.module.css";
1885
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1886
+ const tag = document.createElement("style");
1887
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
1888
+ tag.dataset.pluginCss = tagId$3;
1889
+ tag.textContent = css$3;
1890
+ document.head.appendChild(tag);
1891
+ }
1892
+ var SidebarRooms_module_css_default = {
1893
+ "rowIcon": "s0QgKq_rowIcon",
1894
+ "unread": "s0QgKq_unread",
1895
+ "headerActions": "s0QgKq_headerActions",
1896
+ "leaveRoomName": "s0QgKq_leaveRoomName",
1897
+ "menu": "s0QgKq_menu",
1898
+ "menuRename": "s0QgKq_menuRename",
1899
+ "searchSlotExpanded": "s0QgKq_searchSlotExpanded",
1900
+ "rowWrap": "s0QgKq_rowWrap",
1901
+ "menuConfirm": "s0QgKq_menuConfirm",
1902
+ "viewWrap": "s0QgKq_viewWrap",
1903
+ "joinButton": "s0QgKq_joinButton",
1904
+ "listArea": "s0QgKq_listArea",
1905
+ "sessionRowSelected": "s0QgKq_sessionRowSelected",
1906
+ "fade": "s0QgKq_fade",
1907
+ "sectionLabel": "s0QgKq_sectionLabel",
1908
+ "discoverHeading": "s0QgKq_discoverHeading",
1909
+ "check": "s0QgKq_check",
1910
+ "discoverName": "s0QgKq_discoverName",
1911
+ "iconButton": "s0QgKq_iconButton",
1912
+ "menuLayer": "s0QgKq_menuLayer",
1913
+ "leaveCopy": "s0QgKq_leaveCopy",
1914
+ "empty": "s0QgKq_empty",
1915
+ "emptyState": "s0QgKq_emptyState",
1916
+ "clearButton": "s0QgKq_clearButton",
1917
+ "discoverRow": "s0QgKq_discoverRow",
1918
+ "leaveConfirmDanger": "s0QgKq_leaveConfirmDanger",
1919
+ "unreadMentioned": "s0QgKq_unreadMentioned",
1920
+ "searchButton": "s0QgKq_searchButton",
1921
+ "search": "s0QgKq_search",
1922
+ "searchInput": "s0QgKq_searchInput",
1923
+ "discoverSection": "s0QgKq_discoverSection",
1924
+ "sessionRow": "s0QgKq_sessionRow",
1925
+ "menuItem": "s0QgKq_menuItem",
1926
+ "menuCancel": "s0QgKq_menuCancel",
1927
+ "title": "s0QgKq_title",
1928
+ "searchSlot": "s0QgKq_searchSlot",
1929
+ "resizer": "s0QgKq_resizer",
1930
+ "flatList": "s0QgKq_flatList",
1931
+ "root": "s0QgKq_root",
1932
+ "time": "s0QgKq_time",
1933
+ "menuInline": "s0QgKq_menuInline",
1934
+ "menuHeading": "s0QgKq_menuHeading",
1935
+ "sectionHeader": "s0QgKq_sectionHeader",
1936
+ "menuInput": "s0QgKq_menuInput",
1937
+ "list": "s0QgKq_list",
1938
+ "searchExpanded": "s0QgKq_searchExpanded",
1939
+ "visuallyHidden": "s0QgKq_visuallyHidden",
1940
+ "leaveBody": "s0QgKq_leaveBody",
1941
+ "leaveError": "s0QgKq_leaveError",
1942
+ "footer": "s0QgKq_footer",
1943
+ "menuActions": "s0QgKq_menuActions",
1944
+ "discoverMeta": "s0QgKq_discoverMeta"
1945
+ };
1946
+ //#endregion
1947
+ //#region src/client/SidebarRooms.tsx
1948
+ /**
1949
+ * The group-chat region of the left sidebar.
1950
+ *
1951
+ * Structured after `ui-workspace`'s browsing region so the two read as the same design
1952
+ * system rather than a lookalike: a 36px header holding a 14px section label, a
1953
+ * collapsible search slot, and the trailing icon actions (view options, new room), then
1954
+ * a flat list of rows. Sizes, tokens, and icon set are the ones that package uses — the
1955
+ * group-chat region is the workspace region with the grouping removed, because a room is
1956
+ * 1:1 with its session while a workspace is 1:N.
1957
+ *
1958
+ * Registered into `sidebar.footer.action`, the official list seat at the sidebar foot. The
1959
+ * shell renders it inside `footerActions` (full width, `flex: none`), under
1960
+ * `sidebar.workspaces` and above Settings.
1961
+ *
1962
+ * @module @sparkelf/dsh-group-chat/client/SidebarRooms
1963
+ */
1964
+ /**
1965
+ * Height bounds for the region, in pixels.
1966
+ *
1967
+ * The floor keeps a room row and the header visible; the ceiling keeps the region from
1968
+ * crowding out the workspace list above it, which is the same reason the stylesheet capped it
1969
+ * at `46vh` before this was adjustable. The host clamps to the same pair, so a stored value
1970
+ * cannot arrive outside the range the region can draw.
1971
+ */
1972
+ const HEIGHT_MIN = 120;
1973
+ const HEIGHT_MAX = 1200;
1974
+ /** Clamps a requested height into the range the region can draw. */
1975
+ const clampHeight = (value) => Math.min(HEIGHT_MAX, Math.max(HEIGHT_MIN, Math.round(value)));
1976
+ /** How often the list re-reads, so a peer's new room appears without a reload. */
1977
+ const REFRESH_MS = 1e4;
1978
+ /**
1979
+ * Render the sidebar's group-chat region.
1980
+ * @param props - the injected copy resolver and the shared room store.
1981
+ * @returns the section header plus the flat room list.
1982
+ */
1983
+ function SidebarRooms(props) {
1984
+ const { t, tPreset, store } = props;
1985
+ const { rooms, activeRoomId, failure, searchQuery, discoverable } = (0, react.useSyncExternalStore)(store.subscribe, store.getState, store.getState);
1986
+ const [searchExpanded, setSearchExpanded] = (0, react.useState)(false);
1987
+ const [viewOpen, setViewOpen] = (0, react.useState)(false);
1988
+ const [orderBy, setOrderBy] = (0, react.useState)("updated");
1989
+ const [archivedFilter, setArchivedFilter] = (0, react.useState)("default");
1990
+ const [joining, setJoining] = (0, react.useState)(void 0);
1991
+ const searchInput = (0, react.useRef)(null);
1992
+ /**
1993
+ * Height of this region, in pixels, once the user has dragged it.
1994
+ *
1995
+ * `undefined` until either the saved value arrives or a drag happens: the region then keeps
1996
+ * the stylesheet's `max-height` and shrinks to its content, which is what every workspace
1997
+ * looked like before this existed. Storing a number here switches it to a fixed height.
1998
+ */
1999
+ const [heightPx, setHeightPx] = (0, react.useState)(void 0);
2000
+ /** The height being dragged to; `undefined` when no drag is in progress. */
2001
+ const [draggingPx, setDraggingPx] = (0, react.useState)(void 0);
2002
+ const dragStart = (0, react.useRef)(null);
2003
+ (0, react.useEffect)(() => {
2004
+ store.refresh();
2005
+ }, [store]);
2006
+ (0, react.useEffect)(() => {
2007
+ let live = true;
2008
+ readPreferences().then((saved) => {
2009
+ if (!live || saved.sidebarHeightPx === void 0) return;
2010
+ setHeightPx(saved.sidebarHeightPx);
2011
+ });
2012
+ return () => {
2013
+ live = false;
2014
+ };
2015
+ }, []);
2016
+ (0, react.useEffect)(() => {
2017
+ const timer = setInterval(() => {
2018
+ store.refresh();
2019
+ }, REFRESH_MS);
2020
+ return () => clearInterval(timer);
2021
+ }, [store]);
2022
+ /**
2023
+ * Start a height drag from the grip on the region's top edge.
2024
+ *
2025
+ * The delta is inverted because the grip sits above the region: dragging up (a smaller `y`)
2026
+ * makes the region taller, which is the same direction the right sidebar's grip works in.
2027
+ * Listeners go on the document so the pointer may leave the 4px grip — and the window —
2028
+ * without dropping the drag halfway.
2029
+ */
2030
+ const beginResize = (event) => {
2031
+ event.preventDefault();
2032
+ const section = event.currentTarget.parentElement;
2033
+ if (section === null) return;
2034
+ dragStart.current = {
2035
+ y: event.clientY,
2036
+ height: section.getBoundingClientRect().height
2037
+ };
2038
+ setDraggingPx(dragStart.current.height);
2039
+ const onMove = (move) => {
2040
+ const start = dragStart.current;
2041
+ if (start === null) return;
2042
+ setDraggingPx(clampHeight(start.height + (start.y - move.clientY)));
2043
+ };
2044
+ const onUp = () => {
2045
+ document.removeEventListener("pointermove", onMove);
2046
+ document.removeEventListener("pointerup", onUp);
2047
+ document.removeEventListener("pointercancel", onUp);
2048
+ dragStart.current = null;
2049
+ setDraggingPx((settled) => {
2050
+ if (settled !== void 0) {
2051
+ setHeightPx(settled);
2052
+ savePreferences({ sidebarHeightPx: settled }).catch(() => void 0);
2053
+ }
2054
+ });
2055
+ };
2056
+ document.addEventListener("pointermove", onMove);
2057
+ document.addEventListener("pointerup", onUp);
2058
+ document.addEventListener("pointercancel", onUp);
2059
+ };
2060
+ (0, react.useEffect)(() => {
2061
+ if (!searchExpanded) return;
2062
+ const onDown = (event) => {
2063
+ if (event.target?.closest(`.${SidebarRooms_module_css_default.search}`) === null) {
2064
+ if (searchQuery.length === 0) setSearchExpanded(false);
2065
+ }
2066
+ };
2067
+ document.addEventListener("mousedown", onDown);
2068
+ return () => document.removeEventListener("mousedown", onDown);
2069
+ }, [searchExpanded, searchQuery.length]);
2070
+ const query = searchQuery.trim().toLowerCase();
2071
+ const visible = (rooms ?? []).filter((room) => archivedFilter === "all" || room.joined).filter((room) => query.length === 0 || room.name.toLowerCase().includes(query)).slice().sort((a, b) => orderBy === "manual" ? 0 : Date.parse(b.updatedAt || b.createdAt) - Date.parse(a.updatedAt || a.createdAt));
2072
+ const onPickOrder = (0, react.useCallback)(async (mode) => {
2073
+ setOrderBy(mode);
2074
+ setViewOpen(false);
2075
+ }, []);
2076
+ (0, react.useEffect)(() => {
2077
+ const keyword = searchQuery.trim();
2078
+ if (keyword.length === 0) return;
2079
+ let cancelled = false;
2080
+ const timer = setTimeout(() => {
2081
+ if (!cancelled) store.discover(keyword);
2082
+ }, 300);
2083
+ return () => {
2084
+ cancelled = true;
2085
+ clearTimeout(timer);
2086
+ };
2087
+ }, [searchQuery, store]);
2088
+ const joinable = joinableRooms(discoverable ?? [], new Set((rooms ?? []).map((room) => room.id)));
2089
+ const onJoin = async (roomId) => {
2090
+ setJoining(roomId);
2091
+ try {
2092
+ await store.join(roomId);
2093
+ store.open(roomId);
2094
+ store.setSearch("");
2095
+ setSearchExpanded(false);
2096
+ } finally {
2097
+ setJoining(void 0);
2098
+ }
2099
+ };
2100
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2101
+ className: SidebarRooms_module_css_default.root,
2102
+ "aria-label": t("title"),
2103
+ style: {
2104
+ height: `${String(draggingPx ?? heightPx ?? HEIGHT_MIN)}px`,
2105
+ maxHeight: "none"
2106
+ },
2107
+ children: [
2108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2109
+ className: SidebarRooms_module_css_default.resizer,
2110
+ role: "separator",
2111
+ "aria-orientation": "horizontal",
2112
+ "aria-label": t("resizeRegion"),
2113
+ "aria-valuenow": Math.round(draggingPx ?? heightPx ?? 0) || void 0,
2114
+ tabIndex: 0,
2115
+ onPointerDown: beginResize,
2116
+ onKeyDown: (event) => {
2117
+ const step = event.shiftKey ? 40 : 10;
2118
+ const grow = event.key === "ArrowUp" || event.key === "ArrowLeft";
2119
+ const shrink = event.key === "ArrowDown" || event.key === "ArrowRight";
2120
+ if (!grow && !shrink) return;
2121
+ event.preventDefault();
2122
+ event.currentTarget.parentElement;
2123
+ const next = clampHeight((heightPx ?? HEIGHT_MIN) + (grow ? step : -step));
2124
+ setHeightPx(next);
2125
+ savePreferences({ sidebarHeightPx: next }).catch(() => void 0);
2126
+ }
2127
+ }),
2128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2129
+ className: SidebarRooms_module_css_default.sectionHeader,
2130
+ children: [
2131
+ !searchExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2132
+ className: SidebarRooms_module_css_default.sectionLabel,
2133
+ children: t("title")
2134
+ }),
2135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2136
+ className: searchExpanded ? SidebarRooms_module_css_default.searchSlotExpanded : SidebarRooms_module_css_default.searchSlot,
2137
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2138
+ className: searchExpanded ? SidebarRooms_module_css_default.searchExpanded : SidebarRooms_module_css_default.search,
2139
+ onClick: () => {
2140
+ setSearchExpanded(true);
2141
+ searchInput.current?.focus();
2142
+ },
2143
+ children: [
2144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2145
+ label: t("search"),
2146
+ side: "bottom",
2147
+ delayMs: 500,
2148
+ disabled: searchExpanded,
2149
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2150
+ type: "button",
2151
+ className: SidebarRooms_module_css_default.searchButton,
2152
+ "aria-label": t("search"),
2153
+ "aria-expanded": searchExpanded,
2154
+ onClick: (event) => {
2155
+ event.stopPropagation();
2156
+ setSearchExpanded((v) => !v);
2157
+ },
2158
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: searchExpanded ? 11 : 14 })
2159
+ })
2160
+ }),
2161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2162
+ ref: searchInput,
2163
+ className: SidebarRooms_module_css_default.searchInput,
2164
+ type: "text",
2165
+ placeholder: t("searchPlaceholder"),
2166
+ value: searchQuery,
2167
+ tabIndex: searchExpanded ? 0 : -1,
2168
+ onChange: (event) => store.setSearch(event.target.value),
2169
+ onKeyDown: (event) => {
2170
+ if (event.key !== "Escape") return;
2171
+ store.setSearch("");
2172
+ setSearchExpanded(false);
2173
+ }
2174
+ }),
2175
+ searchExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2176
+ type: "button",
2177
+ className: SidebarRooms_module_css_default.clearButton,
2178
+ "aria-label": t("searchClear"),
2179
+ onClick: (event) => {
2180
+ event.stopPropagation();
2181
+ store.setSearch("");
2182
+ setSearchExpanded(false);
2183
+ },
2184
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
2185
+ })
2186
+ ]
2187
+ })
2188
+ }),
2189
+ !searchExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2190
+ className: SidebarRooms_module_css_default.headerActions,
2191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
2192
+ t,
2193
+ open: viewOpen,
2194
+ orderBy,
2195
+ archivedFilter,
2196
+ onToggle: () => setViewOpen((v) => !v),
2197
+ onPickOrder: (mode) => void onPickOrder(mode),
2198
+ onPickArchived: (mode) => {
2199
+ setArchivedFilter(mode);
2200
+ setViewOpen(false);
2201
+ }
2202
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2203
+ label: t("createRoom"),
2204
+ side: "bottom",
2205
+ delayMs: 500,
2206
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2207
+ type: "button",
2208
+ className: SidebarRooms_module_css_default.iconButton,
2209
+ "aria-label": t("createRoom"),
2210
+ onClick: () => store.openCreate(),
2211
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconProjectAddOutlineRegular, { size: 16 })
2212
+ })
2213
+ })]
2214
+ })
2215
+ ]
2216
+ }),
2217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2218
+ className: SidebarRooms_module_css_default.listArea,
2219
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2220
+ className: SidebarRooms_module_css_default.list,
2221
+ children: [
2222
+ failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2223
+ className: SidebarRooms_module_css_default.empty,
2224
+ children: t(failure)
2225
+ }),
2226
+ failure === void 0 && rooms === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2227
+ className: SidebarRooms_module_css_default.empty,
2228
+ children: t("loading")
2229
+ }),
2230
+ failure === void 0 && rooms !== void 0 && visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2231
+ className: SidebarRooms_module_css_default.emptyState,
2232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutlineRegular, { size: 28 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: query.length > 0 ? t("searchEmpty") : t("emptyRooms") })]
2233
+ }),
2234
+ visible.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2235
+ className: SidebarRooms_module_css_default.flatList,
2236
+ children: visible.map((room) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomRow, {
2237
+ room,
2238
+ t,
2239
+ store,
2240
+ selected: room.id === activeRoomId,
2241
+ onOpen: () => store.open(room.id)
2242
+ }) }, room.id))
2243
+ }),
2244
+ query.length > 0 && discoverable !== void 0 && joinable.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2245
+ className: SidebarRooms_module_css_default.discoverSection,
2246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2247
+ className: SidebarRooms_module_css_default.discoverHeading,
2248
+ children: t("discoverHeading")
2249
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2250
+ className: SidebarRooms_module_css_default.flatList,
2251
+ children: joinable.map((room) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2252
+ className: SidebarRooms_module_css_default.discoverRow,
2253
+ children: [
2254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2255
+ className: SidebarRooms_module_css_default.discoverName,
2256
+ children: room.name
2257
+ }),
2258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2259
+ className: SidebarRooms_module_css_default.discoverMeta,
2260
+ children: t("discoverMembers").replace("{count}", String(room.memberCount))
2261
+ }),
2262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2263
+ type: "button",
2264
+ className: SidebarRooms_module_css_default.joinButton,
2265
+ disabled: joining === room.id,
2266
+ onClick: () => {
2267
+ onJoin(room.id);
2268
+ },
2269
+ children: joining === room.id ? t("joining") : t("joinRoom")
2270
+ })
2271
+ ]
2272
+ }) }, room.id))
2273
+ })]
2274
+ })
2275
+ ]
2276
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2277
+ className: SidebarRooms_module_css_default.fade,
2278
+ "aria-hidden": "true"
2279
+ })]
2280
+ }),
2281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreateRoomDialog, {
2282
+ t,
2283
+ tPreset,
2284
+ store
2285
+ }),
2286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomDialogs, {
2287
+ t,
2288
+ store
2289
+ })
2290
+ ]
2291
+ });
2292
+ }
2293
+ /** One room row: glyph, name, and the member count where a session row shows its time. */
2294
+ function RoomRow(props) {
2295
+ const { room, t, selected, onOpen, store } = props;
2296
+ const [menuAnchor, setMenuAnchor] = (0, react.useState)(null);
2297
+ const [confirmingLeave, setConfirmingLeave] = (0, react.useState)(false);
2298
+ const [leaving, setLeaving] = (0, react.useState)(false);
2299
+ const [leaveFailure, setLeaveFailure] = (0, react.useState)(void 0);
2300
+ const [confirmingDissolve, setConfirmingDissolve] = (0, react.useState)(false);
2301
+ const [dissolving, setDissolving] = (0, react.useState)(false);
2302
+ const [dissolveFailure, setDissolveFailure] = (0, react.useState)(void 0);
2303
+ const unread = room.joined ? room.unreadCount ?? 0 : 0;
2304
+ const mentioned = unread > 0 && (room.mentionCount ?? 0) > 0;
2305
+ const closeLeave = () => {
2306
+ setConfirmingLeave(false);
2307
+ setLeaveFailure(void 0);
2308
+ };
2309
+ const closeDissolve = () => {
2310
+ setConfirmingDissolve(false);
2311
+ setDissolveFailure(void 0);
2312
+ };
2313
+ const leave = async () => {
2314
+ if (leaving) return;
2315
+ setLeaving(true);
2316
+ setLeaveFailure(void 0);
2317
+ try {
2318
+ await store.leave(room.id);
2319
+ setConfirmingLeave(false);
2320
+ } catch (cause) {
2321
+ setLeaveFailure(failureOf(cause));
2322
+ } finally {
2323
+ setLeaving(false);
2324
+ }
2325
+ };
2326
+ const dissolve = async () => {
2327
+ if (dissolving) return;
2328
+ setDissolving(true);
2329
+ setDissolveFailure(void 0);
2330
+ try {
2331
+ await store.dissolve(room.id);
2332
+ setConfirmingDissolve(false);
2333
+ } catch (cause) {
2334
+ setDissolveFailure(failureOf(cause));
2335
+ } finally {
2336
+ setDissolving(false);
2337
+ }
2338
+ };
2339
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2340
+ className: SidebarRooms_module_css_default.rowWrap,
2341
+ children: [
2342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2343
+ type: "button",
2344
+ className: selected ? SidebarRooms_module_css_default.sessionRowSelected : SidebarRooms_module_css_default.sessionRow,
2345
+ onClick: onOpen,
2346
+ onContextMenu: (event) => {
2347
+ event.preventDefault();
2348
+ setMenuAnchor(event.currentTarget.getBoundingClientRect());
2349
+ },
2350
+ "aria-current": selected,
2351
+ children: [
2352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2353
+ className: SidebarRooms_module_css_default.rowIcon,
2354
+ "aria-hidden": "true",
2355
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
2356
+ width: "16",
2357
+ height: "16",
2358
+ viewBox: "0 0 16 16",
2359
+ fill: "none",
2360
+ stroke: "currentColor",
2361
+ strokeWidth: "1.4",
2362
+ strokeLinecap: "round",
2363
+ strokeLinejoin: "round",
2364
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 3.5A1.5 1.5 0 0 1 4 2h5a1.5 1.5 0 0 1 1.5 1.5v3A1.5 1.5 0 0 1 9 8H5.5L3 10V8h-.5A1.5 1.5 0 0 1 2.5 6.5z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M11 5.5h1A1.5 1.5 0 0 1 13.5 7v3.5A1.5 1.5 0 0 1 12 12h-.5v2l-2.5-2H7" })]
2365
+ })
2366
+ }),
2367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2368
+ className: SidebarRooms_module_css_default.title,
2369
+ children: room.name
2370
+ }),
2371
+ unread > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2372
+ className: mentioned ? SidebarRooms_module_css_default.unreadMentioned : SidebarRooms_module_css_default.unread,
2373
+ "aria-label": mentioned ? t("unreadMentionAria").replace("{count}", String(unread)) : t("unreadAria").replace("{count}", String(unread)),
2374
+ children: unread > 99 ? "99+" : String(unread)
2375
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2376
+ className: SidebarRooms_module_css_default.time,
2377
+ children: room.joined ? t("memberCountShort").replace("{count}", String(room.memberCount)) : t("left")
2378
+ })
2379
+ ]
2380
+ }),
2381
+ menuAnchor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomMenu, {
2382
+ t,
2383
+ room,
2384
+ store,
2385
+ anchor: menuAnchor,
2386
+ onClose: () => setMenuAnchor(null),
2387
+ onRequestLeave: () => {
2388
+ setMenuAnchor(null);
2389
+ setConfirmingLeave(true);
2390
+ },
2391
+ onRequestDissolve: () => {
2392
+ setMenuAnchor(null);
2393
+ setConfirmingDissolve(true);
2394
+ }
2395
+ }),
2396
+ confirmingLeave && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeaveRoomDialog, {
2397
+ t,
2398
+ room,
2399
+ busy: leaving,
2400
+ failure: leaveFailure,
2401
+ onCancel: closeLeave,
2402
+ onConfirm: () => void leave()
2403
+ }),
2404
+ confirmingDissolve && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DissolveRoomDialog, {
2405
+ t,
2406
+ room,
2407
+ busy: dissolving,
2408
+ failure: dissolveFailure,
2409
+ onCancel: closeDissolve,
2410
+ onConfirm: () => void dissolve()
2411
+ })
2412
+ ]
2413
+ });
2414
+ }
2415
+ /**
2416
+ * The row's context menu.
2417
+ *
2418
+ * Two official workspace entries are absent here rather than relabelled. Archiving would
2419
+ * duplicate "leave room", and forking copies history whose events carry no member
2420
+ * attribution — producing a room that looks continuous but whose earlier messages belong
2421
+ * to nobody. The remaining entries are the ones the design settled on, and each is shown
2422
+ * only when it applies to this room and this caller.
2423
+ */
2424
+ function RoomMenu(props) {
2425
+ const { t, room, store, onClose, anchor, onRequestLeave, onRequestDissolve } = props;
2426
+ const [renaming, setRenaming] = (0, react.useState)(false);
2427
+ const [draftName, setDraftName] = (0, react.useState)(room.name);
2428
+ const [busy, setBusy] = (0, react.useState)(false);
2429
+ const run = async (action) => {
2430
+ setBusy(true);
2431
+ try {
2432
+ await action();
2433
+ onClose();
2434
+ } finally {
2435
+ setBusy(false);
2436
+ }
2437
+ };
2438
+ const below = window.innerHeight - anchor.bottom - 8;
2439
+ const placement = below < 220 && anchor.top > below ? { bottom: window.innerHeight - anchor.top + 4 } : { top: anchor.bottom + 4 };
2440
+ const portal = (body) => (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2441
+ className: SidebarRooms_module_css_default.menuLayer,
2442
+ style: {
2443
+ ...placement,
2444
+ left: anchor.left
2445
+ },
2446
+ onClick: (event) => event.stopPropagation(),
2447
+ children: body
2448
+ }), document.body);
2449
+ if (renaming) {
2450
+ const commit = () => {
2451
+ run(() => store.rename(room.id, draftName.trim()));
2452
+ };
2453
+ const cancel = () => {
2454
+ setRenaming(false);
2455
+ setDraftName(room.name);
2456
+ };
2457
+ return portal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2458
+ className: SidebarRooms_module_css_default.menuRename,
2459
+ role: "dialog",
2460
+ "aria-label": t("renameTitle"),
2461
+ children: [
2462
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2463
+ className: SidebarRooms_module_css_default.menuHeading,
2464
+ children: t("renameTitle")
2465
+ }),
2466
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2467
+ className: SidebarRooms_module_css_default.menuInput,
2468
+ value: draftName,
2469
+ autoFocus: true,
2470
+ placeholder: t("renamePlaceholder"),
2471
+ onChange: (event) => setDraftName(event.target.value),
2472
+ onKeyDown: (event) => {
2473
+ if (event.key === "Enter" && draftName.trim().length > 0) commit();
2474
+ if (event.key === "Escape") cancel();
2475
+ }
2476
+ }),
2477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2478
+ className: SidebarRooms_module_css_default.menuActions,
2479
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2480
+ type: "button",
2481
+ className: SidebarRooms_module_css_default.menuCancel,
2482
+ onClick: cancel,
2483
+ children: t("cancel")
2484
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2485
+ type: "button",
2486
+ className: SidebarRooms_module_css_default.menuConfirm,
2487
+ disabled: busy || draftName.trim().length === 0,
2488
+ onClick: commit,
2489
+ children: t("save")
2490
+ })]
2491
+ })
2492
+ ]
2493
+ }));
2494
+ }
2495
+ return portal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2496
+ className: SidebarRooms_module_css_default.menu,
2497
+ role: "menu",
2498
+ children: [
2499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2500
+ type: "button",
2501
+ role: "menuitem",
2502
+ className: SidebarRooms_module_css_default.menuItem,
2503
+ onClick: () => setRenaming(true),
2504
+ children: t("menuRename")
2505
+ }),
2506
+ room.joined && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2507
+ type: "button",
2508
+ role: "menuitem",
2509
+ className: SidebarRooms_module_css_default.menuItem,
2510
+ onClick: () => {
2511
+ store.setDialog({
2512
+ kind: "invite",
2513
+ roomId: room.id
2514
+ });
2515
+ onClose();
2516
+ },
2517
+ children: t("menuInvite")
2518
+ }),
2519
+ store.openMembers !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2520
+ type: "button",
2521
+ role: "menuitem",
2522
+ className: SidebarRooms_module_css_default.menuItem,
2523
+ onClick: () => {
2524
+ store.openMembers?.(room.id);
2525
+ onClose();
2526
+ },
2527
+ children: t("menuMembers")
2528
+ }),
2529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2530
+ type: "button",
2531
+ role: "menuitem",
2532
+ className: SidebarRooms_module_css_default.menuItem,
2533
+ onClick: () => {
2534
+ store.setDialog({
2535
+ kind: "settings",
2536
+ roomId: room.id
2537
+ });
2538
+ onClose();
2539
+ },
2540
+ children: t("menuSettings")
2541
+ }),
2542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2543
+ type: "button",
2544
+ role: "menuitem",
2545
+ className: SidebarRooms_module_css_default.menuItem,
2546
+ onClick: () => {
2547
+ navigator.clipboard?.writeText(room.id);
2548
+ onClose();
2549
+ },
2550
+ children: t("copyRoomId")
2551
+ }),
2552
+ room.joined && store.selfId() === room.ownerUserId && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2553
+ type: "button",
2554
+ role: "menuitem",
2555
+ className: SidebarRooms_module_css_default.menuItem,
2556
+ onClick: () => {
2557
+ store.setDialog({
2558
+ kind: "transfer",
2559
+ roomId: room.id
2560
+ });
2561
+ onClose();
2562
+ },
2563
+ children: t("menuTransfer")
2564
+ }),
2565
+ room.joined && store.selfId() !== room.ownerUserId && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2566
+ type: "button",
2567
+ role: "menuitem",
2568
+ className: SidebarRooms_module_css_default.menuItem,
2569
+ onClick: onRequestLeave,
2570
+ children: t("menuLeave")
2571
+ }),
2572
+ (store.selfId() === room.ownerUserId || store.administrator()) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2573
+ type: "button",
2574
+ role: "menuitem",
2575
+ className: SidebarRooms_module_css_default.menuItem,
2576
+ onClick: onRequestDissolve,
2577
+ children: t("menuDissolve")
2578
+ })
2579
+ ]
2580
+ }));
2581
+ }
2582
+ /**
2583
+ * The dissolve-room confirmation.
2584
+ *
2585
+ * A separate dialog from leaving, not a variant of it: leaving removes one membership and the
2586
+ * room carries on for everyone else, while dissolving ends it for every member and takes the
2587
+ * messages with it. Sharing one dialog would let the far more destructive act read as the
2588
+ * mild one.
2589
+ *
2590
+ * Like the leave dialog, a refusal keeps this open and prints the mapped reason: the room is
2591
+ * still there, so closing would read as success.
2592
+ */
2593
+ function DissolveRoomDialog(props) {
2594
+ const { t, room, busy, failure, onCancel, onConfirm } = props;
2595
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2596
+ open: true,
2597
+ onClose: onCancel,
2598
+ title: t("menuDissolve"),
2599
+ closeLabel: t("cancel"),
2600
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2601
+ className: SidebarRooms_module_css_default.footer,
2602
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2603
+ "data-modal-autofocus": true,
2604
+ variant: "ghost",
2605
+ size: "md",
2606
+ disabled: busy,
2607
+ onClick: onCancel,
2608
+ children: t("cancel")
2609
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2610
+ variant: "ghost",
2611
+ size: "md",
2612
+ disabled: busy,
2613
+ className: SidebarRooms_module_css_default.leaveConfirmDanger,
2614
+ onClick: onConfirm,
2615
+ children: t("menuDissolve")
2616
+ })]
2617
+ }),
2618
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2619
+ className: SidebarRooms_module_css_default.leaveBody,
2620
+ children: [
2621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2622
+ className: SidebarRooms_module_css_default.leaveCopy,
2623
+ children: t("menuDissolveConfirm")
2624
+ }),
2625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2626
+ className: SidebarRooms_module_css_default.leaveRoomName,
2627
+ children: [t("menuDissolveTarget"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: room.name })]
2628
+ }),
2629
+ failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2630
+ className: SidebarRooms_module_css_default.leaveError,
2631
+ role: "alert",
2632
+ children: t(failure)
2633
+ })
2634
+ ]
2635
+ })
2636
+ });
2637
+ }
2638
+ /**
2639
+ * The leave-room confirmation.
2640
+ *
2641
+ * It exists because leaving is not undoable from this surface: the sidebar keeps the room
2642
+ * under the "left rooms" filter, but rejoining is a separate discovery step. The copy names
2643
+ * that the agent's recorded history stays, which is the part a reader cannot infer.
2644
+ *
2645
+ * A refused leave keeps this dialog open and prints the mapped failure. Closing on failure
2646
+ * was the old behaviour and read as success — the only sign it had gone wrong was the room
2647
+ * still being in the list.
2648
+ */
2649
+ function LeaveRoomDialog(props) {
2650
+ const { t, room, busy, failure, onCancel, onConfirm } = props;
2651
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2652
+ open: true,
2653
+ onClose: onCancel,
2654
+ title: t("menuLeave"),
2655
+ closeLabel: t("cancel"),
2656
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2657
+ className: SidebarRooms_module_css_default.footer,
2658
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2659
+ "data-modal-autofocus": true,
2660
+ variant: "ghost",
2661
+ size: "md",
2662
+ disabled: busy,
2663
+ onClick: onCancel,
2664
+ children: t("cancel")
2665
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2666
+ variant: "primary",
2667
+ size: "md",
2668
+ disabled: busy,
2669
+ onClick: onConfirm,
2670
+ children: t("menuLeave")
2671
+ })]
2672
+ }),
2673
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2674
+ className: SidebarRooms_module_css_default.leaveBody,
2675
+ children: [
2676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2677
+ className: SidebarRooms_module_css_default.leaveCopy,
2678
+ children: t("menuLeaveConfirm")
2679
+ }),
2680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2681
+ className: SidebarRooms_module_css_default.leaveRoomName,
2682
+ children: room.name
2683
+ }),
2684
+ failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2685
+ className: SidebarRooms_module_css_default.leaveError,
2686
+ role: "alert",
2687
+ children: t(failure)
2688
+ })
2689
+ ]
2690
+ })
2691
+ });
2692
+ }
2693
+ /** The trailing view menu: ordering and whether rooms the operator left are listed. */
2694
+ function ViewOptionsMenu(props) {
2695
+ const { t, open, orderBy, archivedFilter, onToggle, onPickOrder, onPickArchived } = props;
2696
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2697
+ className: SidebarRooms_module_css_default.viewWrap,
2698
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2699
+ label: t("viewOptions"),
2700
+ side: "bottom",
2701
+ delayMs: 500,
2702
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2703
+ type: "button",
2704
+ className: SidebarRooms_module_css_default.iconButton,
2705
+ "aria-label": t("viewOptions"),
2706
+ "aria-expanded": open,
2707
+ onClick: onToggle,
2708
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, { size: 16 })
2709
+ })
2710
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2711
+ className: SidebarRooms_module_css_default.menuInline,
2712
+ role: "menu",
2713
+ children: [
2714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2715
+ className: SidebarRooms_module_css_default.menuHeading,
2716
+ children: t("orderBy")
2717
+ }),
2718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2719
+ type: "button",
2720
+ role: "menuitem",
2721
+ className: SidebarRooms_module_css_default.menuItem,
2722
+ onClick: () => onPickOrder("updated"),
2723
+ children: [
2724
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, { size: 14 }),
2725
+ t("orderByUpdated"),
2726
+ orderBy === "updated" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2727
+ className: SidebarRooms_module_css_default.check,
2728
+ children: "✓"
2729
+ })
2730
+ ]
2731
+ }),
2732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2733
+ type: "button",
2734
+ role: "menuitem",
2735
+ className: SidebarRooms_module_css_default.menuItem,
2736
+ onClick: () => onPickOrder("manual"),
2737
+ children: [
2738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, { size: 14 }),
2739
+ t("orderByManual"),
2740
+ orderBy === "manual" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2741
+ className: SidebarRooms_module_css_default.check,
2742
+ children: "✓"
2743
+ })
2744
+ ]
2745
+ }),
2746
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2747
+ className: SidebarRooms_module_css_default.menuHeading,
2748
+ children: t("archivedFilter")
2749
+ }),
2750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2751
+ type: "button",
2752
+ role: "menuitem",
2753
+ className: SidebarRooms_module_css_default.menuItem,
2754
+ onClick: () => onPickArchived("default"),
2755
+ children: [t("archivedHide"), archivedFilter === "default" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2756
+ className: SidebarRooms_module_css_default.check,
2757
+ children: "✓"
2758
+ })]
2759
+ }),
2760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2761
+ type: "button",
2762
+ role: "menuitem",
2763
+ className: SidebarRooms_module_css_default.menuItem,
2764
+ onClick: () => onPickArchived("all"),
2765
+ children: [t("archivedShow"), archivedFilter === "all" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2766
+ className: SidebarRooms_module_css_default.check,
2767
+ children: "✓"
2768
+ })]
2769
+ })
2770
+ ]
2771
+ })]
2772
+ });
2773
+ }
2774
+ //#endregion
2775
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/MessageSide.module.css.mjs
2776
+ const css$2 = ".KXabfq_otherRow{display:contents}.KXabfq_otherRow.KXabfq_otherRow>*{align-items:flex-start}.KXabfq_otherRow{flex-direction:column;align-items:flex-start;min-width:0;max-width:100%;display:flex}.KXabfq_author{min-width:0;max-width:100%;color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;margin:0 0 4px 2px;font-size:12px;line-height:16px;display:flex}.KXabfq_authorName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.KXabfq_otherRow.KXabfq_otherRow>:not(.KXabfq_author){align-items:flex-start;max-width:100%}";
2777
+ const tagId$2 = "@sparkelf/dsh-group-chat/MessageSide.module.css";
2778
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
2779
+ const tag = document.createElement("style");
2780
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
2781
+ tag.dataset.pluginCss = tagId$2;
2782
+ tag.textContent = css$2;
2783
+ document.head.appendChild(tag);
2784
+ }
2785
+ var MessageSide_module_css_default = {
2786
+ "authorName": "KXabfq_authorName",
2787
+ "author": "KXabfq_author",
2788
+ "otherRow": "KXabfq_otherRow"
2789
+ };
2790
+ //#endregion
2791
+ //#region src/client/MessageSide.tsx
2792
+ /**
2793
+ * Left/right placement for group-chat messages.
2794
+ *
2795
+ * The official Chat renderer draws every user message right-aligned — its own CSS is
2796
+ * unconditional (`align-items: flex-end`), with no self/other distinction, because in an
2797
+ * ordinary Session the only author is the operator. A room has several authors, so the
2798
+ * side has to carry a meaning the official renderer does not compute: this workspace's own
2799
+ * messages stay right, everyone else's move left.
2800
+ *
2801
+ * It is implemented by **wrapping the official renderer, not replacing it**. The official
2802
+ * component is read back out of the slot registry and rendered as an element, so markdown,
2803
+ * images, file and skill references, per-message actions and the streaming behaviour all
2804
+ * stay the official implementation's. Re-drawing the bubble here would have to track every
2805
+ * one of those forever.
2806
+ *
2807
+ * An ordinary Session is untouched: a message with no room source is rendered through the
2808
+ * official component with no wrapper at all, which is what keeps the two paths equal in
2809
+ * what they draw.
2810
+ *
2811
+ * Another author's message also carries their name and picture above the bubble. The official
2812
+ * renderer draws neither — in an ordinary Session every message is the operator's own, so there
2813
+ * is no author to attribute — and the event itself carries only the author's id. The name and
2814
+ * picture therefore come from the roster, looked up by that id. When the roster has not loaded,
2815
+ * or the author has since left, the message renders bare rather than labelled with an id: a
2816
+ * wrong name is worse than no name.
2817
+ *
2818
+ * @module @sparkelf/dsh-group-chat/client/MessageSide
2819
+ */
2820
+ /**
2821
+ * Which side a message belongs on, or `undefined` when it is not a room message.
2822
+ *
2823
+ * `roomId` is the discriminator: the Host stamps it on every message it admits to a room,
2824
+ * and an ordinary Session's message carries `{kind, rpcId, clientTimeZone}` instead. The
2825
+ * remaining cases split the room's own authors — an agent is always someone else, and a
2826
+ * human is "self" only when the id matches this workspace's account. A message whose
2827
+ * author cannot be identified is placed left, because assuming "self" would silently
2828
+ * attribute another member's words to the operator.
2829
+ *
2830
+ * @param message - the message data the node carries.
2831
+ * @param selfId - this workspace's own account id, when known.
2832
+ * @returns `'self'`, `'other'`, or `undefined` for a non-room message.
2833
+ */
2834
+ const messageSide = (message, selfId) => {
2835
+ const source = message?.source;
2836
+ if (source === void 0 || source === null) return void 0;
2837
+ if (typeof source.roomId !== "string" || source.roomId.length === 0) return void 0;
2838
+ if (source.authorKind === "agent") return "other";
2839
+ return selfId !== void 0 && source.memberId === selfId ? "self" : "other";
2840
+ };
2841
+ /**
2842
+ * The roster entry for one message's author, when the room knows it.
2843
+ *
2844
+ * The bubble needs the name and the picture, and neither is on the event: a message carries only
2845
+ * `source.memberId`. The roster is the only place those two facts live, so this is the lookup
2846
+ * that lets another author's message say who wrote it.
2847
+ *
2848
+ * @param message - the message data the node carries.
2849
+ * @param roster - that room's members, or `undefined` before the roster has been read.
2850
+ * @returns the matching member, or `undefined` when the author is unknown or has left.
2851
+ */
2852
+ const authorOf = (message, roster) => {
2853
+ const memberId = message?.source?.memberId;
2854
+ if (typeof memberId !== "string" || memberId.length === 0) return void 0;
2855
+ return roster?.find((member) => member.userId === memberId);
2856
+ };
2857
+ /**
2858
+ * The name to draw above another author's message.
2859
+ *
2860
+ * Falls back to the roster's display name only, never to the id: an unlabelled bubble is
2861
+ * readable, but a bubble labelled `u_7f3a91` is not, and it would also leak an internal id into
2862
+ * the conversation.
2863
+ *
2864
+ * @param member - the author's roster entry.
2865
+ * @returns the name, or `undefined` when the roster has no usable one.
2866
+ */
2867
+ const authorNameOf = (member) => {
2868
+ const name = member?.displayName?.trim() ?? "";
2869
+ return name.length === 0 ? void 0 : name;
2870
+ };
2871
+ /**
2872
+ * Render one node with its room side applied.
2873
+ *
2874
+ * Two details this has to get right, both of which fail silently otherwise:
2875
+ *
2876
+ * The official view is created as a React **element**, never called as a plain function. It
2877
+ * is a `React.memo` component, which compiles to a non-callable object, so calling it throws
2878
+ * `TypeError: … is not a function`; and even when a view is callable, calling it directly
2879
+ * would run its Hooks as this component's and drop its memoization.
2880
+ *
2881
+ * The account id is read through `useSyncExternalStore` rather than captured. It is fetched
2882
+ * after the first paint, and a value captured when this renderer was registered would stay
2883
+ * `undefined` for the life of the page — every message would keep the unknown-author
2884
+ * placement, and nothing would ever correct it. The store is the same observable the room
2885
+ * list uses, and the snapshot is a plain string, so an unrelated room-list change notifies
2886
+ * without re-rendering: React compares the snapshot by identity.
2887
+ *
2888
+ * @param props - the official props plus this wrapper's placement inputs.
2889
+ * @returns the official view, wrapped only when the message is another author's room message.
2890
+ */
2891
+ const MessageSide = (props) => {
2892
+ const { node, Official, getSelfId, subscribeSelf, sided, getRoster } = props;
2893
+ const selfId = (0, react.useSyncExternalStore)(subscribeSelf, getSelfId, getSelfId);
2894
+ if (typeof Official !== "function" && typeof Official !== "object") return null;
2895
+ const view = (0, react.createElement)(Official, props);
2896
+ const message = node?.data;
2897
+ if ((sided ? messageSide(message, selfId) : void 0) !== "other") return view;
2898
+ const roomId = message?.source?.roomId;
2899
+ const member = authorOf(message, typeof roomId === "string" && roomId.length > 0 ? getRoster(roomId) : void 0);
2900
+ const name = authorNameOf(member);
2901
+ const head = name === void 0 || member === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2902
+ className: MessageSide_module_css_default["author"],
2903
+ "data-group-chat-author": member.userId,
2904
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
2905
+ name,
2906
+ src: member.avatarUrl,
2907
+ size: "sm"
2908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2909
+ className: MessageSide_module_css_default["authorName"],
2910
+ children: name
2911
+ })]
2912
+ });
2913
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2914
+ className: MessageSide_module_css_default["otherRow"],
2915
+ "data-group-chat-side": "other",
2916
+ children: [head, view]
2917
+ });
2918
+ };
2919
+ //#endregion
2920
+ //#region src/session-id.ts
2921
+ /**
2922
+ * The room/session binding, shared by both halves of the plugin.
2923
+ *
2924
+ * The browser half needs it to open a room's Session; the host half needs it to create and
2925
+ * recognize one. It lives in its own module because the two halves are built for different
2926
+ * runtimes: importing it from the host entry would drag the whole server side into the client
2927
+ * bundle, which the build's platform guard refuses — correctly, since none of it can run there.
2928
+ *
2929
+ * @module @sparkelf/dsh-group-chat/session-id
2930
+ */
2931
+ /**
2932
+ * Every room session id starts with this, so a room's session is recognizable after a restart
2933
+ * and an ordinary session never matches.
2934
+ *
2935
+ * The binding lives in the id rather than in `SessionHeader` because the header carries no
2936
+ * free-form metadata field.
2937
+ */
2938
+ const ROOM_SESSION_PREFIX = "group-room-";
2939
+ /** Deterministic session id for a room. */
2940
+ const roomSessionId = (roomId) => `${ROOM_SESSION_PREFIX}${roomId}`;
2941
+ /** Reads the room a session is bound to, or `undefined` for an ordinary session. */
2942
+ const roomIdOfSession = (session) => {
2943
+ if (typeof session !== "object" || session === null) return void 0;
2944
+ const id = session.id;
2945
+ if (typeof id !== "string" || !id.startsWith("group-room-")) return void 0;
2946
+ const roomId = id.slice(11);
2947
+ return roomId.length > 0 ? roomId : void 0;
2948
+ };
2949
+ //#endregion
2950
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/MembersPanel.module.css.mjs
2951
+ const css$1 = ".VeQVAa_root{box-sizing:border-box;flex-direction:column;gap:16px;min-width:200px;padding:12px 12px 20px;display:flex}.VeQVAa_section{flex-direction:column;gap:8px;min-width:0;display:flex}.VeQVAa_section+.VeQVAa_section{border-top:1px solid var(--dsw-alias-border-l3);padding-top:14px}.VeQVAa_sectionHeader{align-items:center;gap:6px;min-width:0;display:flex}.VeQVAa_sectionToggle{min-width:0;color:inherit;font:inherit;cursor:pointer;text-align:start;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:flex}.VeQVAa_caret{width:10px;color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px;line-height:12px}.VeQVAa_sectionBody{flex-direction:column;gap:8px;min-width:0;display:flex}.VeQVAa_sectionLabel{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px}.VeQVAa_count{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.VeQVAa_searchToggle{border-radius:var(--dsw-radius-sm);width:20px;height:20px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;flex:none;justify-content:center;align-items:center;margin-left:auto;padding:0;display:flex}.VeQVAa_searchToggle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.VeQVAa_searchToggle:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:1px}.VeQVAa_linkButton{border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-state-business-primary);font:inherit;cursor:pointer;background:0 0;border:none;margin-left:auto;padding:2px 6px;font-size:12px}.VeQVAa_linkButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VeQVAa_announcementText{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;padding:8px 10px;font-size:13px;line-height:20px}.VeQVAa_editor{flex-direction:column;gap:8px;display:flex}.VeQVAa_textarea{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;background:0 0;padding:8px 10px;font-size:13px;line-height:20px}.VeQVAa_textarea:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}.VeQVAa_editorFooter{align-items:center;gap:6px;display:flex}.VeQVAa_editorFooter .VeQVAa_hint{margin-right:auto}.VeQVAa_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:16px}.VeQVAa_unavailable{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.VeQVAa_error{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-primary));margin:0;font-size:12px;line-height:16px}.VeQVAa_notice{color:var(--dsw-alias-label-tertiary);margin:0;padding:16px 12px;font-size:12px;line-height:18px}.VeQVAa_search{min-width:0}.VeQVAa_list{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.VeQVAa_row{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;min-width:0;padding:5px 4px;display:flex}.VeQVAa_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.VeQVAa_name{min-width:0;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:13px;line-height:18px;overflow:hidden}.VeQVAa_fileButton{width:100%;color:inherit;font:inherit;text-align:start;cursor:pointer;border-radius:var(--dsw-alias-border-radius-s);background:0 0;border:0;align-items:center;gap:8px;padding:0;display:flex}.VeQVAa_fileButton:hover .VeQVAa_name{text-decoration:underline}.VeQVAa_filePicker{display:none}.VeQVAa_crumbs{flex-wrap:wrap;align-items:center;gap:2px;min-width:0;display:flex}.VeQVAa_crumbGroup{align-items:center;gap:2px;min-width:0;display:inline-flex}.VeQVAa_crumb{text-overflow:ellipsis;white-space:nowrap;max-width:120px;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;padding:0 2px;font-size:11px;overflow:hidden}.VeQVAa_crumb:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.VeQVAa_crumbSep{color:var(--dsw-alias-label-tertiary);font-size:11px}.VeQVAa_refresh{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;margin-inline-start:auto;padding:0 4px;font-size:12px;line-height:16px}.VeQVAa_refresh:hover{color:var(--dsw-alias-label-primary)}.VeQVAa_preview{border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-alias-border-radius-m);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base));margin-top:8px;overflow:hidden}.VeQVAa_previewHeader{border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:6px 8px;display:flex}.VeQVAa_previewTitle{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;font-size:12px;font-weight:500;overflow:hidden}.VeQVAa_previewClose{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;flex:none;padding:0 4px;font-size:14px;line-height:16px}.VeQVAa_previewClose:hover{color:var(--dsw-alias-label-primary)}.VeQVAa_previewImage{border-radius:var(--dsw-alias-border-radius-s);background:var(--dsw-alias-bg-base);max-width:100%;height:auto;display:block}.VeQVAa_previewBody{max-height:240px;font-family:var(--dsw-alias-font-family-mono,monospace);white-space:pre-wrap;word-break:break-word;margin:0;padding:8px;font-size:11px;line-height:16px;overflow:auto}.VeQVAa_fileIcon{text-align:center;flex:none;width:16px;font-size:12px;line-height:16px}.VeQVAa_fileSize{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.VeQVAa_remove{border-radius:var(--dsw-radius-sm);width:20px;height:20px;color:var(--dsw-alias-label-tertiary);opacity:0;cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background .12s,color .12s;display:inline-flex}.VeQVAa_row:hover .VeQVAa_remove,.VeQVAa_remove:focus-visible{opacity:1}.VeQVAa_remove:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary)}.VeQVAa_selfTag{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);flex:none;padding:0 5px;font-size:11px;line-height:16px}.VeQVAa_ownerTag{border:1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary,#f7ad31) 40%, transparent);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-state-warning-primary,#f7ad31);flex:none;padding:0 5px;font-size:11px;line-height:16px}";
2952
+ const tagId$1 = "@sparkelf/dsh-group-chat/MembersPanel.module.css";
2953
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
2954
+ const tag = document.createElement("style");
2955
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
2956
+ tag.dataset.pluginCss = tagId$1;
2957
+ tag.textContent = css$1;
2958
+ document.head.appendChild(tag);
2959
+ }
2960
+ var MembersPanel_module_css_default = {
2961
+ "announcementText": "VeQVAa_announcementText",
2962
+ "notice": "VeQVAa_notice",
2963
+ "name": "VeQVAa_name",
2964
+ "previewClose": "VeQVAa_previewClose",
2965
+ "unavailable": "VeQVAa_unavailable",
2966
+ "crumbGroup": "VeQVAa_crumbGroup",
2967
+ "textarea": "VeQVAa_textarea",
2968
+ "search": "VeQVAa_search",
2969
+ "linkButton": "VeQVAa_linkButton",
2970
+ "fileButton": "VeQVAa_fileButton",
2971
+ "previewImage": "VeQVAa_previewImage",
2972
+ "section": "VeQVAa_section",
2973
+ "sectionToggle": "VeQVAa_sectionToggle",
2974
+ "fileSize": "VeQVAa_fileSize",
2975
+ "crumbSep": "VeQVAa_crumbSep",
2976
+ "selfTag": "VeQVAa_selfTag",
2977
+ "ownerTag": "VeQVAa_ownerTag",
2978
+ "sectionBody": "VeQVAa_sectionBody",
2979
+ "preview": "VeQVAa_preview",
2980
+ "previewTitle": "VeQVAa_previewTitle",
2981
+ "sectionHeader": "VeQVAa_sectionHeader",
2982
+ "remove": "VeQVAa_remove",
2983
+ "editor": "VeQVAa_editor",
2984
+ "error": "VeQVAa_error",
2985
+ "caret": "VeQVAa_caret",
2986
+ "count": "VeQVAa_count",
2987
+ "crumb": "VeQVAa_crumb",
2988
+ "row": "VeQVAa_row",
2989
+ "filePicker": "VeQVAa_filePicker",
2990
+ "refresh": "VeQVAa_refresh",
2991
+ "previewHeader": "VeQVAa_previewHeader",
2992
+ "editorFooter": "VeQVAa_editorFooter",
2993
+ "root": "VeQVAa_root",
2994
+ "previewBody": "VeQVAa_previewBody",
2995
+ "fileIcon": "VeQVAa_fileIcon",
2996
+ "sectionLabel": "VeQVAa_sectionLabel",
2997
+ "hint": "VeQVAa_hint",
2998
+ "searchToggle": "VeQVAa_searchToggle",
2999
+ "list": "VeQVAa_list",
3000
+ "crumbs": "VeQVAa_crumbs"
3001
+ };
3002
+ //#endregion
3003
+ //#region src/client/MembersPanel.tsx
3004
+ /**
3005
+ * The members panel, shown in the right sidebar beside a room's conversation.
3006
+ *
3007
+ * A room is a Session, so the shell already draws its messages; what the shell has no reason to
3008
+ * know about is the roster behind them. This panel is that missing view: who is in the room, who
3009
+ * owns it, and the one message every member is meant to see.
3010
+ *
3011
+ * It is a **tab in the official right sidebar**, not a dialog. A roster is something a reader
3012
+ * consults while reading — opening it must not cover the conversation it describes — and the
3013
+ * shell already owns tab placement, width, persistence and the ability to put it side by side
3014
+ * with the document preview. Drawing our own overlay would have to track all of that.
3015
+ *
3016
+ * What the panel shows comes from the shared store rather than its own requests: the sidebar
3017
+ * already reads the roster, and a second reader would let the two disagree about who is present.
3018
+ * The one exception is writing the announcement, which no other surface can do.
3019
+ *
3020
+ * @module @sparkelf/dsh-group-chat/client/MembersPanel
3021
+ */
3022
+ /** The tab type this panel is opened as. */
3023
+ const MEMBERS_TAB_KIND = "groupChatMembers";
3024
+ /**
3025
+ * Render the roster and announcement for the room this Session belongs to.
3026
+ *
3027
+ * @param props - the Session, copy, and shared store.
3028
+ * @returns the panel, or a short notice when this Session is not a room.
3029
+ */
3030
+ function MembersPanel(props) {
3031
+ const { sessionId, t, store } = props;
3032
+ const roomId = roomIdOfSession({ id: sessionId });
3033
+ const state = useRoomState(store);
3034
+ (0, react.useEffect)(() => {
3035
+ if (roomId !== void 0) store.loadMembers(roomId);
3036
+ }, [roomId, store]);
3037
+ const members = roomId === void 0 ? void 0 : state.members[roomId];
3038
+ const rows = (0, react.useMemo)(() => members === void 0 ? void 0 : memberRows(members, state.selfId), [members, state.selfId]);
3039
+ const room = state.rooms?.find((candidate) => candidate.id === roomId);
3040
+ if (roomId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3041
+ className: MembersPanel_module_css_default["notice"],
3042
+ children: t("membersNotARoom")
3043
+ });
3044
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3045
+ className: MembersPanel_module_css_default["root"],
3046
+ children: [
3047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Announcement, {
3048
+ t,
3049
+ roomId,
3050
+ announcement: room?.announcement,
3051
+ canEdit: room !== void 0 && room.ownerUserId === state.selfId,
3052
+ onSaved: () => {
3053
+ store.refresh();
3054
+ }
3055
+ }),
3056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SharedFiles, {
3057
+ t,
3058
+ roomId
3059
+ }),
3060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Roster, {
3061
+ t,
3062
+ rows,
3063
+ canRemove: room !== void 0 && room.ownerUserId === state.selfId,
3064
+ onRemove: (userId) => {
3065
+ store.removeMember(roomId, userId);
3066
+ }
3067
+ })
3068
+ ]
3069
+ });
3070
+ }
3071
+ /**
3072
+ * Subscribe to the shared store.
3073
+ *
3074
+ * Written as a hook rather than read inline so a roster that arrives after the first paint
3075
+ * re-renders the panel: the store is a plain observable, not a framework store.
3076
+ *
3077
+ * @param store - the shared room store.
3078
+ * @returns its current state, re-read on every change.
3079
+ */
3080
+ function useRoomState(store) {
3081
+ const [snapshot, setSnapshot] = (0, react.useState)(() => store.getState());
3082
+ (0, react.useEffect)(() => {
3083
+ setSnapshot(store.getState());
3084
+ return store.subscribe(() => {
3085
+ setSnapshot(store.getState());
3086
+ });
3087
+ }, [store]);
3088
+ return snapshot;
3089
+ }
3090
+ /**
3091
+ * The announcement area: read it, and for the owner, write it.
3092
+ *
3093
+ * Saving re-reads the room list instead of patching local state. The server stores a cleared
3094
+ * announcement as `null`, so a local patch would leave this panel and the sidebar showing
3095
+ * different spellings of the same absence.
3096
+ *
3097
+ * @param props - the text, the permission, and how to report a save.
3098
+ * @returns the section.
3099
+ */
3100
+ function Announcement(props) {
3101
+ const { t, roomId, announcement, canEdit, onSaved } = props;
3102
+ const view = announcementView(announcement);
3103
+ const [editing, setEditing] = (0, react.useState)(false);
3104
+ const [draft, setDraft] = (0, react.useState)("");
3105
+ const [saving, setSaving] = (0, react.useState)(false);
3106
+ const [missing, setMissing] = (0, react.useState)(false);
3107
+ const [failure, setFailure] = (0, react.useState)(void 0);
3108
+ const begin = (0, react.useCallback)(() => {
3109
+ setDraft(view.state === "text" ? view.text : "");
3110
+ setFailure(void 0);
3111
+ setEditing(true);
3112
+ }, [view]);
3113
+ const save = (0, react.useCallback)(async () => {
3114
+ setSaving(true);
3115
+ setFailure(void 0);
3116
+ try {
3117
+ await setAnnouncement(roomId, draft.trim());
3118
+ setEditing(false);
3119
+ onSaved();
3120
+ } catch (error) {
3121
+ if (announcementMissing(error)) setMissing(true);
3122
+ else setFailure("announcementFailed");
3123
+ } finally {
3124
+ setSaving(false);
3125
+ }
3126
+ }, [
3127
+ draft,
3128
+ onSaved,
3129
+ roomId
3130
+ ]);
3131
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3132
+ t,
3133
+ title: t("announcementTitle"),
3134
+ actions: canEdit && !editing && !missing && view.state !== "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3135
+ type: "button",
3136
+ className: MembersPanel_module_css_default["linkButton"],
3137
+ onClick: begin,
3138
+ children: view.state === "empty" ? t("announcementAdd") : t("announcementEdit")
3139
+ }) : void 0,
3140
+ children: [missing || view.state === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3141
+ className: MembersPanel_module_css_default["unavailable"],
3142
+ children: t("announcementUnavailable")
3143
+ }) : editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3144
+ className: MembersPanel_module_css_default["editor"],
3145
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3146
+ className: MembersPanel_module_css_default["textarea"],
3147
+ value: draft,
3148
+ placeholder: t("announcementPlaceholder"),
3149
+ onChange: (event) => {
3150
+ setDraft(event.target.value);
3151
+ },
3152
+ onKeyDown: (event) => {
3153
+ if (event.key === "Escape") {
3154
+ event.preventDefault();
3155
+ setEditing(false);
3156
+ return;
3157
+ }
3158
+ if (event.key === "Enter" && !event.shiftKey) {
3159
+ event.preventDefault();
3160
+ save();
3161
+ }
3162
+ },
3163
+ rows: 4
3164
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3165
+ className: MembersPanel_module_css_default["editorFooter"],
3166
+ children: [
3167
+ saving ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3168
+ className: MembersPanel_module_css_default["hint"],
3169
+ children: t("announcementSaving")
3170
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3171
+ className: MembersPanel_module_css_default["hint"],
3172
+ children: t("announcementCleared")
3173
+ }),
3174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3175
+ size: "sm",
3176
+ onClick: () => {
3177
+ setEditing(false);
3178
+ },
3179
+ children: t("cancel")
3180
+ }),
3181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3182
+ size: "sm",
3183
+ variant: "primary",
3184
+ onClick: () => void save(),
3185
+ disabled: saving,
3186
+ children: t("save")
3187
+ })
3188
+ ]
3189
+ })]
3190
+ }) : view.state === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3191
+ className: MembersPanel_module_css_default["announcementText"],
3192
+ children: view.text
3193
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3194
+ className: MembersPanel_module_css_default["unavailable"],
3195
+ children: canEdit ? t("announcementEmpty") : t("announcementOwnerOnly")
3196
+ }), failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3197
+ className: MembersPanel_module_css_default["error"],
3198
+ children: t(failure)
3199
+ })]
3200
+ });
3201
+ }
3202
+ /**
3203
+ * A collapsible section header.
3204
+ *
3205
+ * The right sidebar can be dragged narrow, and this panel stacks three sections in it. A
3206
+ * section that stays open at 200px squeezes the roster until names collide, so each section
3207
+ * can collapse to its title — the title row is the thinnest thing that still tells a member
3208
+ * what exists below. Collapsed state is local: a member who folded "shared files" to make
3209
+ * room for the roster is describing what they are looking at now, not a preference to
3210
+ * persist across rooms.
3211
+ *
3212
+ * @param props - visible title, optional count, and the body to hide.
3213
+ * @returns the section.
3214
+ */
3215
+ function CollapsibleSection(props) {
3216
+ const { title, count, defaultOpen = true, actions, t, children } = props;
3217
+ const [open, setOpen] = (0, react.useState)(defaultOpen);
3218
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3219
+ className: MembersPanel_module_css_default["section"],
3220
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3221
+ className: MembersPanel_module_css_default["sectionHeader"],
3222
+ children: [
3223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3224
+ type: "button",
3225
+ className: MembersPanel_module_css_default["sectionToggle"],
3226
+ "aria-expanded": open,
3227
+ onClick: () => {
3228
+ setOpen((value) => !value);
3229
+ },
3230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3231
+ className: MembersPanel_module_css_default["caret"],
3232
+ "aria-hidden": "true",
3233
+ children: open ? "▾" : "▸"
3234
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3235
+ className: MembersPanel_module_css_default["sectionLabel"],
3236
+ children: title
3237
+ })]
3238
+ }),
3239
+ count !== void 0 && count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3240
+ className: MembersPanel_module_css_default["count"],
3241
+ children: count
3242
+ }),
3243
+ actions
3244
+ ]
3245
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3246
+ className: MembersPanel_module_css_default["sectionBody"],
3247
+ children
3248
+ })]
3249
+ });
3250
+ }
3251
+ /**
3252
+ * The room's shared folder, as the members panel shows it.
3253
+ *
3254
+ * Read on demand rather than kept in the shared store: the folder changes when someone
3255
+ * uploads or when the room's agent writes a file, and a panel that only refreshes on room
3256
+ * switch would show a stale list for the whole time it is open. Re-reading each time the
3257
+ * panel opens keeps the list honest without a poll of its own.
3258
+ *
3259
+ * @param props - the room and copy.
3260
+ * @returns the section.
3261
+ */
3262
+ function SharedFiles(props) {
3263
+ const { t, roomId } = props;
3264
+ const [state, setState] = (0, react.useState)(void 0);
3265
+ const [preview, setPreview] = (0, react.useState)(void 0);
3266
+ const [previewFailure, setPreviewFailure] = (0, react.useState)(void 0);
3267
+ const [uploading, setUploading] = (0, react.useState)(false);
3268
+ const [uploadFailure, setUploadFailure] = (0, react.useState)(void 0);
3269
+ const [reloadKey, setReloadKey] = (0, react.useState)(0);
3270
+ const [subPath, setSubPath] = (0, react.useState)("");
3271
+ const fileInput = (0, react.useRef)(null);
3272
+ (0, react.useEffect)(() => {
3273
+ let live = true;
3274
+ listRoomFiles(roomId, subPath).then((next) => {
3275
+ if (live) setState(next);
3276
+ }).catch(() => {
3277
+ if (live) setState({
3278
+ path: null,
3279
+ files: []
3280
+ });
3281
+ });
3282
+ return () => {
3283
+ live = false;
3284
+ };
3285
+ }, [
3286
+ roomId,
3287
+ reloadKey,
3288
+ subPath
3289
+ ]);
3290
+ if (state === void 0) return null;
3291
+ const crumbs = state.crumbs ?? [];
3292
+ if (state.path === null) return null;
3293
+ const openPreview = (path) => {
3294
+ setPreviewFailure(void 0);
3295
+ getRoomFile(roomId, path).then((next) => {
3296
+ setPreview(next);
3297
+ }).catch(() => {
3298
+ setPreviewFailure(t("membersFileUnreadable"));
3299
+ });
3300
+ };
3301
+ const upload = (picked) => {
3302
+ setUploading(true);
3303
+ setUploadFailure(void 0);
3304
+ uploadRoomFile(roomId, picked).then(() => {
3305
+ setReloadKey((value) => value + 1);
3306
+ }).catch(() => {
3307
+ setUploadFailure(t("membersFileUploadFailed"));
3308
+ }).finally(() => {
3309
+ setUploading(false);
3310
+ if (fileInput.current !== null) fileInput.current.value = "";
3311
+ });
3312
+ };
3313
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3314
+ t,
3315
+ title: t("membersFilesTitle"),
3316
+ count: state.files.length,
3317
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3319
+ ref: fileInput,
3320
+ type: "file",
3321
+ className: MembersPanel_module_css_default["filePicker"],
3322
+ "aria-label": t("membersFileUpload"),
3323
+ onChange: (event) => {
3324
+ const picked = event.target.files?.[0];
3325
+ if (picked !== void 0) upload(picked);
3326
+ }
3327
+ }),
3328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3329
+ type: "button",
3330
+ className: MembersPanel_module_css_default["refresh"],
3331
+ "aria-label": t("membersFileUpload"),
3332
+ title: t("membersFileUpload"),
3333
+ disabled: uploading,
3334
+ onClick: () => {
3335
+ fileInput.current?.click();
3336
+ },
3337
+ children: uploading ? "⋯" : "+"
3338
+ }),
3339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3340
+ type: "button",
3341
+ className: MembersPanel_module_css_default["refresh"],
3342
+ "aria-label": t("membersFilesRefresh"),
3343
+ title: t("membersFilesRefresh"),
3344
+ onClick: () => {
3345
+ setReloadKey((value) => value + 1);
3346
+ },
3347
+ children: "↻"
3348
+ })
3349
+ ] }),
3350
+ children: [
3351
+ crumbs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
3352
+ className: MembersPanel_module_css_default["crumbs"],
3353
+ "aria-label": t("membersFilesTitle"),
3354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3355
+ type: "button",
3356
+ className: MembersPanel_module_css_default["crumb"],
3357
+ onClick: () => {
3358
+ setSubPath("");
3359
+ },
3360
+ children: t("membersFilesRoot")
3361
+ }), crumbs.map((crumb) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3362
+ className: MembersPanel_module_css_default["crumbGroup"],
3363
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3364
+ className: MembersPanel_module_css_default["crumbSep"],
3365
+ "aria-hidden": "true",
3366
+ children: "/"
3367
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3368
+ type: "button",
3369
+ className: MembersPanel_module_css_default["crumb"],
3370
+ onClick: () => {
3371
+ setSubPath(crumb.path);
3372
+ },
3373
+ children: crumb.name
3374
+ })]
3375
+ }, crumb.path))]
3376
+ }),
3377
+ state.files.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3378
+ className: MembersPanel_module_css_default["hint"],
3379
+ children: t("membersFilesEmpty")
3380
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3381
+ className: MembersPanel_module_css_default["list"],
3382
+ children: state.files.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
3383
+ className: MembersPanel_module_css_default["row"],
3384
+ children: file.directory ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3385
+ type: "button",
3386
+ className: MembersPanel_module_css_default["fileButton"],
3387
+ onClick: () => {
3388
+ setSubPath(joinPath(subPath, file.path));
3389
+ },
3390
+ title: file.path,
3391
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3392
+ className: MembersPanel_module_css_default["fileIcon"],
3393
+ "aria-hidden": "true",
3394
+ children: "📁"
3395
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3396
+ className: MembersPanel_module_css_default["name"],
3397
+ children: file.name
3398
+ })]
3399
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3400
+ type: "button",
3401
+ className: MembersPanel_module_css_default["fileButton"],
3402
+ onClick: () => {
3403
+ openPreview(joinPath(subPath, file.path));
3404
+ },
3405
+ title: file.path,
3406
+ children: [
3407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3408
+ className: MembersPanel_module_css_default["fileIcon"],
3409
+ "aria-hidden": "true",
3410
+ children: "📄"
3411
+ }),
3412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3413
+ className: MembersPanel_module_css_default["name"],
3414
+ children: file.name
3415
+ }),
3416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3417
+ className: MembersPanel_module_css_default["fileSize"],
3418
+ children: formatBytes(file.size)
3419
+ })
3420
+ ]
3421
+ })
3422
+ }, file.path))
3423
+ }),
3424
+ previewFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3425
+ className: MembersPanel_module_css_default["hint"],
3426
+ children: previewFailure
3427
+ }),
3428
+ uploadFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3429
+ className: MembersPanel_module_css_default["error"],
3430
+ children: uploadFailure
3431
+ }),
3432
+ preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3433
+ className: MembersPanel_module_css_default["preview"],
3434
+ "aria-label": preview.name,
3435
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3436
+ className: MembersPanel_module_css_default["previewHeader"],
3437
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3438
+ className: MembersPanel_module_css_default["previewTitle"],
3439
+ title: preview.path,
3440
+ children: preview.name
3441
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3442
+ type: "button",
3443
+ className: MembersPanel_module_css_default["previewClose"],
3444
+ "aria-label": t("membersFileClose"),
3445
+ onClick: () => {
3446
+ setPreview(void 0);
3447
+ },
3448
+ children: "×"
3449
+ })]
3450
+ }), preview.image !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3451
+ className: MembersPanel_module_css_default["previewImage"],
3452
+ src: preview.image,
3453
+ alt: preview.name
3454
+ }) : preview.textLike ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
3455
+ className: MembersPanel_module_css_default["previewBody"],
3456
+ children: preview.text
3457
+ }), preview.truncated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3458
+ className: MembersPanel_module_css_default["hint"],
3459
+ children: t("membersFileTruncated")
3460
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3461
+ className: MembersPanel_module_css_default["hint"],
3462
+ children: t("membersFileBinary")
3463
+ })]
3464
+ })
3465
+ ]
3466
+ });
3467
+ }
3468
+ /** Joins a folder-relative path onto the folder currently shown. */
3469
+ const joinPath = (prefix, name) => prefix === "" ? name : `${prefix}/${name}`;
3470
+ /** Bytes as a short human string; the panel has one column, not a table. */
3471
+ const formatBytes = (bytes) => {
3472
+ if (bytes < 1024) return `${String(bytes)} B`;
3473
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(0)} KB`;
3474
+ return `${(bytes / 1048576).toFixed(1)} MB`;
3475
+ };
3476
+ /**
3477
+ * The roster: a search box and one row per member.
3478
+ *
3479
+ * The search box appears only once the roster is long enough that scanning is slower than
3480
+ * typing. Below that it would be a control that cannot help.
3481
+ *
3482
+ * @param props - the rows and copy.
3483
+ * @returns the section.
3484
+ */
3485
+ function Roster(props) {
3486
+ const { t, rows, canRemove, onRemove } = props;
3487
+ const [query, setQuery] = (0, react.useState)("");
3488
+ const [searching, setSearching] = (0, react.useState)(false);
3489
+ const visible = (0, react.useMemo)(() => rows === void 0 ? void 0 : filterMemberRows(rows, query), [rows, query]);
3490
+ const closeSearch = () => {
3491
+ setSearching(false);
3492
+ setQuery("");
3493
+ };
3494
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3495
+ t,
3496
+ title: t("membersRosterTitle"),
3497
+ count: rows?.length,
3498
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3499
+ type: "button",
3500
+ className: MembersPanel_module_css_default["searchToggle"],
3501
+ "aria-label": searching ? t("membersSearchClear") : t("membersSearchOpen"),
3502
+ "aria-expanded": searching,
3503
+ title: searching ? t("membersSearchClear") : t("membersSearchOpen"),
3504
+ onClick: () => {
3505
+ if (searching) closeSearch();
3506
+ else setSearching(true);
3507
+ },
3508
+ children: searching ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3509
+ width: "14",
3510
+ height: "14",
3511
+ viewBox: "0 0 16 16",
3512
+ fill: "none",
3513
+ stroke: "currentColor",
3514
+ strokeWidth: "1.6",
3515
+ strokeLinecap: "round",
3516
+ "aria-hidden": "true",
3517
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
3518
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3519
+ width: "14",
3520
+ height: "14",
3521
+ viewBox: "0 0 16 16",
3522
+ fill: "none",
3523
+ stroke: "currentColor",
3524
+ strokeWidth: "1.6",
3525
+ strokeLinecap: "round",
3526
+ "aria-hidden": "true",
3527
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3528
+ cx: "7",
3529
+ cy: "7",
3530
+ r: "4.5"
3531
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10.5 10.5L14 14" })]
3532
+ })
3533
+ }),
3534
+ children: [searching && rows !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3535
+ className: MembersPanel_module_css_default["search"],
3536
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
3537
+ value: query,
3538
+ autoFocus: true,
3539
+ placeholder: t("membersSearchPlaceholder"),
3540
+ "aria-label": t("membersSearch"),
3541
+ onChange: (event) => {
3542
+ setQuery(event.target.value);
3543
+ }
3544
+ })
3545
+ }), visible === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3546
+ className: MembersPanel_module_css_default["hint"],
3547
+ children: t("loading")
3548
+ }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3549
+ className: MembersPanel_module_css_default["hint"],
3550
+ children: rows !== void 0 && rows.length > 0 ? t("membersSearchEmpty") : t("membersEmpty")
3551
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3552
+ className: MembersPanel_module_css_default["list"],
3553
+ children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
3554
+ className: MembersPanel_module_css_default["row"],
3555
+ children: [
3556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
3557
+ name: row.name,
3558
+ src: row.avatarUrl,
3559
+ size: "md"
3560
+ }),
3561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3562
+ className: MembersPanel_module_css_default["name"],
3563
+ title: row.name,
3564
+ children: row.name
3565
+ }),
3566
+ row.self && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3567
+ className: MembersPanel_module_css_default["selfTag"],
3568
+ children: t("membersSelf")
3569
+ }),
3570
+ row.owner && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3571
+ className: MembersPanel_module_css_default["ownerTag"],
3572
+ children: t("membersOwner")
3573
+ }),
3574
+ canRemove && !row.self && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3575
+ type: "button",
3576
+ className: MembersPanel_module_css_default["remove"],
3577
+ title: t("removeMember"),
3578
+ "aria-label": t("removeMember"),
3579
+ onClick: () => {
3580
+ onRemove(row.userId);
3581
+ },
3582
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3583
+ width: "14",
3584
+ height: "14",
3585
+ viewBox: "0 0 16 16",
3586
+ fill: "none",
3587
+ stroke: "currentColor",
3588
+ strokeWidth: "1.6",
3589
+ strokeLinecap: "round",
3590
+ "aria-hidden": "true",
3591
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 4.5h10M6.5 4.5V3h3v1.5M5 4.5l.6 8a1 1 0 0 0 1 1h2.8a1 1 0 0 0 1-1l.6-8" })
3592
+ })
3593
+ })
3594
+ ]
3595
+ }, row.userId))
3596
+ })]
3597
+ });
3598
+ }
3599
+ /**
3600
+ * The chip that labels the members tab in the sidebar's tab strip.
3601
+ *
3602
+ * The sidebar calls this separately from the body so the strip can render without mounting every
3603
+ * placed tab; it therefore needs the label alone, and none of the body's data.
3604
+ *
3605
+ * @param props - the localized copy.
3606
+ * @returns the tab's label.
3607
+ */
3608
+ function MembersTabTitle(props) {
3609
+ return props.t("membersTitle");
3610
+ }
3611
+ //#endregion
3612
+ //#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/GuideEntry.module.css.mjs
3613
+ const css = "._0Vnsuq_entry{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-bg-layer-1);width:380px;max-width:100%;min-height:56px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;align-items:center;gap:14px;padding:14px 20px;display:flex}._0Vnsuq_entry:hover{background:var(--dsw-alias-interactive-bg-hover)}._0Vnsuq_icon{width:26px;height:26px;color:var(--dsw-alias-label-secondary);flex:none;justify-content:center;align-items:center;display:flex}._0Vnsuq_text{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}._0Vnsuq_title{text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:1.4;overflow:hidden}._0Vnsuq_description{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:1.4;overflow:hidden}";
3614
+ const tagId = "@sparkelf/dsh-group-chat/GuideEntry.module.css";
3615
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
3616
+ const tag = document.createElement("style");
3617
+ tag.dataset.plugin = "@sparkelf/dsh-group-chat";
3618
+ tag.dataset.pluginCss = tagId;
3619
+ tag.textContent = css;
3620
+ document.head.appendChild(tag);
3621
+ }
3622
+ var GuideEntry_module_css_default = {
3623
+ "title": "_0Vnsuq_title",
3624
+ "text": "_0Vnsuq_text",
3625
+ "icon": "_0Vnsuq_icon",
3626
+ "description": "_0Vnsuq_description",
3627
+ "entry": "_0Vnsuq_entry"
3628
+ };
3629
+ //#endregion
3630
+ //#region src/client/GuideEntry.tsx
3631
+ /**
3632
+ * Draw the members box, or nothing when the open conversation is not a room.
3633
+ *
3634
+ * @param props - the box's copy, its Session, and how to open the page.
3635
+ * @returns the box, or `null` outside a room.
3636
+ */
3637
+ function MembersGuideEntry(props) {
3638
+ const { title, description, sessionId, onPick } = props;
3639
+ if (roomIdOfSession({ id: sessionId }) === void 0) return null;
3640
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3641
+ type: "button",
3642
+ className: GuideEntry_module_css_default["entry"],
3643
+ "data-group-chat-guide-entry": props.entryId,
3644
+ onClick: () => {
3645
+ onPick?.();
3646
+ },
3647
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3648
+ className: GuideEntry_module_css_default["icon"],
3649
+ "aria-hidden": "true",
3650
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3651
+ width: "26",
3652
+ height: "26",
3653
+ viewBox: "0 0 24 24",
3654
+ fill: "none",
3655
+ stroke: "currentColor",
3656
+ strokeWidth: "1.7",
3657
+ strokeLinecap: "round",
3658
+ strokeLinejoin: "round",
3659
+ children: [
3660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
3661
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3662
+ cx: "9",
3663
+ cy: "7",
3664
+ r: "4"
3665
+ }),
3666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
3667
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
3668
+ ]
3669
+ })
3670
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3671
+ className: GuideEntry_module_css_default["text"],
3672
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3673
+ className: GuideEntry_module_css_default["title"],
3674
+ children: title
3675
+ }), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3676
+ className: GuideEntry_module_css_default["description"],
3677
+ children: description
3678
+ })]
3679
+ })]
3680
+ });
3681
+ }
3682
+ //#endregion
3683
+ //#region src/client/locales.ts
3684
+ /**
3685
+ * Group-chat copy, in the shape every locale-backed DSH client plugin uses: one
3686
+ * English object is the key set, and each other locale is a complete record over
3687
+ * that key set so a missing translation fails to compile.
3688
+ *
3689
+ * @module @sparkelf/dsh-group-chat/client/locales
3690
+ */
3691
+ /** English group-chat copy. */
3692
+ const en = {
3693
+ nav: "Group chat",
3694
+ title: "Group chat",
3695
+ description: "Rooms this workspace has joined. @ yourself to make the agent answer; every other message is recorded and read, but nothing runs.",
3696
+ loading: "Loading rooms…",
3697
+ retry: "Retry",
3698
+ emptyRooms: "No rooms yet",
3699
+ createRoom: "New room",
3700
+ roomName: "Room name",
3701
+ create: "Create",
3702
+ search: "Search rooms",
3703
+ searchPlaceholder: "Search rooms…",
3704
+ searchClear: "Clear search",
3705
+ searchEmpty: "No room matches",
3706
+ discoverHeading: "Rooms you can join",
3707
+ discoverMembers: "{count} members",
3708
+ joinRoom: "Join",
3709
+ joining: "Joining…",
3710
+ viewOptions: "View options",
3711
+ orderBy: "Sort",
3712
+ orderByUpdated: "Recently active",
3713
+ orderByManual: "Manual",
3714
+ archivedFilter: "Left rooms",
3715
+ archivedHide: "Hide",
3716
+ archivedShow: "Show",
3717
+ copyRoomId: "Copy room ID",
3718
+ left: "Left",
3719
+ memberCountShort: "{count}",
3720
+ unreadAria: "{count} unread",
3721
+ unreadMentionAria: "{count} unread, one mentions you",
3722
+ createTitle: "New room",
3723
+ createName: "Room name",
3724
+ createCwd: "Working directory",
3725
+ createCwdHint: "Determines which files the agent may change. Members of this room can ask the agent to modify anything under this directory.",
3726
+ createPreset: "Agent preset",
3727
+ createPresetHint: "Determines which tools the agent has in this room.",
3728
+ createPresetDefault: "The deployment's default preset",
3729
+ createPresetUnavailable: "The preset list is unavailable; the room will use the deployment default.",
3730
+ createMembers: "Initial members",
3731
+ createMembersHint: "You become the first member. Others can be added after the room exists.",
3732
+ createPolicy: "Who may invite",
3733
+ createVisibility: "Who can find this room",
3734
+ createTrigger: "When the agent replies",
3735
+ createNotice: "These settings control where the room appears in the interface. They are not access control.",
3736
+ menuRename: "Rename",
3737
+ menuInvite: "Invite members",
3738
+ menuSettings: "Room settings",
3739
+ menuMembers: "Manage members",
3740
+ menuTransfer: "Transfer ownership",
3741
+ menuTransferTitle: "Transfer ownership",
3742
+ menuTransferHint: "Choose the member who becomes the owner. You become an ordinary member and can then leave.",
3743
+ menuTransferConfirm: "Transfer",
3744
+ menuTransferEmpty: "There is no other member to transfer to yet. Invite someone first.",
3745
+ menuOwnerCannotLeave: "The owner cannot leave. Transfer ownership, or dissolve the room.",
3746
+ menuDissolve: "Dissolve room",
3747
+ menuDissolveTarget: "Room to dissolve: ",
3748
+ menuDissolveConfirm: "Dissolve this room? Every member loses it, and its messages go with it. This cannot be undone.",
3749
+ menuLeave: "Leave room",
3750
+ menuLeaveConfirm: "Leave this room? The history your agent already recorded stays.",
3751
+ renameTitle: "Rename room",
3752
+ renamePlaceholder: "New name",
3753
+ membersTitle: "Manage members",
3754
+ /** The roster section's own heading; the tab label above already says "Manage members". */
3755
+ membersRosterTitle: "Members",
3756
+ membersSearch: "Search members",
3757
+ membersSearchOpen: "Search members",
3758
+ membersSearchPlaceholder: "Search members…",
3759
+ membersSearchClear: "Clear search",
3760
+ membersSearchEmpty: "No member matches",
3761
+ membersEmpty: "This room has no members yet.",
3762
+ membersError: "The member list could not be read. Retry from the room menu.",
3763
+ announcementTitle: "Announcement",
3764
+ announcementEmpty: "No announcement yet",
3765
+ announcementAdd: "Add announcement",
3766
+ announcementEdit: "Edit",
3767
+ announcementPlaceholder: "Write something every member should see",
3768
+ announcementUnavailable: "Announcements are unavailable in this deployment",
3769
+ announcementSaving: "Saving…",
3770
+ announcementOwnerOnly: "Only the room owner can edit the announcement",
3771
+ announcementCleared: "The announcement was cleared",
3772
+ announcementFailed: "Could not save the announcement. Try again.",
3773
+ membersSelf: "You",
3774
+ membersFilesTitle: "Shared files",
3775
+ membersFilesEmpty: "Nothing shared yet.",
3776
+ membersFilesRefresh: "Refresh files",
3777
+ membersFileClose: "Close preview",
3778
+ membersFileUnreadable: "Could not open that file.",
3779
+ membersFileTruncated: "Only the beginning is shown.",
3780
+ membersFileBinary: "Binary file — no text preview.",
3781
+ membersSectionCollapsed: "Collapsed",
3782
+ membersFileUpload: "Upload a file",
3783
+ membersFileUploadFailed: "Upload failed.",
3784
+ resizeRegion: "Resize the group chat region",
3785
+ membersFilesRoot: "Files",
3786
+ membersOwner: "Owner",
3787
+ membersNotARoom: "This conversation is not a group chat, so it has no members.",
3788
+ membersGuideHint: "Members and the announcement for this room",
3789
+ inviteTitle: "Invite members",
3790
+ invitePlaceholder: "Search DataOps accounts by name or handle",
3791
+ inviteHint: "Only existing DataOps accounts can be added.",
3792
+ inviteSubmit: "Invite",
3793
+ inviteEmpty: "No matching accounts. They may already be in this room.",
3794
+ inviteSearching: "Searching…",
3795
+ inviteRemove: "Remove",
3796
+ inviteSearchLabel: "Search accounts",
3797
+ inviteSelectedCount: "{count} selected",
3798
+ inviteSubmitCount: "Invite ({count})",
3799
+ optionEmpty: "Nothing to choose from.",
3800
+ pickThisDirectory: "Use this directory",
3801
+ settingsTitle: "Room settings",
3802
+ removeMember: "Remove",
3803
+ ownerBadge: "Owner",
3804
+ you: "You",
3805
+ createSubmit: "Create",
3806
+ cancel: "Cancel",
3807
+ members: "Members",
3808
+ memberCount: "members",
3809
+ invite: "Invite",
3810
+ leave: "Leave room",
3811
+ leaveConfirm: "Leave this room? Your own agent keeps the history it already recorded.",
3812
+ owner: "Owner",
3813
+ sendPlaceholder: "Message the room. @ yourself to have your agent answer.",
3814
+ send: "Send",
3815
+ sending: "Sending…",
3816
+ agentBadge: "agent",
3817
+ triggeredHint: "Your agent is answering.",
3818
+ notConfigured: "The group-chat server is not configured, so no rooms can be listed.",
3819
+ unreachable: "The group-chat server did not answer.",
3820
+ credentialMissing: "No workspace credential is available, so rooms cannot be read.",
3821
+ credentialRejected: "The workspace credential was refused. Sign in to DataOps again, then retry.",
3822
+ ownerCannotLeave: "The owner cannot leave. Transfer ownership, or dissolve the room.",
3823
+ policy: "Who may invite",
3824
+ policyAnyone: "Any member",
3825
+ policyAdmin: "Owner only",
3826
+ discoverability: "Discoverability",
3827
+ discoverable: "Listed in search",
3828
+ inviteOnly: "Invite only",
3829
+ trigger: "When the agent replies",
3830
+ triggerMention: "Only when @-mentioned",
3831
+ triggerAlways: "Every message",
3832
+ triggerNever: "Never (record only)",
3833
+ save: "Save",
3834
+ saved: "Saved"
3835
+ };
3836
+ /** Simplified Chinese group-chat copy. */
3837
+ const zh = {
3838
+ nav: "群聊",
3839
+ title: "群聊",
3840
+ description: "本工作区已加入的群聊。@ 自己会触发 agent 应答;其他消息都会记录并进入上下文,但不执行。",
3841
+ loading: "正在加载群聊…",
3842
+ retry: "重试",
3843
+ emptyRooms: "暂无群聊",
3844
+ createRoom: "新建群聊",
3845
+ roomName: "群聊名称",
3846
+ create: "创建",
3847
+ search: "搜索群聊",
3848
+ searchPlaceholder: "搜索群聊…",
3849
+ searchClear: "清除搜索",
3850
+ searchEmpty: "无匹配结果",
3851
+ discoverHeading: "可以加入的群聊",
3852
+ discoverMembers: "{count} 位成员",
3853
+ joinRoom: "加入",
3854
+ joining: "加入中…",
3855
+ viewOptions: "视图选项",
3856
+ orderBy: "排序",
3857
+ orderByUpdated: "最近活跃",
3858
+ orderByManual: "手动",
3859
+ archivedFilter: "已退出的群聊",
3860
+ archivedHide: "隐藏",
3861
+ archivedShow: "显示",
3862
+ copyRoomId: "复制群聊 ID",
3863
+ left: "已退出",
3864
+ memberCountShort: "{count}",
3865
+ unreadAria: "{count} 条未读",
3866
+ unreadMentionAria: "{count} 条未读,其中有 @ 你",
3867
+ createTitle: "新建群聊",
3868
+ createName: "群聊名称",
3869
+ createCwd: "工作目录",
3870
+ createCwdHint: "决定机器人能操作哪些文件。此群成员可以让机器人修改该目录下的任何文件。",
3871
+ createPreset: "Agent preset",
3872
+ createPresetHint: "决定机器人在本群可用的工具能力。",
3873
+ createPresetDefault: "跟随部署默认预设",
3874
+ createPresetUnavailable: "预设列表暂时读取不到;创建后将使用部署默认预设。",
3875
+ createMembers: "初始成员",
3876
+ createMembersHint: "创建者自动成为第一个成员;其余成员可在建群后邀请。",
3877
+ createPolicy: "谁能邀请成员",
3878
+ createVisibility: "谁能在群里发现这个房间",
3879
+ createTrigger: "机器人何时回复",
3880
+ createNotice: "这些设置控制房间在界面上的可见性,不构成访问控制。",
3881
+ menuRename: "重命名",
3882
+ menuInvite: "邀请成员",
3883
+ menuSettings: "群聊设置",
3884
+ menuMembers: "成员管理",
3885
+ menuTransfer: "转让群主",
3886
+ menuTransferTitle: "转让群主",
3887
+ menuTransferHint: "选择一位成员成为新群主。转让后你会变成普通成员,那时就可以退出群聊。",
3888
+ menuTransferConfirm: "转让",
3889
+ menuTransferEmpty: "目前没有其他成员可以转让,请先邀请成员。",
3890
+ menuOwnerCannotLeave: "群主不能退出群聊。请先转让群主,或解散群聊。",
3891
+ menuDissolve: "解散群聊",
3892
+ menuDissolveTarget: "将要解散群聊:",
3893
+ menuDissolveConfirm: "解散这个群聊?所有成员都会失去它,群消息也会一并删除,且无法恢复。",
3894
+ menuLeave: "退出群聊",
3895
+ menuLeaveConfirm: "退出这个群聊?你自己的 agent 已记录的历史会保留。",
3896
+ renameTitle: "重命名群聊",
3897
+ renamePlaceholder: "新名称",
3898
+ membersTitle: "成员管理",
3899
+ membersRosterTitle: "成员",
3900
+ membersSearch: "搜索成员",
3901
+ membersSearchOpen: "搜索成员",
3902
+ membersSearchPlaceholder: "搜索成员…",
3903
+ membersSearchClear: "清除搜索",
3904
+ membersSearchEmpty: "没有匹配的成员",
3905
+ membersEmpty: "这个群还没有成员。",
3906
+ membersError: "成员列表读取失败,可从群聊菜单重试。",
3907
+ announcementTitle: "群公告",
3908
+ announcementEmpty: "暂无群公告",
3909
+ announcementAdd: "+ 添加公告",
3910
+ announcementEdit: "编辑",
3911
+ announcementPlaceholder: "写一条所有成员都能看到的公告",
3912
+ announcementUnavailable: "公告功能暂不可用",
3913
+ announcementSaving: "保存中…",
3914
+ announcementOwnerOnly: "只有群主可以编辑公告",
3915
+ announcementCleared: "公告已清空",
3916
+ announcementFailed: "公告保存失败,请重试。",
3917
+ membersSelf: "我",
3918
+ membersFilesTitle: "群文件",
3919
+ membersFilesEmpty: "还没有共享文件。",
3920
+ membersFilesRefresh: "刷新文件",
3921
+ membersFileClose: "关闭预览",
3922
+ membersFileUnreadable: "无法打开该文件。",
3923
+ membersFileTruncated: "仅显示开头部分。",
3924
+ membersFileBinary: "二进制文件,不提供文本预览。",
3925
+ membersSectionCollapsed: "已折叠",
3926
+ membersFileUpload: "上传文件",
3927
+ membersFileUploadFailed: "上传失败。",
3928
+ resizeRegion: "拖动调节群聊区域高度",
3929
+ membersFilesRoot: "群文件",
3930
+ membersOwner: "群主",
3931
+ membersNotARoom: "该会话不是群聊,没有群成员。",
3932
+ membersGuideHint: "本群成员与群公告",
3933
+ inviteTitle: "邀请成员",
3934
+ invitePlaceholder: "按姓名或账号搜索 DataOps 用户",
3935
+ inviteHint: "只能加入已存在的 DataOps 账号。",
3936
+ inviteEmpty: "没有匹配的账号,可能已在群里。",
3937
+ inviteSearching: "搜索中…",
3938
+ inviteRemove: "移除",
3939
+ inviteSearchLabel: "搜索账号",
3940
+ inviteSelectedCount: "已选 {count} 人",
3941
+ inviteSubmitCount: "邀请 ({count})",
3942
+ optionEmpty: "没有可选项。",
3943
+ pickThisDirectory: "用这个目录",
3944
+ inviteSubmit: "邀请",
3945
+ settingsTitle: "群聊设置",
3946
+ removeMember: "移除",
3947
+ ownerBadge: "群主",
3948
+ you: "我",
3949
+ createSubmit: "创建",
3950
+ cancel: "取消",
3951
+ members: "成员",
3952
+ memberCount: "位成员",
3953
+ invite: "邀请",
3954
+ leave: "退出群聊",
3955
+ leaveConfirm: "退出这个群聊?你自己的 agent 会保留已记录的历史。",
3956
+ owner: "群主",
3957
+ sendPlaceholder: "发消息。@ 自己可以让你的 agent 应答。",
3958
+ send: "发送",
3959
+ sending: "发送中…",
3960
+ agentBadge: "agent",
3961
+ triggeredHint: "你的 agent 正在应答。",
3962
+ notConfigured: "未配置群聊服务器,无法列出群聊。",
3963
+ unreachable: "群聊服务器没有响应。",
3964
+ credentialMissing: "没有可用的工作区凭证,无法读取群聊。",
3965
+ credentialRejected: "工作区凭证被拒绝。请重新登录 DataOps 后重试。",
3966
+ ownerCannotLeave: "群主不能退出群聊。请先转让群主,或解散群聊。",
3967
+ policy: "谁可以邀请",
3968
+ policyAnyone: "任何成员",
3969
+ policyAdmin: "仅群主",
3970
+ discoverability: "可发现性",
3971
+ discoverable: "可在搜索中列出",
3972
+ inviteOnly: "仅限邀请",
3973
+ trigger: "机器人何时回复",
3974
+ triggerMention: "仅被 @ 时回复",
3975
+ triggerAlways: "每条都回复",
3976
+ triggerNever: "从不回复(纯记录)",
3977
+ save: "保存",
3978
+ saved: "已保存"
3979
+ };
3980
+ //#endregion
3981
+ //#region src/client/index.ts
3982
+ const NS = "settings.groupChat";
3983
+ /**
3984
+ * Every `user`-shaped cell of the chat-node seat.
3985
+ *
3986
+ * `user` and `steering` are separate cells that render the *same* official component: a room
3987
+ * message is `steering` once a turn has claimed it and `user` otherwise, so shadowing only
3988
+ * `user` would leave exactly the messages that arrived while the agent was working on the
3989
+ * right. Both are cell-local shadows, so neither reaches an ordinary Session: the side is
3990
+ * decided from the message's own room source, and an ordinary message has none.
3991
+ */
3992
+ const CHAT_NODE_KEYS = ["user", "steering"];
3993
+ /** Priority this plugin claims its chat-node cells at; the official entries sit at `0`. */
3994
+ const SHADOW_PRIORITY = -1;
3995
+ /**
3996
+ * This plugin's own chat-node renderer, so the lookup below cannot mistake it for the
3997
+ * official one. Compared by identity against the registered component.
3998
+ */
3999
+ const OUR_VIEW = MessageSide;
4000
+ /**
4001
+ * The entry the shell would render for one cell without this plugin.
4002
+ *
4003
+ * The registry projects shadowing winners through `entriesOfSlot`, which is exactly the
4004
+ * wrong question here — this plugin is the winner. `entries` keeps the whole ledger, so the
4005
+ * official entry is still visible, and picking the lowest-priority live one other than this
4006
+ * plugin's own reproduces the pre-plugin choice.
4007
+ *
4008
+ * @param ledger - every registered entry for the chat-node slot.
4009
+ * @param key - the cell being claimed.
4010
+ * @param ours - this plugin's own component, excluded from the candidates.
4011
+ * @returns the official component, or `undefined` when nothing else is registered.
4012
+ */
4013
+ const officialViewFor = (ledger, key, ours) => ledger.filter((entry) => entry.options.key === key && entry.component !== ours).reduce((lowest, entry) => lowest === void 0 || (entry.options.priority ?? 0) < (lowest.options.priority ?? 0) ? entry : lowest, void 0)?.component;
4014
+ /**
4015
+ * Report why one chat-node cell could not be claimed.
4016
+ *
4017
+ * A rejected claim is not fatal — the official renderer still draws that cell — but it does
4018
+ * mean the side never flips, which is invisible in the UI. The console is the only channel
4019
+ * this half has, and the message has to name the cell so the missing side can be traced to it.
4020
+ */
4021
+ const logClaimFailure = (key, error) => {
4022
+ console.warn(`[group-chat] could not claim the "${key}" chat-node cell; messages of this kind stay on the official side`, error);
4023
+ };
4024
+ /** Main-panel key the sidebar rows select. */
4025
+ const PANEL_ID = "group-chat";
4026
+ /**
4027
+ * The members tab type.
4028
+ *
4029
+ * `MEMBERS_TAB_ID` keys the type's slot entries; `MEMBERS_TAB_KIND` is what `openTab` resolves.
4030
+ * They are separate values because the sidebar stores a placed tab by kind while the slot ledger
4031
+ * is keyed by id — the same split the official schedule plugin makes.
4032
+ */
4033
+ const MEMBERS_TAB_ID = "@sparkelf/dsh-group-chat/members";
4034
+ /**
4035
+ * The guide entry's own identity, and where it sorts on the start page.
4036
+ *
4037
+ * An id rather than the tab's: the guide lists one box per entry, and the id is what the
4038
+ * entry-level slot replacement below is keyed by. The order lands the box among the other
4039
+ * `extension` types instead of at either end.
4040
+ */
4041
+ const MEMBERS_GUIDE_ID = "@sparkelf/dsh-group-chat/members-guide";
4042
+ const MEMBERS_GUIDE_ORDER = 60;
4043
+ const inject = [
4044
+ "slots",
4045
+ "locale",
4046
+ "uiWorkspace",
4047
+ "workspaces",
4048
+ "sidebarRightTabs",
4049
+ "sidebarRight"
4050
+ ];
4051
+ /**
4052
+ * Register the sidebar region and the room page.
4053
+ * @param ctx - DSH client context with locale and slot registries.
4054
+ */
4055
+ function apply(ctx) {
4056
+ ctx.effect(() => ctx.locale.register(NS, {
4057
+ zh,
4058
+ en
4059
+ }), "group-chat: copy");
4060
+ const t = ctx.locale.bind(NS);
4061
+ const tPreset = ctx.locale.bind(PRESET_LOCALE_NS);
4062
+ const base = createRoomStore();
4063
+ const store = {
4064
+ ...base,
4065
+ open: (roomId) => {
4066
+ base.open(roomId);
4067
+ ctx.uiWorkspace.openSession(roomSessionId(roomId));
4068
+ queueMicrotask(() => {
4069
+ store.openMembers?.(roomId);
4070
+ });
4071
+ },
4072
+ /**
4073
+ * Dissolve a room, then take its Session off the shell's grouping surfaces.
4074
+ *
4075
+ * The room row disappears because the server deleted it, but the Session is a durable
4076
+ * record that outlives the room: `dissolveRoom` cannot reach it, so without this the
4077
+ * room's leftover workspace (from before rooms stopped registering one) and its Session
4078
+ * entry would stay listed for a conversation that no longer exists. Archiving is the
4079
+ * documented, reversible way to remove a Session from those surfaces — the same action
4080
+ * the shell's own "archive session" uses.
4081
+ *
4082
+ * Runs after the delete so a refused dissolve leaves the Session alone.
4083
+ */
4084
+ dissolve: async (roomId) => {
4085
+ await base.dissolve(roomId);
4086
+ try {
4087
+ await ctx.workspaces.archiveSession(roomSessionId(roomId), { stopActivity: true });
4088
+ const sessionId = roomSessionId(roomId);
4089
+ const snapshot = ctx.workspaces.list.getSnapshot();
4090
+ for (const workspace of snapshot?.items ?? []) {
4091
+ if (!workspace.sessionIds.includes(sessionId)) continue;
4092
+ await ctx.workspaces.delete(workspace.workspaceId);
4093
+ }
4094
+ } catch {}
4095
+ },
4096
+ /**
4097
+ * Open the members panel for a room.
4098
+ *
4099
+ * Two steps, in this order and not the other way round: the room has to be the *mounted*
4100
+ * Session before a right-sidebar tab can be placed beside it, because `openTab` acts on the
4101
+ * mounted seat and throws when there is none. `openSession` is synchronous in what it records
4102
+ * but the seat re-binds on the next frame, so the tab is opened on a zero-delay task rather
4103
+ * than inline — inline would aim at the session the operator is leaving.
4104
+ */
4105
+ openMembers: (roomId) => {
4106
+ const open = () => {
4107
+ ctx.sidebarRight.openTab(MEMBERS_TAB_KIND, { params: { roomId } });
4108
+ };
4109
+ if (base.getState().activeRoomId === roomId) open();
4110
+ else setTimeout(open, 0);
4111
+ }
4112
+ };
4113
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
4114
+ name: "sidebar.footer.action",
4115
+ id: PANEL_ID,
4116
+ order: 20,
4117
+ inject: () => ({
4118
+ t,
4119
+ tPreset,
4120
+ store
4121
+ })
4122
+ }, SidebarRooms));
4123
+ /**
4124
+ * The members panel, as a tab in the official right sidebar.
4125
+ *
4126
+ * Three registrations, because the sidebar keeps three separate facts about a tab: the type it
4127
+ * belongs to (so navigation can resolve and persist it), the body to draw, and the chip that
4128
+ * labels it. `id` identifies the type's slot entries; `kind` is what `openTab` takes. They are
4129
+ * not the same value — the official schedule plugin uses
4130
+ * `"@deepseek-ai/dsh-client-ui-schedule/task"` and `"scheduleTask"` respectively — and
4131
+ * collapsing them would make `openTab` throw where nobody can see it.
4132
+ *
4133
+ * The panel is registered for every profile and decides at render time whether the Session it
4134
+ * was opened beside is a room. Registering conditionally instead would lose a placed tab on the
4135
+ * next reload: a tab whose type is gone cannot be restored.
4136
+ */
4137
+ /**
4138
+ * Open this plugin's page type from the start guide.
4139
+ *
4140
+ * The guide's shipped box opens through the guide's own `openTab`; a replacement box is not
4141
+ * handed that callback, so it goes through the same service the plugin already holds.
4142
+ */
4143
+ const openTabFromGuide = (kind) => {
4144
+ try {
4145
+ ctx.sidebarRight.openTab(kind);
4146
+ } catch (error) {
4147
+ console.error("[group-chat] opening the members page failed", error);
4148
+ }
4149
+ };
4150
+ ctx.effect(() => ctx.sidebarRightTabs.register({
4151
+ id: MEMBERS_TAB_ID,
4152
+ kind: MEMBERS_TAB_KIND,
4153
+ priority: "extension",
4154
+ title: () => t("membersTitle"),
4155
+ guide: [{
4156
+ id: MEMBERS_GUIDE_ID,
4157
+ order: MEMBERS_GUIDE_ORDER,
4158
+ title: () => t("membersTitle"),
4159
+ description: () => t("membersGuideHint")
4160
+ }]
4161
+ }), "group-chat: members tab type");
4162
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
4163
+ name: "sidebar.right.pane.tab",
4164
+ key: MEMBERS_TAB_ID,
4165
+ locale: NS,
4166
+ inject: ((sessionId) => ({
4167
+ sessionId,
4168
+ t,
4169
+ store
4170
+ }))
4171
+ }, MembersPanel)), "group-chat: members tab body");
4172
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({
4173
+ name: "sidebar.right.pane.tab.title",
4174
+ key: MEMBERS_TAB_ID,
4175
+ locale: NS,
4176
+ inject: () => ({ t })
4177
+ }, MembersTabTitle)), "group-chat: members tab title");
4178
+ ctx.effect(() => ctx.slots.inject("sidebar.right.tab.guide.entry", () => ctx.slots.register({
4179
+ name: "sidebar.right.tab.guide.entry",
4180
+ key: MEMBERS_TAB_ID,
4181
+ locale: NS,
4182
+ inject: ((sessionId) => ({
4183
+ sessionId,
4184
+ onPick: () => {
4185
+ openTabFromGuide(MEMBERS_TAB_KIND);
4186
+ }
4187
+ }))
4188
+ }, MembersGuideEntry)), "group-chat: members guide box");
4189
+ ctx.slots.inject("conversation.chat.node", () => {
4190
+ let claims = [];
4191
+ const release = () => {
4192
+ for (const dispose of claims) dispose();
4193
+ claims = [];
4194
+ };
4195
+ const reconcile = () => {
4196
+ const ledger = ctx.slots.entries("conversation.chat.node");
4197
+ const claimed = new Set(ledger.filter((entry) => (entry.options.priority ?? 0) === SHADOW_PRIORITY).map((entry) => entry.options.key));
4198
+ for (const key of CHAT_NODE_KEYS) {
4199
+ if (claimed.has(key)) continue;
4200
+ const official = officialViewFor(ledger, key, OUR_VIEW);
4201
+ if (official === void 0) continue;
4202
+ let dispose;
4203
+ try {
4204
+ dispose = ctx.slots.register({
4205
+ name: "conversation.chat.node",
4206
+ key,
4207
+ priority: SHADOW_PRIORITY,
4208
+ locale: "chat"
4209
+ }, (props) => MessageSide({
4210
+ ...props,
4211
+ Official: official,
4212
+ getSelfId: store.selfId,
4213
+ subscribeSelf: store.subscribe,
4214
+ getRoster: (roomId) => store.getState().members[roomId],
4215
+ sided: props["node"] !== void 0
4216
+ }));
4217
+ } catch (error) {
4218
+ logClaimFailure(key, error);
4219
+ }
4220
+ if (dispose !== void 0) claims.push(dispose);
4221
+ }
4222
+ };
4223
+ const unsubscribe = ctx.slots.subscribe("conversation.chat.node", reconcile);
4224
+ reconcile();
4225
+ return () => {
4226
+ unsubscribe();
4227
+ release();
4228
+ };
4229
+ });
4230
+ }
4231
+ //#endregion
4232
+ exports.apply = apply;
4233
+ exports.inject = inject;
4234
+ return module.exports;
4235
+ }
4236
+ });
4237
+
4238
+ //# sourceMappingURL=client.js.map