@khorsheed/dsh-mobile 0.1.0 → 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 CHANGED
@@ -2929,8 +2929,12 @@ html[data-dsh-mobile] [data-slot="conversation.input.right"] { max-width: 100%;
2929
2929
  [data-mobile-dialog] button[aria-pressed="true"] { color: #4d6bfe; background: #4d6bfe12; }
2930
2930
  [data-mobile-dialog] [data-mobile-done] { display: block; width: 100%; margin-top: 24px; background: #4d6bfe; color: white; border-radius: 100px; }
2931
2931
 
2932
+ /* The host trigger menu is translucent; mobile overlays need an opaque face. */
2933
+ html[data-dsh-mobile] [data-trigger-menu] { background:var(--mobile-face,var(--dsw-alias-bg-base))!important; --dsw-specific-menu:var(--mobile-face,var(--dsw-alias-bg-base)); opacity:1; backdrop-filter:none; -webkit-backdrop-filter:none; border:1px solid var(--mobile-edge); padding:8px; border-radius:22px; box-shadow:var(--mobile-shadow); }
2934
+ html[data-dsh-mobile] [data-trigger-menu] [role=option] { min-height:48px; padding:12px; font-size:16px; line-height:1.45; }
2935
+ html[data-dsh-mobile] [data-trigger-menu] [role=listbox] { overscroll-behavior:contain; }
2932
2936
  /* Hide an empty ordinary breadcrumb; populated catalogs keep their Host trigger. */
2933
- html[data-dsh-mobile]:has([data-mobile-session-title]) [data-slot="main.conversation"] header:has([data-conversation-header-corner]) nav:has(> span:only-child > button:disabled + [data-slot="conversation.session.header.lineage"]:empty) { display:none; }
2937
+ html[data-dsh-mobile]:has([data-mobile-session-title]) [data-slot="main.conversation"] header:has([data-conversation-header-corner]) nav:has(> span:only-child > :is(button:disabled,span:not([data-slot])) + [data-slot="conversation.session.header.lineage"]:empty) { display:none; }
2934
2938
  html[data-dsh-mobile] [data-mobile-lineage-count] { flex-shrink:0; white-space:nowrap; }
2935
2939
  [data-mobile-restore] { position:fixed; right:16px; bottom:max(16px,env(safe-area-inset-bottom)); z-index:1200; pointer-events:auto; min-height:44px; padding:10px 18px; border:1px solid #4d6bfe40; border-radius:24px; background:var(--dsw-alias-bg-base,white); color:#4d6bfe; font:inherit; box-shadow:0 3px 18px #15244318; }
2936
2940
 
@@ -3067,17 +3071,17 @@ html[data-dsh-mobile]:has([data-mobile-session-title]) [data-mobile-header-hidde
3067
3071
  [data-mobile-folded-action] { display:none!important; }
3068
3072
  [data-mobile-tools-seat] { display:contents; }
3069
3073
  html[data-dsh-mobile] [data-mobile-tools-open] { display:grid; place-items:center; width:44px; height:44px; min-width:44px; padding:0; border:0; border-radius:50%; corner-shape:round; color:var(--dsw-alias-label-primary); background:var(--mobile-soft); }
3070
- [data-mobile-tools-dialog] { box-sizing:border-box; margin:auto auto 0; width:min(100%,520px); max-width:100%; max-height:75dvh; overflow-y:auto; padding:16px 20px max(20px,env(safe-area-inset-bottom)); border:1px solid var(--mobile-line); border-bottom:0; border-radius:26px 26px 0 0; background:var(--mobile-face); color:var(--dsw-alias-label-primary); box-shadow:0 -8px 40px #0002; }
3074
+ [data-mobile-tools-dialog] { box-sizing:border-box; margin:auto auto 0; width:min(100%,520px); max-width:100%; max-height:75dvh; overscroll-behavior:contain; overflow-y:auto; padding:16px 20px max(20px,env(safe-area-inset-bottom)); border:1px solid var(--mobile-line); border-bottom:0; border-radius:26px 26px 0 0; background:var(--mobile-face); color:var(--dsw-alias-label-primary); box-shadow:0 -8px 40px #0002; }
3071
3075
  [data-mobile-tools-dialog]::backdrop { background:#0005; }
3072
- [data-mobile-tools-dialog] [data-mobile-tools-handle] { width:34px; height:4px; margin:0 auto 12px; border-radius:4px; background:var(--dsw-alias-label-tertiary); opacity:.5; }
3073
- [data-mobile-tools-dialog] header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
3074
- [data-mobile-tools-dialog] strong { font-size:16px; font-weight:500; }
3076
+ [data-mobile-tools-dialog] [data-mobile-tools-handle] { width:34px; height:4px; margin:0 auto 12px; touch-action:pan-x; border-radius:4px; background:var(--dsw-alias-label-tertiary); opacity:.5; }
3077
+ [data-mobile-tools-dialog] header { touch-action:pan-x; display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
3078
+ [data-mobile-tools-dialog] strong { font-size:20px; font-weight:600; }
3075
3079
  [data-mobile-tools-dialog] button { font:inherit; color:inherit; cursor:pointer; border:0; background:transparent; -webkit-tap-highlight-color:transparent; }
3076
3080
  [data-mobile-tools-dialog] button:disabled { opacity:.45; cursor:default; }
3077
3081
  [data-mobile-tools-dialog] header button { width:44px; height:44px; display:grid; place-items:center; border-radius:50%; padding:0; }
3078
3082
  [data-mobile-tools-grid] { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
3079
- [data-mobile-tools-grid] button { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; min-height:92px; padding:12px; border-radius:20px; background:var(--mobile-soft); font-size:14px; }
3080
- [data-mobile-tools-permission] { display:flex; align-items:center; width:100%; gap:10px; margin-top:12px; min-height:64px; padding:12px 0; text-align:left; font-size:14px!important; }
3083
+ [data-mobile-tools-grid] button { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; min-height:96px; padding:16px; border-radius:18px; background:var(--mobile-soft); font-size:14px; }
3084
+ [data-mobile-tools-permission] { display:flex; align-items:center; width:100%; gap:10px; margin-top:12px; border-top:1px solid var(--mobile-line)!important; min-height:64px; padding:12px 0; text-align:left; font-size:14px!important; }
3081
3085
  [data-mobile-tools-permission] > span { flex-shrink:0; }
3082
3086
  [data-mobile-tools-permission] > small { margin-left:auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; color:var(--dsw-alias-label-secondary); }
3083
3087
  [data-mobile-tools-dialog] svg { flex:none; }
@@ -3186,6 +3190,12 @@ html[data-dsh-mobile] [data-mobile-room-form] :is(input,textarea,select) { box-s
3186
3190
  html[data-dsh-mobile] [data-mobile-room-form] textarea { min-height:94px; resize:vertical; }
3187
3191
  html[data-dsh-mobile] [data-mobile-room-form] button { min-height:44px; min-width:44px; font-size:16px; border-radius:14px; }
3188
3192
  html[data-dsh-mobile] [data-mobile-room-form] summary { min-height:44px; padding:10px 0; font-size:16px; color:var(--dsw-alias-label-primary); }
3193
+ /* Harness model controls are expandable summaries, not native selects. Keep the
3194
+ advanced-settings disclosure plain while aligning picker faces with form fields. */
3195
+ html[data-dsh-mobile] [data-mobile-room-form] details > summary[aria-expanded] { display:flex; align-items:center; box-sizing:border-box; width:100%; max-width:none; min-width:0; min-height:48px; gap:12px; padding:11px 12px; border:1px solid var(--mobile-edge); border-radius:14px; background:var(--mobile-soft); font-size:17px; line-height:1.5; color:var(--dsw-alias-label-primary); }
3196
+ html[data-dsh-mobile] [data-mobile-room-form] details > summary[aria-expanded] > span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
3197
+ html[data-dsh-mobile] [data-mobile-room-form] details > summary[aria-expanded]::after { flex-shrink:0; margin-inline-start:auto; margin-inline-end:2px; }
3198
+
3189
3199
  html[data-dsh-mobile] [data-mobile-room-form] > div:last-child { position:sticky; bottom:-16px; background:var(--mobile-face); padding:12px 0 0; margin-top:0; display:grid; grid-template-columns:1fr 1fr; gap:12px; }
3190
3200
  html[data-dsh-mobile] [data-mobile-room-form] [role=alert] { font-size:15px; line-height:1.5; }
3191
3201
  html[data-dsh-mobile] [data-member] { font-size:16px; }
@@ -3246,6 +3256,15 @@ html[data-dsh-mobile] [data-mobile-picker] { position:fixed!important; inset:aut
3246
3256
  html[data-dsh-mobile] [data-mobile-picker] > [role=presentation] { max-height:none; overflow:visible; }
3247
3257
  html[data-dsh-mobile] [data-mobile-picker] [role=menuitem] { min-height:52px; padding:12px; border-radius:14px; font-size:17px; }
3248
3258
  html[data-dsh-mobile] [data-mobile-picker] [role=menuitem][hidden] { display:none; }
3259
+ /* Keep the workspace list inside its flex viewport: visible overflow used to
3260
+ * paint the last folder over the pinned add-action divider on short screens. */
3261
+ html[data-dsh-mobile] [data-mobile-picker=workspace] { overflow:hidden; }
3262
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [data-mobile-picker-chrome] { flex:none; }
3263
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [role=presentation]:nth-child(2) { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; }
3264
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [role=presentation] > * { flex:none; }
3265
+ html[data-dsh-mobile] [data-mobile-picker=workspace] > [role=presentation]:nth-child(3):last-child { flex:none; margin-top:8px; padding-top:8px; border-top:1px solid var(--mobile-edge); }
3266
+ html[data-dsh-mobile] [data-mobile-picker=workspace] [role=menuitem] { box-sizing:border-box; min-height:52px; padding:12px; line-height:28px; }
3267
+
3249
3268
  html[data-dsh-mobile] [data-mobile-picker=preset] [role=menuitem] span > span:last-child { font-size:14px; line-height:1.55; color:var(--dsw-alias-label-secondary); }
3250
3269
 
3251
3270
  html[data-mobile-gesture] [data-mobile-library-pane][hidden] { display:block; }
@@ -3327,13 +3346,13 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
3327
3346
  this.clearMarks();
3328
3347
  const root = frame?.querySelector("[data-slot=\"main.conversation\"] > [data-phase]");
3329
3348
  const row = root?.querySelector("header [data-conversation-header-corner]")?.parentElement;
3330
- const ordinary = row?.querySelector("nav > span:only-child > button:disabled + [data-slot=\"conversation.session.header.lineage\"]:empty");
3349
+ const ordinary = row?.querySelector("nav > span:only-child > :is(button:disabled,span:not([data-slot])) + [data-slot=\"conversation.session.header.lineage\"]:empty");
3331
3350
  let preset = "";
3332
3351
  if (row) {
3333
3352
  for (const button of row.querySelectorAll("[data-slot=\"conversation.session.header.actions\"] button[aria-label]")) if (/^(重命名会话|Rename session)$/.test(button.getAttribute("aria-label") ?? "")) this.mark(button, "data-mobile-original-rename");
3334
3353
  }
3335
3354
  if (root?.dataset.phase === "active" && row) {
3336
- for (const title of row.querySelectorAll("nav > span > button:disabled")) {
3355
+ for (const title of row.querySelectorAll("nav > span > :is(button:disabled,span:not([data-slot]))")) {
3337
3356
  const slot = title.nextElementSibling;
3338
3357
  if (slot?.getAttribute("data-slot") !== "conversation.session.header.lineage") continue;
3339
3358
  const count = slot.querySelector(":scope > div:only-child > button[aria-haspopup=\"tree\"]");
@@ -3343,17 +3362,18 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
3343
3362
  this.mark(separator, "data-mobile-header-hidden");
3344
3363
  this.mark(count, "data-mobile-lineage-count");
3345
3364
  }
3346
- const label = row.querySelector("[data-slot=\"conversation.session.header.actions\"] > span[title]:has(svg mask[id^=\"mask0_agent_preset\"])");
3365
+ const label = row.querySelector("[data-slot=\"conversation.session.header.actions\"] > span[title]:has(svg :is(mask[id^=\"mask0_agent_preset\"],path[d^=\"M6.51867 12.3282\"]))");
3347
3366
  if (label) {
3348
3367
  preset = label.textContent?.trim() ?? "";
3349
3368
  this.mark(label, "data-mobile-header-hidden");
3350
3369
  }
3370
+ for (const target of row.querySelectorAll("[data-slot=\"conversation.session.header.utilities\"] [data-open-target=\"directory\"]")) this.mark(target, "data-mobile-header-hidden");
3351
3371
  for (const img of row.querySelectorAll("img[src^=\"/open-in-app/icon/\"]")) {
3352
3372
  const split = img.closest("button")?.parentElement;
3353
3373
  if (split && split.querySelectorAll("button").length === 2) this.mark(split, "data-mobile-header-hidden");
3354
3374
  }
3355
3375
  for (const button of row.querySelectorAll("[data-sidebar-right-expand], [data-slot=\"conversation.session.header.utilities\"] > span > button[aria-haspopup=\"menu\"][aria-busy]")) if (button.hasAttribute("data-sidebar-right-expand") || /^(更多操作|More actions)$/i.test(button.getAttribute("aria-label") ?? "")) this.mark(button, "data-mobile-header-hidden");
3356
- if (this.doc.querySelector("[data-mobile-members-open], [data-mobile-tools-open]")) {
3376
+ if (this.doc.querySelector("[data-mobile-members-open]:not([disabled])")) {
3357
3377
  for (const button of row.querySelectorAll("[data-slot=\"conversation.session.header.actions\"] button")) if (/^(+|\+)?\s*(邀请 agent|Invite agent)$/i.test(button.textContent?.trim() ?? "")) this.mark(button, "data-mobile-header-hidden");
3358
3378
  }
3359
3379
  const content = row.cloneNode(true);
@@ -4092,6 +4112,19 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4092
4112
  return null;
4093
4113
  }
4094
4114
  //#endregion
4115
+ //#region src/client/commands.ts
4116
+ function openMobileCommands(controller, draft, span) {
4117
+ if (!controller || typeof controller.toggleSource !== "function") return false;
4118
+ controller.toggleSource("command", {
4119
+ trigger: "/",
4120
+ query: "",
4121
+ quoted: false,
4122
+ position: draft.slice(0, span.start).trim() === "" ? "leading" : "inline",
4123
+ span
4124
+ });
4125
+ return true;
4126
+ }
4127
+ //#endregion
4095
4128
  //#region src/client/composerActions.ts
4096
4129
  var ComposerActions = class {
4097
4130
  seat;
@@ -4108,7 +4141,11 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4108
4141
  subtree: true,
4109
4142
  characterData: true,
4110
4143
  attributes: true,
4111
- attributeFilter: ["disabled", "aria-label"]
4144
+ attributeFilter: [
4145
+ "disabled",
4146
+ "aria-label",
4147
+ "contenteditable"
4148
+ ]
4112
4149
  });
4113
4150
  this.sync();
4114
4151
  }
@@ -4120,21 +4157,25 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4120
4157
  };
4121
4158
  };
4122
4159
  sync() {
4123
- const file = this.seat.closest("[data-composer-card]")?.querySelector(":scope > div:last-child > div:first-child > input[type=file][hidden]");
4124
- const attachments = file?.previousElementSibling;
4125
- const commands = attachments?.previousElementSibling;
4160
+ const card = this.seat.closest("[data-composer-card]");
4161
+ const file = card?.querySelector(":scope > div:last-child > div:first-child > input[type=file][hidden]");
4162
+ const previous = file?.previousElementSibling;
4163
+ const unified = previous instanceof HTMLButtonElement && previous.getAttribute("aria-haspopup") === "listbox";
4164
+ const attachments = unified ? file : previous;
4165
+ const commands = unified ? previous : previous?.previousElementSibling;
4126
4166
  const next = {};
4127
- if (commands instanceof HTMLButtonElement && commands.getAttribute("aria-haspopup") === "listbox" && attachments instanceof HTMLButtonElement) {
4167
+ if (commands instanceof HTMLButtonElement && commands.getAttribute("aria-haspopup") === "listbox" && (attachments instanceof HTMLButtonElement || attachments instanceof HTMLInputElement)) {
4128
4168
  next.commands = this.target(commands);
4129
4169
  next.attachments = this.target(attachments);
4130
- const permission = (file?.nextElementSibling)?.querySelector(":scope > span > button[aria-label]");
4170
+ if (unified) next.attachments.disabled ||= commands.disabled || !card?.querySelector("[contenteditable=\"true\"]");
4171
+ const permission = (file?.nextElementSibling)?.querySelector("[data-slot=\"conversation.input.permission\"] button[aria-label], :scope > span > button[aria-label]");
4131
4172
  if (permission?.querySelector(":scope > span:not([aria-hidden])") && permission.querySelector(":scope > span[aria-hidden] svg")) next.permissions = this.target(permission);
4132
4173
  }
4133
4174
  for (const button of this.marked) if (!Object.values(next).some((value) => value.button === button)) {
4134
4175
  button.removeAttribute("data-mobile-folded-action");
4135
4176
  this.marked.delete(button);
4136
4177
  }
4137
- for (const value of Object.values(next)) if (!this.marked.has(value.button)) {
4178
+ for (const value of Object.values(next)) if (value.button instanceof HTMLButtonElement && !this.marked.has(value.button)) {
4138
4179
  value.button.setAttribute("data-mobile-folded-action", "");
4139
4180
  this.marked.add(value.button);
4140
4181
  }
@@ -4154,10 +4195,11 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4154
4195
  disabled: button.disabled
4155
4196
  };
4156
4197
  }
4157
- invoke(action) {
4198
+ invoke(action, openCommands) {
4158
4199
  this.sync();
4159
4200
  const target = this.snapshot[action];
4160
4201
  if (!target || target.disabled || !target.button.isConnected) return false;
4202
+ if (action === "commands" && openCommands?.()) return true;
4161
4203
  if (action === "commands") this.seat.closest("[data-composer-card]")?.querySelector("[contenteditable=\"true\"]")?.focus({ preventScroll: true });
4162
4204
  target.button.click();
4163
4205
  return true;
@@ -4447,8 +4489,9 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4447
4489
  //#endregion
4448
4490
  //#region src/client/MobileTools.tsx
4449
4491
  /** Own only the launcher and grouped navigation; action targets stay host-owned. */
4450
- function MobileTools({ t, useSession, rooms }) {
4492
+ function MobileTools({ t, useSession, useInput, inputActions, rooms, openCommands }) {
4451
4493
  const sessionId = useSession((s) => s.sessionId);
4494
+ const draft = useInput((s) => s.draft);
4452
4495
  const seat = (0, react.useRef)(null);
4453
4496
  const [actions, setActions] = (0, react.useState)();
4454
4497
  (0, react.useLayoutEffect)(() => {
@@ -4463,11 +4506,12 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4463
4506
  children: actions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tools, {
4464
4507
  actions,
4465
4508
  t,
4509
+ openCommands: openCommands ? () => openCommands(sessionId, draft, inputActions.captureInsertion()) : void 0,
4466
4510
  invite: rooms.available() ? () => openMobileMembers(sessionId, true) : void 0
4467
4511
  })
4468
4512
  });
4469
4513
  }
4470
- function Tools({ actions, t, invite }) {
4514
+ function Tools({ actions, t, invite, openCommands }) {
4471
4515
  const targets = (0, react.useSyncExternalStore)(actions.subscribe, actions.getSnapshot);
4472
4516
  const dialog = (0, react.useRef)(null), launcher = (0, react.useRef)(null);
4473
4517
  const close = () => {
@@ -4480,7 +4524,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4480
4524
  if (!targets.commands || !targets.attachments) return null;
4481
4525
  const invoke = (action) => {
4482
4526
  close();
4483
- actions.invoke(action);
4527
+ actions.invoke(action, openCommands);
4484
4528
  };
4485
4529
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4486
4530
  ref: launcher,
@@ -4489,6 +4533,8 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4489
4533
  "aria-haspopup": "dialog",
4490
4534
  "aria-expanded": "false",
4491
4535
  onClick: () => {
4536
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
4537
+ launcher.current?.focus({ preventScroll: true });
4492
4538
  dialog.current?.showModal();
4493
4539
  launcher.current?.setAttribute("aria-expanded", "true");
4494
4540
  },
@@ -4772,6 +4818,42 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4772
4818
  }), document.body);
4773
4819
  }
4774
4820
  //#endregion
4821
+ //#region src/client/catalogReads.ts
4822
+ /** Bound optional catalog enrichment so cold log reads cannot fan out across the library. */
4823
+ var CatalogReads = class {
4824
+ limit;
4825
+ pending = [];
4826
+ running = 0;
4827
+ constructor(limit = 2) {
4828
+ this.limit = limit;
4829
+ }
4830
+ add(work, priority = 1) {
4831
+ const item = {
4832
+ cancelled: false,
4833
+ priority,
4834
+ work
4835
+ };
4836
+ this.pending.push(item);
4837
+ this.pending.sort((a, b) => a.priority - b.priority);
4838
+ this.pump();
4839
+ return () => {
4840
+ item.cancelled = true;
4841
+ this.pending = this.pending.filter((entry) => entry !== item);
4842
+ };
4843
+ }
4844
+ pump() {
4845
+ while (this.running < this.limit && this.pending.length) {
4846
+ const item = this.pending.shift();
4847
+ if (item.cancelled) continue;
4848
+ this.running++;
4849
+ Promise.resolve().then(() => item.cancelled ? void 0 : item.work(() => !item.cancelled)).catch(() => {}).finally(() => {
4850
+ this.running--;
4851
+ this.pump();
4852
+ });
4853
+ }
4854
+ }
4855
+ };
4856
+ //#endregion
4775
4857
  //#region src/client/native.ts
4776
4858
  /** Additive bridge features: older shells and ordinary browsers retain Web controls. */
4777
4859
  function hasNativeAction(action) {
@@ -4793,7 +4875,8 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4793
4875
  //#endregion
4794
4876
  //#region src/client/MobileLibrary.tsx
4795
4877
  /** Browse official metadata; the official workspace service remains the only navigation writer. */
4796
- function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, t }) {
4878
+ function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, visible = true, t }) {
4879
+ const reads = (0, react.useMemo)(() => new CatalogReads(), [navigation.sessions]);
4797
4880
  const sessionFeed = navigation.sessions.list, workspaceFeed = navigation.workspaces.list;
4798
4881
  const sessions = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => sessionFeed.subscribe(listener), [sessionFeed]), (0, react.useCallback)(() => sessionFeed.getSnapshot(), [sessionFeed]));
4799
4882
  const workspaces = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => workspaceFeed.subscribe(listener), [workspaceFeed]), (0, react.useCallback)(() => workspaceFeed.getSnapshot(), [workspaceFeed]));
@@ -4891,8 +4974,10 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4891
4974
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4892
4975
  "data-mobile-session-copy": true,
4893
4976
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: row.title || (row.blank ? t("newSession") : row.displayTitle) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionMetadata, {
4894
- id: row.id,
4895
- cwd: row.cwd ?? "",
4977
+ row,
4978
+ sessions: navigation.sessions,
4979
+ reads,
4980
+ visible,
4896
4981
  ...rooms ? { rooms } : {},
4897
4982
  t
4898
4983
  })]
@@ -4950,29 +5035,56 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4950
5035
  }
4951
5036
  const emptyRoomSubscribe = () => () => {};
4952
5037
  const emptyRoomSnapshot = () => 0;
4953
- function SessionMetadata({ id, cwd, rooms, t }) {
5038
+ function SessionMetadata({ row, sessions, reads, visible, rooms, t }) {
4954
5039
  const ref = (0, react.useRef)(null);
4955
5040
  (0, react.useSyncExternalStore)(rooms?.subscribe ?? emptyRoomSubscribe, rooms?.getSnapshot ?? emptyRoomSnapshot);
4956
5041
  (0, react.useEffect)(() => {
4957
- if (!rooms?.available() || !ref.current) return;
5042
+ if (!visible || !ref.current) return;
5043
+ let cancel;
5044
+ const enter = () => {
5045
+ if (cancel) return;
5046
+ const cancelTitle = !row.title && !row.blank && typeof sessions.refreshProjections === "function" ? reads.add(async () => {
5047
+ await sessions.refreshProjections(row.id);
5048
+ }, 0) : void 0;
5049
+ const cancelRoom = rooms?.available() ? reads.add(async () => {
5050
+ await rooms.refresh(row.id);
5051
+ }) : void 0;
5052
+ cancel = () => {
5053
+ cancelTitle?.();
5054
+ cancelRoom?.();
5055
+ };
5056
+ };
5057
+ const leave = () => {
5058
+ cancel?.();
5059
+ cancel = void 0;
5060
+ };
4958
5061
  if (typeof IntersectionObserver === "undefined") {
4959
- rooms.refresh(id);
4960
- return;
5062
+ enter();
5063
+ return leave;
4961
5064
  }
4962
5065
  const observer = new IntersectionObserver((entries) => {
4963
- if (entries.some((e) => e.isIntersecting)) {
4964
- rooms.refresh(id);
4965
- observer.disconnect();
4966
- }
4967
- });
5066
+ if (entries.some((entry) => entry.isIntersecting)) enter();
5067
+ else leave();
5068
+ }, { root: ref.current.closest("[data-mobile-recents]") });
4968
5069
  observer.observe(ref.current);
4969
- return () => observer.disconnect();
4970
- }, [rooms, id]);
4971
- const state = rooms?.get(id), count = state ? state.members.length : state === null || !rooms?.available() ? 1 : "…";
5070
+ return () => {
5071
+ observer.disconnect();
5072
+ leave();
5073
+ };
5074
+ }, [
5075
+ rooms,
5076
+ row.id,
5077
+ row.title,
5078
+ row.blank,
5079
+ sessions,
5080
+ reads,
5081
+ visible
5082
+ ]);
5083
+ const state = rooms?.get(row.id), count = state ? state.members.length : state === null || !rooms?.available() ? 1 : "…";
4972
5084
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", {
4973
5085
  ref,
4974
5086
  children: [
4975
- cwd.split(/[\\/]/).filter(Boolean).at(-1) || t("unassignedWorkspace"),
5087
+ (row.cwd ?? "").split(/[\\/]/).filter(Boolean).at(-1) || t("unassignedWorkspace"),
4976
5088
  " · ",
4977
5089
  count,
4978
5090
  " ",
@@ -5186,6 +5298,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5186
5298
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileLibrary, {
5187
5299
  ...rooms ? { rooms } : {},
5188
5300
  navigation: available,
5301
+ visible: library || wide,
5189
5302
  t,
5190
5303
  onBeforeOpen: presentation.prepareNavigation,
5191
5304
  onOpen: () => setLibrary(false)
@@ -5581,6 +5694,90 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5581
5694
  }
5582
5695
  };
5583
5696
  //#endregion
5697
+ //#region src/client/sheetGestures.ts
5698
+ /** Mobile dismissal delegates to native dialog.close or the Room owner's close handler. */
5699
+ function installSheetGestures(doc) {
5700
+ let drag;
5701
+ const reset = () => {
5702
+ if (drag) drag.sheet.style.transform = drag.transform;
5703
+ drag = void 0;
5704
+ };
5705
+ const close = (overlay) => {
5706
+ if (overlay.isConnected && doc.documentElement.hasAttribute("data-dsh-mobile")) {
5707
+ if (overlay instanceof HTMLDialogElement) {
5708
+ overlay.close();
5709
+ return;
5710
+ }
5711
+ overlay.dispatchEvent(new MouseEvent("mousedown", {
5712
+ bubbles: true,
5713
+ cancelable: true
5714
+ }));
5715
+ }
5716
+ };
5717
+ const down = (event) => {
5718
+ reset();
5719
+ if (!doc.documentElement.hasAttribute("data-dsh-mobile") || event.touches.length !== 1) return;
5720
+ const target = event.target;
5721
+ const native = target.closest("dialog[data-mobile-tools-dialog][open]");
5722
+ const overlay = native ?? target.closest("[data-mobile-room-overlay]");
5723
+ const sheet = native ?? overlay?.querySelector("[data-mobile-room-form]");
5724
+ if (!sheet || !overlay || !native && doc.querySelector("dialog[open]") || doc.getSelection()?.toString()) return;
5725
+ const point = event.touches[0], bounds = sheet.getBoundingClientRect();
5726
+ const outside = native ? target === native && (point.clientY < bounds.top || point.clientY > bounds.bottom || point.clientX < bounds.left || point.clientX > bounds.right) : target === overlay;
5727
+ if (!outside && (sheet.scrollTop > 0 || point.clientY > sheet.getBoundingClientRect().top + 96 || target.closest("button,input,textarea,select,details,a,[contenteditable],[role=menu]"))) return;
5728
+ drag = {
5729
+ sheet,
5730
+ overlay,
5731
+ x: point.clientX,
5732
+ y: point.clientY,
5733
+ outside,
5734
+ dragging: false,
5735
+ transform: sheet.style.transform
5736
+ };
5737
+ };
5738
+ const move = (event) => {
5739
+ if (!drag) return;
5740
+ if (event.touches.length !== 1 || !drag.sheet.isConnected || !doc.documentElement.hasAttribute("data-dsh-mobile")) {
5741
+ reset();
5742
+ return;
5743
+ }
5744
+ const dx = event.touches[0].clientX - drag.x, dy = event.touches[0].clientY - drag.y;
5745
+ if (drag.outside) {
5746
+ if (Math.hypot(dx, dy) > 10) reset();
5747
+ return;
5748
+ }
5749
+ if (!drag.dragging && (dy < -10 || Math.abs(dx) > 12 && Math.abs(dx) > Math.abs(dy))) {
5750
+ reset();
5751
+ return;
5752
+ }
5753
+ if (!drag.dragging && (dy < 12 || dy < Math.abs(dx) * 1.5)) return;
5754
+ drag.dragging = true;
5755
+ drag.sheet.style.transform = `translate3d(0,${Math.max(0, dy)}px,0)`;
5756
+ if (event.cancelable) event.preventDefault();
5757
+ };
5758
+ const up = (event) => {
5759
+ const from = drag, point = event.changedTouches[0];
5760
+ reset();
5761
+ if (!from || !point) return;
5762
+ const dx = point.clientX - from.x, dy = point.clientY - from.y;
5763
+ if (from.outside && Math.hypot(dx, dy) <= 10 || from.dragging && dy >= 72 && dy > Math.abs(dx) * 1.5) {
5764
+ if (event.cancelable) event.preventDefault();
5765
+ close(from.overlay);
5766
+ }
5767
+ };
5768
+ doc.addEventListener("touchstart", down, { passive: true });
5769
+ doc.addEventListener("touchmove", move, { passive: false });
5770
+ doc.addEventListener("touchend", up, { passive: false });
5771
+ doc.addEventListener("touchcancel", reset);
5772
+ return () => {
5773
+ reset();
5774
+ doc.removeEventListener("touchstart", down);
5775
+ doc.removeEventListener("touchmove", move);
5776
+ doc.removeEventListener("touchend", up);
5777
+ doc.removeEventListener("touchcancel", reset);
5778
+ };
5779
+ }
5780
+ //#endregion
5584
5781
  //#region src/client/foreground.ts
5585
5782
  /** Resume a stale official follow generation, preserving live reply baselines.
5586
5783
  * Brief interruptions and duplicate native/browser signals leave healthy streams alone. */
@@ -5969,6 +6166,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5969
6166
  navigation
5970
6167
  })
5971
6168
  }, MobileDirectoryOverlay));
6169
+ ctx.effect(() => installSheetGestures(document), "mobile: sheet dismissal");
5972
6170
  ctx.effect(() => () => {
5973
6171
  presentation.dispose();
5974
6172
  }, "mobile: presentation");
@@ -6067,7 +6265,14 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
6067
6265
  id: "mobile-input-tools",
6068
6266
  order: -100,
6069
6267
  locale: NS,
6070
- inject: () => ({ rooms })
6268
+ inject: () => ({
6269
+ rooms,
6270
+ openCommands: (id, draft, span) => {
6271
+ const actx = ctx.get("sessions")?.scope(id);
6272
+ const service = ctx.get("inputTriggers");
6273
+ return actx !== void 0 && typeof service?.sessionOf === "function" && openMobileCommands(service.sessionOf(actx), draft, span);
6274
+ }
6275
+ })
6071
6276
  }, MobileTools);
6072
6277
  else {
6073
6278
  remove?.();
@@ -119,6 +119,6 @@ export function MobileChrome({ presentation, toggleSidebar, connection, navigati
119
119
  }, [state.drawer, settings, library, toggleSidebar]);
120
120
  if (!state.active && !settings && state.mode !== 'desktop')
121
121
  return null;
122
- return _jsxs(_Fragment, { children: [!state.active && state.mode === 'desktop' && _jsx("button", { "data-mobile-restore": true, onClick: () => presentation.setMode('mobile'), children: t('restoreMobile') }), state.active && _jsxs(_Fragment, { children: [available && _jsxs("aside", { "data-mobile-library-pane": true, hidden: !library && !wide, children: [_jsxs("nav", { "data-mobile-library-toolbar": true, "aria-label": t('menu'), children: [_jsx("button", { "aria-label": t('settings'), onClick: openSettings, children: _jsx(MobileIcon, { name: "settings" }) }), _jsx("button", { "aria-label": t('newSession'), onClick: startSession, children: _jsx(MobileIcon, { name: "compose" }) })] }), _jsx(MobileLibrary, { ...(rooms ? { rooms } : {}), navigation: available, t: t, onBeforeOpen: presentation.prepareNavigation, onOpen: () => setLibrary(false) })] }), library && available && !wide && _jsx("button", { type: "button", "data-mobile-peek-close": true, "aria-label": t('returnToConversation'), onClick: () => { presentation.prepareNavigation(); setLibrary(false); } }), state.drawer && _jsx("button", { "data-mobile-shade": true, "aria-label": t('close'), onClick: toggleSidebar }), _jsxs("nav", { "data-mobile-toolbar": true, "data-library": false, "aria-label": t('menu'), children: [_jsx("button", { "aria-label": t('menu'), onClick: openLibrary, children: _jsx(MobileIcon, { name: "back" }) }), _jsxs("div", { "data-mobile-nav-title": true, children: [(available ? _jsx(ConversationTitle, { navigation: available, prepareNavigation: presentation.prepareNavigation, surface: presentation.surface, fallback: t('newSession'), t: t }) : _jsx("strong", { children: t('brand') })), wire !== 'connected' && !connectionExpanded && _jsx("small", { role: "status", children: t(wire ?? 'connecting') })] }), _jsx("button", { "data-mobile-new": !!available, "aria-label": available ? t('newSession') : t('settings'), onClick: available ? startSession : openSettings, children: _jsx(MobileIcon, { name: available ? "compose" : "settings" }) }), available && rooms && _jsx(MobileRoomNavigation, { rooms: rooms, navigation: available, prepareNavigation: presentation.prepareNavigation, t: t })] }), wire !== 'connected' && connectionExpanded && _jsxs("div", { "data-mobile-connection-status": true, role: "status", children: [_jsx("span", { children: t(wire ?? 'connecting') }), _jsx("button", { onClick: () => connection.reconnect(), children: t('reconnect') })] })] }), _jsxs("dialog", { ref: dialog, "data-mobile-dialog": true, onClose: () => setSettings(false), "aria-label": t('settings'), children: [_jsx("h2", { children: t('settings') }), available && _jsx("button", { "data-mobile-option": true, onClick: startSession, children: t('newSession') }), _jsx("button", { "data-mobile-option": true, onClick: () => { setSettings(false); setLibrary(false); if (!state.drawer)
122
+ return _jsxs(_Fragment, { children: [!state.active && state.mode === 'desktop' && _jsx("button", { "data-mobile-restore": true, onClick: () => presentation.setMode('mobile'), children: t('restoreMobile') }), state.active && _jsxs(_Fragment, { children: [available && _jsxs("aside", { "data-mobile-library-pane": true, hidden: !library && !wide, children: [_jsxs("nav", { "data-mobile-library-toolbar": true, "aria-label": t('menu'), children: [_jsx("button", { "aria-label": t('settings'), onClick: openSettings, children: _jsx(MobileIcon, { name: "settings" }) }), _jsx("button", { "aria-label": t('newSession'), onClick: startSession, children: _jsx(MobileIcon, { name: "compose" }) })] }), _jsx(MobileLibrary, { ...(rooms ? { rooms } : {}), navigation: available, visible: library || wide, t: t, onBeforeOpen: presentation.prepareNavigation, onOpen: () => setLibrary(false) })] }), library && available && !wide && _jsx("button", { type: "button", "data-mobile-peek-close": true, "aria-label": t('returnToConversation'), onClick: () => { presentation.prepareNavigation(); setLibrary(false); } }), state.drawer && _jsx("button", { "data-mobile-shade": true, "aria-label": t('close'), onClick: toggleSidebar }), _jsxs("nav", { "data-mobile-toolbar": true, "data-library": false, "aria-label": t('menu'), children: [_jsx("button", { "aria-label": t('menu'), onClick: openLibrary, children: _jsx(MobileIcon, { name: "back" }) }), _jsxs("div", { "data-mobile-nav-title": true, children: [(available ? _jsx(ConversationTitle, { navigation: available, prepareNavigation: presentation.prepareNavigation, surface: presentation.surface, fallback: t('newSession'), t: t }) : _jsx("strong", { children: t('brand') })), wire !== 'connected' && !connectionExpanded && _jsx("small", { role: "status", children: t(wire ?? 'connecting') })] }), _jsx("button", { "data-mobile-new": !!available, "aria-label": available ? t('newSession') : t('settings'), onClick: available ? startSession : openSettings, children: _jsx(MobileIcon, { name: available ? "compose" : "settings" }) }), available && rooms && _jsx(MobileRoomNavigation, { rooms: rooms, navigation: available, prepareNavigation: presentation.prepareNavigation, t: t })] }), wire !== 'connected' && connectionExpanded && _jsxs("div", { "data-mobile-connection-status": true, role: "status", children: [_jsx("span", { children: t(wire ?? 'connecting') }), _jsx("button", { onClick: () => connection.reconnect(), children: t('reconnect') })] })] }), _jsxs("dialog", { ref: dialog, "data-mobile-dialog": true, onClose: () => setSettings(false), "aria-label": t('settings'), children: [_jsx("h2", { children: t('settings') }), available && _jsx("button", { "data-mobile-option": true, onClick: startSession, children: t('newSession') }), _jsx("button", { "data-mobile-option": true, onClick: () => { setSettings(false); setLibrary(false); if (!state.drawer)
123
123
  toggleSidebar(); }, children: t('workspaces') }), hasNativeAction('settings') ? _jsx("button", { "data-mobile-option": true, onClick: () => { setSettings(false); requestNativeAction('settings'); }, children: t('connectionSettings') }) : _jsxs(_Fragment, { children: [_jsx("h3", { children: t('connection') }), _jsx("p", { children: window.location.host }), _jsx("p", { children: t(wire ?? 'connecting') }), _jsx("button", { onClick: () => connection.reconnect(), children: t('reconnect') })] }), _jsx("h3", { children: t('display') }), _jsx("div", { role: "group", "aria-label": t('display'), children: ['auto', 'mobile', 'desktop'].map((mode) => _jsx("button", { "aria-pressed": state.mode === mode, onClick: () => presentation.setMode(mode), children: t(mode === 'auto' ? 'automatic' : mode) }, mode)) }), _jsx("p", { children: t('appearance') }), _jsx("button", { "data-mobile-done": true, onClick: () => setSettings(false), children: t('done') })] })] });
124
124
  }
@@ -6,8 +6,9 @@ type Props = PropsLocale<'mobile'> & {
6
6
  navigation: NavigationCapabilities;
7
7
  onOpen: () => void;
8
8
  onBeforeOpen?: () => void;
9
+ visible?: boolean;
9
10
  };
10
11
  /** Browse official metadata; the official workspace service remains the only navigation writer. */
11
- export declare function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, t }: Props): import("react").JSX.Element;
12
+ export declare function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, visible, t }: Props): import("react").JSX.Element;
12
13
  export {};
13
14
  //# sourceMappingURL=MobileLibrary.d.ts.map
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { GROUPING_KEY, groupSessions, mainSessionId, recentSessions } from './navigation.js';
4
+ import { CatalogReads } from './catalogReads.js';
4
5
  import { MobileIcon } from './MobileIcon.js';
5
6
  import { hasNativeAction, requestNativeAction } from './native.js';
6
7
  /** Browse official metadata; the official workspace service remains the only navigation writer. */
7
- export function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, t }) {
8
+ export function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, visible = true, t }) {
9
+ const reads = useMemo(() => new CatalogReads(), [navigation.sessions]);
8
10
  const sessionFeed = navigation.sessions.list, workspaceFeed = navigation.workspaces.list;
9
11
  // Official feeds may expose prototype methods. Keep their receiver and stable subscriptions.
10
12
  const sessions = useSyncExternalStore(useCallback(listener => sessionFeed.subscribe(listener), [sessionFeed]), useCallback(() => sessionFeed.getSnapshot(), [sessionFeed]));
@@ -46,28 +48,45 @@ export function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, t }) {
46
48
  catch {
47
49
  setError(true);
48
50
  }
49
- }, children: [_jsxs("span", { "data-mobile-session-copy": true, children: [_jsx("strong", { children: row.title || (row.blank ? t('newSession') : row.displayTitle) }), _jsx(SessionMetadata, { id: row.id, cwd: row.cwd ?? '', ...(rooms ? { rooms } : {}), t: t })] }), row.running ? _jsx("span", { "data-mobile-activity": true, role: "status", children: t('running') }) : _jsx("time", { dateTime: new Date(row.updatedAt).toISOString(), children: new Date(row.updatedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) })] }) }, row.id)) })] }, group.key);
51
+ }, children: [_jsxs("span", { "data-mobile-session-copy": true, children: [_jsx("strong", { children: row.title || (row.blank ? t('newSession') : row.displayTitle) }), _jsx(SessionMetadata, { row: row, sessions: navigation.sessions, reads: reads, visible: visible, ...(rooms ? { rooms } : {}), t: t })] }), row.running ? _jsx("span", { "data-mobile-activity": true, role: "status", children: t('running') }) : _jsx("time", { dateTime: new Date(row.updatedAt).toISOString(), children: new Date(row.updatedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) })] }) }, row.id)) })] }, group.key);
50
52
  })] }), _jsxs("footer", { "data-mobile-search-dock": true, children: [_jsxs("label", { "data-mobile-search": true, children: [_jsx(MobileIcon, { name: "search", size: 20 }), _jsx("input", { ref: input, type: "search", enterKeyHint: "search", autoComplete: "off", value: query, onFocus: () => setSearching(true), onChange: e => setQuery(e.target.value), placeholder: t('searchTitles'), "aria-label": t('searchTitles') })] }), searching ? _jsx("button", { "data-mobile-search-cancel": true, "aria-label": t('cancel'), onClick: () => { setQuery(''); setSearching(false); input.current?.blur(); }, children: _jsx(MobileIcon, { name: "close" }) }) : hasNativeAction('scan') && _jsx("button", { "data-mobile-scan": true, "aria-label": t('scan'), onClick: () => requestNativeAction('scan'), children: _jsx(MobileIcon, { name: "scan" }) })] })] });
51
53
  }
52
54
  const emptyRoomSubscribe = () => () => { };
53
55
  const emptyRoomSnapshot = () => 0;
54
- function SessionMetadata({ id, cwd, rooms, t }) {
56
+ function SessionMetadata({ row, sessions, reads, visible, rooms, t }) {
55
57
  const ref = useRef(null);
56
58
  useSyncExternalStore(rooms?.subscribe ?? emptyRoomSubscribe, rooms?.getSnapshot ?? emptyRoomSnapshot);
57
59
  useEffect(() => {
58
- if (!rooms?.available() || !ref.current)
60
+ if (!visible || !ref.current)
59
61
  return;
62
+ let cancel;
63
+ const enter = () => {
64
+ if (cancel)
65
+ return;
66
+ // Cold list hints may omit titles. Read the official projection store without
67
+ // retaining/opening a Session; queued title reads take priority over Room scans.
68
+ const cancelTitle = !row.title && !row.blank && typeof sessions.refreshProjections === 'function'
69
+ ? reads.add(async () => { await sessions.refreshProjections(row.id); }, 0)
70
+ : undefined;
71
+ const cancelRoom = rooms?.available()
72
+ ? reads.add(async () => { await rooms.refresh(row.id); })
73
+ : undefined;
74
+ cancel = () => { cancelTitle?.(); cancelRoom?.(); };
75
+ };
76
+ const leave = () => { cancel?.(); cancel = undefined; };
60
77
  if (typeof IntersectionObserver === 'undefined') {
61
- void rooms.refresh(id);
62
- return;
78
+ enter();
79
+ return leave;
63
80
  }
64
- const observer = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting)) {
65
- void rooms.refresh(id);
66
- observer.disconnect();
67
- } });
81
+ const observer = new IntersectionObserver(entries => {
82
+ if (entries.some(entry => entry.isIntersecting))
83
+ enter();
84
+ else
85
+ leave();
86
+ }, { root: ref.current.closest('[data-mobile-recents]') });
68
87
  observer.observe(ref.current);
69
- return () => observer.disconnect();
70
- }, [rooms, id]);
71
- const state = rooms?.get(id), count = state ? state.members.length : state === null || !rooms?.available() ? 1 : '…';
72
- return _jsxs("small", { ref: ref, children: [cwd.split(/[\\/]/).filter(Boolean).at(-1) || t('unassignedWorkspace'), " \u00B7 ", count, " ", t('memberUnit')] });
88
+ return () => { observer.disconnect(); leave(); };
89
+ }, [rooms, row.id, row.title, row.blank, sessions, reads, visible]);
90
+ const state = rooms?.get(row.id), count = state ? state.members.length : state === null || !rooms?.available() ? 1 : '…';
91
+ return _jsxs("small", { ref: ref, children: [(row.cwd ?? '').split(/[\\/]/).filter(Boolean).at(-1) || t('unassignedWorkspace'), " \u00B7 ", count, " ", t('memberUnit')] });
73
92
  }
@@ -1,7 +1,17 @@
1
1
  import type { PropsRuntime, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import { ComposerActions } from './composerActions.ts';
2
3
  import type { MobileRooms } from './rooms.ts';
3
- /** Own only the launcher and grouped navigation; action targets stay host-owned. */
4
- export declare function MobileTools({ t, useSession, rooms }: PropsLocale<'mobile'> & PropsRuntime<'conversation.input.left'> & {
4
+ import type { SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client';
5
+ import type { TokenSpan } from '@deepseek-ai/dsh-client-ui-conversation/client';
6
+ export interface MobileToolsInjected {
5
7
  rooms: MobileRooms;
6
- }): import("react").JSX.Element;
8
+ openCommands: (id: SessionSummary['id'], draft: string, span: TokenSpan) => boolean;
9
+ }
10
+ /** Own only the launcher and grouped navigation; action targets stay host-owned. */
11
+ export declare function MobileTools({ t, useSession, useInput, inputActions, rooms, openCommands }: PropsLocale<'mobile'> & PropsRuntime<'conversation.input.left'> & MobileToolsInjected): import("react").JSX.Element;
12
+ export declare function Tools({ actions, t, invite, openCommands }: {
13
+ actions: ComposerActions;
14
+ openCommands?: (() => boolean) | undefined;
15
+ invite: (() => void) | undefined;
16
+ } & PropsLocale<'mobile'>): import("react").JSX.Element | null;
7
17
  //# sourceMappingURL=MobileTools.d.ts.map