@khorsheed/dsh-mobile 0.1.3 → 0.1.4

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
@@ -2952,6 +2952,8 @@ html[data-dsh-mobile] [data-slot="conversation.input.right"] { max-width: 100%;
2952
2952
  [data-mobile-toolbar] small { color: var(--dsw-alias-label-tertiary, #778195); font-size: 11px; display: block; text-align: center; }
2953
2953
  [data-mobile-toolbar] svg { display: block; }
2954
2954
  [data-mobile-shade] { position: absolute; inset: var(--mobile-bar) 0 0 min(88vw, 360px); border: 0; background: #15244330; pointer-events: auto; }
2955
+ /* The overlay layer rises above an open panel; the drawer's shade must not. */
2956
+ html[data-mobile-panel-open] [data-mobile-shade] { display: none; }
2955
2957
  [data-mobile-dialog] {
2956
2958
  color: var(--dsw-alias-label-primary, #152443); background: var(--dsw-alias-bg-base, white);
2957
2959
  border: 1px solid var(--dsw-alias-border-l3, #e6eaf2); border-radius: 28px;
@@ -3098,7 +3100,7 @@ html[data-dsh-mobile] [data-phase=hero] span:has(> [data-slot="conversation.hero
3098
3100
  [data-mobile-welcome] small { color:var(--mobile-accent); letter-spacing:3px; font-size:13px; }
3099
3101
  [data-mobile-welcome] h2 { font-size:25px; font-weight:500; line-height:1.4; margin:16px 0 9px; letter-spacing:-.6px; }
3100
3102
  [data-mobile-welcome] p { font-size:13px; line-height:1.6; margin:0; color:var(--dsw-alias-label-tertiary,#74767c); }
3101
- html[data-dsh-mobile]:has([data-phase=hero]) [data-mobile-toolbar][data-library=false] [data-mobile-new=true] { visibility:hidden; }
3103
+ html[data-dsh-mobile]:has([data-phase=hero]) [data-mobile-toolbar][data-library=false] [data-mobile-new=true] { display:none; }
3102
3104
  html[data-dsh-mobile] [data-phase=hero] div:has(> [data-slot="conversation.hero.workspace"]) { gap:6px; padding:6px 18px; flex-wrap:wrap; }
3103
3105
  html[data-dsh-mobile] [data-phase=hero] div:has(> [data-slot="conversation.hero.workspace"]) > button, html[data-dsh-mobile] [data-slot="conversation.hero.agentPreset"] button[aria-haspopup] { border-radius:18px; min-height:40px; font-size:13px; background:var(--mobile-soft); }
3104
3106
  /* Quiet, read-only metadata below the mobile title. */
@@ -3167,7 +3169,7 @@ html[data-dsh-mobile] [data-composer-seat]:has([data-mobile-room-queue]) [data-t
3167
3169
  [data-mobile-message-menu] button:active { background:var(--mobile-soft); }
3168
3170
  [data-mobile-message-menu] svg { width:20px; height:20px; flex:none; }
3169
3171
  [data-mobile-message-menu] button:disabled { opacity:.45; }
3170
- [data-mobile-toolbar]:has([data-mobile-members-open]) > [data-mobile-new] { display:none; }
3172
+ [data-mobile-toolbar]:has([data-mobile-members-open]) [data-mobile-new] { display:none; }
3171
3173
  [data-mobile-members-open] { position:relative; }
3172
3174
  [data-mobile-members-open] small { position:absolute; bottom:-3px; right:-2px; min-width:16px; border-radius:10px; font-size:10px; background:var(--mobile-soft); color:var(--dsw-alias-label-primary); }
3173
3175
  [data-mobile-room-dialog] { text-align:left; line-height:1.5; }
@@ -3196,6 +3198,10 @@ html[data-dsh-mobile] [data-slot="conversation.input.dock"] section > ul[aria-la
3196
3198
 
3197
3199
  /* One mobile reading scale; derived from host text size without writing its preference. */
3198
3200
  html[data-dsh-mobile] body { --mobile-text:max(17px,var(--dsh-content-font-size,14px)); --mobile-caption:max(14px,var(--dsh-content-font-size-secondary,13px)); }
3201
+ /* Touch values of the shared panel type tokens (ui-panel-kit reads them with
3202
+ desktop fallbacks; this package never imports it). Title and reading follow
3203
+ the mobile reading scale, so a larger host text size still wins. */
3204
+ html[data-dsh-mobile] body { --dsh-panel-text-title:var(--mobile-text); --dsh-panel-text-body:15px; --dsh-panel-text-meta:13px; --dsh-panel-text-reading:var(--mobile-text); --dsh-panel-reading-line-height:1.7; }
3199
3205
  html[data-dsh-mobile] :is([data-mobile-library],[data-mobile-dialog],[data-mobile-tools-dialog],[data-mobile-message-menu]) { font-size:var(--mobile-text); line-height:1.55; }
3200
3206
  html[data-dsh-mobile] :is([data-mobile-nav-title] strong,[data-mobile-session-copy] strong,[data-mobile-search] input,[data-composer-card] [contenteditable],[data-composer-seat] textarea,[data-mobile-member-chat] strong) { font-size:var(--mobile-text); line-height:1.55; }
3201
3207
  html[data-dsh-mobile] :is([data-mobile-workspace-group] strong,[data-mobile-tools-grid] button,[data-mobile-tools-permission],[data-mobile-dialog] h3,[data-mobile-grouping] button) { font-size:16px!important; }
@@ -3349,6 +3355,56 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
3349
3355
  [data-directory-location] input { width:18px; }
3350
3356
  [data-mobile-directory] footer { flex:none; padding-top:10px; }
3351
3357
  [data-mobile-directory] [data-mobile-done] { margin-top:0; background:var(--mobile-accent); color:white; min-height:48px; }
3358
+
3359
+ [data-mobile-message-menu] { -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
3360
+
3361
+ /* Optional sidebar navigation: content stays in its official dock hosts. */
3362
+ [data-mobile-header-actions] { display:flex; flex:none; gap:4px; }
3363
+ html[data-dsh-mobile] [data-mobile-toolbar] [data-mobile-header-actions] button { box-shadow:none; background:transparent; }
3364
+ /* Panel chrome has its own compact metric; a chat connection banner must not
3365
+ stretch the tabs. Tabs are flat labels; the active one carries an accent
3366
+ underline spanning its label and its close button. */
3367
+ html[data-dsh-mobile] { --mobile-panel-bar:52px; --mobile-panel-offset:52px; }
3368
+ html[data-dsh-mobile]:has([data-mobile-connection-status]) { --mobile-panel-offset:92px; }
3369
+ html[data-dsh-mobile][data-mobile-panel-open] [data-mobile-connection-status] { top:var(--mobile-panel-bar); }
3370
+ [data-mobile-panel-navigation] { position:absolute; inset:0 0 auto; height:var(--mobile-panel-bar,52px); display:flex; align-items:center; gap:4px; padding:0 6px; box-sizing:border-box; background:var(--dsw-alias-bg-layer-1,var(--mobile-soft)); color:var(--dsw-alias-label-primary); box-shadow:inset 0 -1px var(--dsw-alias-border-l2); pointer-events:auto; }
3371
+ [data-mobile-panel-navigation] button { display:grid; place-items:center; flex:none; width:40px; height:44px; padding:0; border:0; border-radius:10px; background:transparent; color:inherit; cursor:pointer; }
3372
+ [data-mobile-panel-navigation] > button { color:var(--dsw-alias-label-secondary); }
3373
+ [data-mobile-panel-navigation] button:disabled { opacity:.35; }
3374
+ [data-mobile-panel-navigation] button:focus-visible { outline:2px solid var(--mobile-accent); outline-offset:-3px; }
3375
+ [data-mobile-panel-tabs] { display:flex; align-self:stretch; align-items:stretch; flex:1; min-width:0; gap:2px; overflow-x:auto; overscroll-behavior-x:contain; scrollbar-width:none; touch-action:pan-x; }
3376
+ [data-mobile-panel-tabs]::-webkit-scrollbar { display:none; }
3377
+ [data-mobile-panel-tab] { position:relative; display:flex; flex:none; align-items:center; max-width:220px; color:var(--dsw-alias-label-secondary); }
3378
+ [data-mobile-panel-tab][data-active=true] { color:var(--dsw-alias-label-primary); }
3379
+ [data-mobile-panel-tab][data-active=true]::after { content:""; position:absolute; left:12px; right:5px; bottom:0; height:2.5px; border-radius:2px; background:var(--mobile-accent); pointer-events:none; }
3380
+ [data-mobile-panel-navigation] [role=tab] { flex:0 1 auto; min-width:0; width:auto; height:100%; padding:0 12px; font:inherit; font-size:15px; line-height:20px; border-radius:8px; }
3381
+ [data-mobile-panel-navigation] [role=tab] span { display:block; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
3382
+ [data-mobile-panel-navigation] [role=tab][aria-selected=true] { font-weight:600; padding-right:0; }
3383
+ [data-mobile-panel-navigation] [data-mobile-panel-tab-close] { width:24px; height:100%; border-radius:8px; color:var(--dsw-alias-label-tertiary); }
3384
+ @media (hover:hover) { [data-mobile-panel-navigation] button:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); } }
3385
+ [data-mobile-panel-error] { position:absolute; inset:100% 8px auto; padding:6px 10px; font-size:13px; background:var(--mobile-face); border:1px solid var(--mobile-edge); border-radius:8px; }
3386
+ html[data-mobile-panel-open] [data-mobile-toolbar] { visibility:hidden; }
3387
+ html[data-dsh-mobile][data-mobile-panel-open] [data-mobile-frame] > [data-shell-overlay] { z-index:65; }
3388
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] { top:var(--mobile-panel-offset,52px); width:100%!important; max-width:none!important; bottom:0; }
3389
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-strip],
3390
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-divider],
3391
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-float-grip],
3392
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-float-resize] { display:none!important; }
3393
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-split] { grid-template-columns:minmax(0,1fr)!important; }
3394
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-host] { grid-column:1!important; grid-row:1!important; min-width:0; }
3395
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-host=float] { width:100%; height:100%; }
3396
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-host]:not(:has([data-dockkit-pane-active],[data-dockkit-float-active])) { display:none!important; }
3397
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-float] { position:relative!important; inset:0!important; width:100%!important; height:100%!important; min-width:0; border-radius:0; }
3398
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel]:not([data-sidebar-right-open]) [data-dockkit-host=float] { visibility:hidden; pointer-events:none; }
3399
+ html[data-dsh-mobile][data-mobile-panel-managed] [data-sidebar-right-panel] [data-dockkit-content] { border-radius:0; min-width:0; }
3400
+ /* Fullscreen may be drawn from a collapsed, zero-width frame track. Anchor to
3401
+ the whole frame rather than its third grid area; saved desktop tracks stay intact. */
3402
+ html[data-dsh-mobile][data-mobile-panel-open] [data-mobile-frame] > [data-rightbar-col] {
3403
+ position:absolute; inset:0; grid-area:auto; z-index:24; width:100%; min-width:0;
3404
+ background:var(--dsw-alias-bg-base,white);
3405
+ }
3406
+ /* Keep the host affordance as fallback whenever the mobile entry is unavailable. */
3407
+ html[data-dsh-mobile]:has([data-mobile-toolbar] [data-mobile-panel-entry]) [data-sidebar-right-expand] { display:none; }
3352
3408
  `;
3353
3409
  //#endregion
3354
3410
  //#region src/client/surface.ts
@@ -3411,7 +3467,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
3411
3467
  const split = img.closest("button")?.parentElement;
3412
3468
  if (split && split.querySelectorAll("button").length === 2) this.mark(split, "data-mobile-header-hidden");
3413
3469
  }
3414
- 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");
3470
+ 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") ? this.doc.querySelector("[data-mobile-panel-entry]") !== null : /^(更多操作|More actions)$/i.test(button.getAttribute("aria-label") ?? "")) this.mark(button, "data-mobile-header-hidden");
3415
3471
  if (this.doc.querySelector("[data-mobile-members-open]:not([disabled])")) {
3416
3472
  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");
3417
3473
  }
@@ -3531,7 +3587,11 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
3531
3587
  this.style.dataset.mobileOwned = "styles";
3532
3588
  this.style.textContent = MOBILE_CSS;
3533
3589
  win.document.head.append(this.style);
3534
- this.observer = new MutationObserver(this.schedule);
3590
+ this.observer = new MutationObserver((records) => {
3591
+ if (records.some((record) => {
3592
+ return !(record.target instanceof Element ? record.target : record.target.parentElement)?.closest("[data-conversation-scroll] [data-slot=\"conversation.session\"]");
3593
+ })) this.schedule();
3594
+ });
3535
3595
  this.observer.observe(win.document.body, {
3536
3596
  subtree: true,
3537
3597
  childList: true,
@@ -3652,6 +3712,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
3652
3712
  //#endregion
3653
3713
  //#region src/client/MobileIcon.tsx
3654
3714
  const paths = {
3715
+ panel: "M4 4h16v16H4zM14 4v16",
3655
3716
  edit: "m15 5 4 4M5 19l4-1L20 7a2 2 0 0 0-4-4L5 14l-1 6Z",
3656
3717
  members: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M22 21v-2a4 4 0 0 0-3-3.87M9 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8m8 .13a4 4 0 0 1 0 7.75",
3657
3718
  plus: "M12 5v14M5 12h14",
@@ -4308,10 +4369,27 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4308
4369
  }
4309
4370
  publish(id, state) {
4310
4371
  if (!this.alive) return;
4372
+ const signature = JSON.stringify(state === null ? null : {
4373
+ members: state.members.map(({ name, kind, provider, instructions, cwd, childSessionId }) => [
4374
+ name,
4375
+ kind,
4376
+ provider,
4377
+ instructions,
4378
+ cwd,
4379
+ childSessionId
4380
+ ]),
4381
+ runs: state.runs.map(({ member, state }) => [member, state])
4382
+ });
4383
+ const previous = this.cache.get(id);
4384
+ if (previous?.signature === signature) {
4385
+ previous.at = Date.now();
4386
+ return;
4387
+ }
4311
4388
  this.cache.delete(id);
4312
4389
  this.cache.set(id, {
4313
4390
  state,
4314
- at: Date.now()
4391
+ at: Date.now(),
4392
+ signature
4315
4393
  });
4316
4394
  if (this.cache.size > 200) this.cache.delete(this.cache.keys().next().value);
4317
4395
  this.version++;
@@ -4655,7 +4733,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4655
4733
  doc.documentElement.removeAttribute("data-mobile-gesture");
4656
4734
  doc.documentElement.style.removeProperty("--mobile-drag-offset");
4657
4735
  };
4658
- const blocked = "input,textarea,[contenteditable],select,pre,code,[role=dialog],[role=menu],[role=slider],[role=switch],[role=checkbox],[data-mobile-tools-dialog],[data-mobile-dialog]";
4736
+ const blocked = "input,textarea,[contenteditable],select,pre,code,[role=dialog],[role=menu],[role=slider],[role=switch],[role=checkbox],[data-mobile-tools-dialog],[data-mobile-dialog],[data-mobile-gesture-owner],.xterm";
4659
4737
  const down = (event) => {
4660
4738
  resetDrag();
4661
4739
  start = void 0;
@@ -4669,6 +4747,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4669
4747
  const viewport = doc.documentElement.clientWidth || doc.defaultView.innerWidth;
4670
4748
  if (viewport >= 960) return;
4671
4749
  const right = target.closest("[data-sidebar-right-panel][data-sidebar-right-open]") ?? void 0;
4750
+ if (right && doc.documentElement.hasAttribute("data-mobile-panel-managed")) return;
4672
4751
  if (right && !right.querySelector("[data-sidebar-right-toggle]")) return;
4673
4752
  const sidebar = closeSidebar ? target.closest("[data-slot=sidebar]")?.parentElement : void 0;
4674
4753
  const panel = right ? {
@@ -4857,6 +4936,141 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
4857
4936
  }), document.body);
4858
4937
  }
4859
4938
  //#endregion
4939
+ //#region src/client/MobilePanelNavigation.tsx
4940
+ function MobilePanelNavigation({ panels, active, prepareNavigation, t }) {
4941
+ const state = (0, react.useSyncExternalStore)(panels.subscribe, panels.getSnapshot);
4942
+ const managed = active && state.managed;
4943
+ const navigation = (0, react.useRef)(null);
4944
+ const prepare = (0, react.useRef)(prepareNavigation);
4945
+ prepare.current = prepareNavigation;
4946
+ (0, react.useEffect)(() => {
4947
+ document.documentElement.toggleAttribute("data-mobile-panel-managed", managed);
4948
+ document.documentElement.toggleAttribute("data-mobile-panel-open", managed && state.expanded);
4949
+ return () => {
4950
+ document.documentElement.removeAttribute("data-mobile-panel-managed");
4951
+ document.documentElement.removeAttribute("data-mobile-panel-open");
4952
+ };
4953
+ }, [managed, state.expanded]);
4954
+ (0, react.useEffect)(() => {
4955
+ if (!managed || !state.expanded) return;
4956
+ const main = document.querySelector("[data-mobile-frame] > div:has(> [data-slot=\"main\"])");
4957
+ const previous = main?.inert;
4958
+ if (main) main.inert = true;
4959
+ const nav = navigation.current;
4960
+ nav?.querySelector("button")?.focus({ preventScroll: true });
4961
+ const escape = (event) => {
4962
+ if (event.key !== "Escape" || event.defaultPrevented || document.querySelector("dialog[open],[role=dialog],[role=menu]") || document.activeElement?.tagName === "SELECT") return;
4963
+ prepare.current();
4964
+ panels.hide();
4965
+ };
4966
+ window.addEventListener("keydown", escape);
4967
+ return () => {
4968
+ if (main) main.inert = previous ?? false;
4969
+ window.removeEventListener("keydown", escape);
4970
+ if (document.activeElement === document.body || document.activeElement && nav?.contains(document.activeElement)) document.querySelector("[data-mobile-panel-entry]")?.focus({ preventScroll: true });
4971
+ };
4972
+ }, [
4973
+ managed,
4974
+ state.expanded,
4975
+ panels
4976
+ ]);
4977
+ (0, react.useEffect)(() => {
4978
+ if (!managed || !state.expanded) return;
4979
+ navigation.current?.querySelector("[data-mobile-panel-tab][data-active=true]")?.scrollIntoView?.({
4980
+ block: "nearest",
4981
+ inline: "nearest"
4982
+ });
4983
+ }, [
4984
+ managed,
4985
+ state.expanded,
4986
+ state.activeId
4987
+ ]);
4988
+ if (!managed || !state.expanded) return null;
4989
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
4990
+ ref: navigation,
4991
+ "data-mobile-panel-navigation": true,
4992
+ "aria-label": t("panels"),
4993
+ children: [
4994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4995
+ type: "button",
4996
+ "aria-label": t("backToChat"),
4997
+ onClick: () => {
4998
+ prepareNavigation();
4999
+ panels.hide();
5000
+ },
5001
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileIcon, { name: "back" })
5002
+ }),
5003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5004
+ role: "tablist",
5005
+ "aria-label": t("switchPanel"),
5006
+ "data-mobile-panel-tabs": true,
5007
+ onKeyDown: (event) => {
5008
+ const index = state.tabs.findIndex((tab) => tab.id === state.activeId);
5009
+ const next = event.key === "ArrowRight" ? (index + 1) % state.tabs.length : event.key === "ArrowLeft" ? (index + state.tabs.length - 1) % state.tabs.length : event.key === "Home" ? 0 : event.key === "End" ? state.tabs.length - 1 : -1;
5010
+ const target = state.tabs[next];
5011
+ if (!target) return;
5012
+ event.preventDefault();
5013
+ panels.select(target.id);
5014
+ event.currentTarget.querySelectorAll("[role=tab]")[next]?.focus({ preventScroll: true });
5015
+ },
5016
+ children: state.tabs.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5017
+ role: "presentation",
5018
+ "data-mobile-panel-tab": true,
5019
+ "data-active": tab.id === state.activeId,
5020
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5021
+ type: "button",
5022
+ role: "tab",
5023
+ title: tab.title || tab.kind,
5024
+ "aria-selected": tab.id === state.activeId,
5025
+ tabIndex: tab.id === state.activeId ? 0 : -1,
5026
+ onClick: () => panels.select(tab.id),
5027
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tab.title || tab.kind })
5028
+ }), tab.id === state.activeId && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5029
+ type: "button",
5030
+ "data-mobile-panel-tab-close": true,
5031
+ "aria-label": t("closePanel"),
5032
+ disabled: state.tabs.length === 1 && tab.kind === "guide",
5033
+ onClick: panels.close,
5034
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileIcon, {
5035
+ name: "close",
5036
+ size: 14
5037
+ })
5038
+ })]
5039
+ }, tab.id))
5040
+ }),
5041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5042
+ type: "button",
5043
+ "aria-label": t("addPanel"),
5044
+ onClick: panels.add,
5045
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileIcon, {
5046
+ name: "plus",
5047
+ size: 20
5048
+ })
5049
+ }),
5050
+ state.failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5051
+ role: "alert",
5052
+ "data-mobile-panel-error": true,
5053
+ children: t("panelError")
5054
+ })
5055
+ ]
5056
+ });
5057
+ }
5058
+ function MobilePanelEntry({ panels, beforeOpen, t }) {
5059
+ const state = (0, react.useSyncExternalStore)(panels.subscribe, panels.getSnapshot);
5060
+ if (!state.available) return null;
5061
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5062
+ type: "button",
5063
+ "data-mobile-panel-entry": true,
5064
+ "aria-label": t("panels"),
5065
+ "aria-expanded": state.expanded,
5066
+ onClick: () => {
5067
+ beforeOpen();
5068
+ panels.toggle();
5069
+ },
5070
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileIcon, { name: "panel" })
5071
+ });
5072
+ }
5073
+ //#endregion
4860
5074
  //#region src/client/catalogReads.ts
4861
5075
  /** Bound optional catalog enrichment so cold log reads cannot fan out across the library. */
4862
5076
  var CatalogReads = class {
@@ -5191,7 +5405,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5191
5405
  ] });
5192
5406
  }
5193
5407
  /** One visible navigation bar, with native connection sheets when supported. */
5194
- function MobileChrome({ presentation, toggleSidebar, connection, navigation, rooms, t }) {
5408
+ function MobileChrome({ presentation, toggleSidebar, connection, navigation, rooms, panels, t }) {
5195
5409
  const state = (0, react.useSyncExternalStore)(presentation.subscribe, presentation.getSnapshot);
5196
5410
  const wire = (0, react.useSyncExternalStore)(connection.state.subscribe, connection.state.getSnapshot);
5197
5411
  const available = (0, react.useSyncExternalStore)(navigation.subscribe, navigation.getSnapshot);
@@ -5226,8 +5440,15 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5226
5440
  if (available && !activated.current) {
5227
5441
  activated.current = true;
5228
5442
  setLibrary(true);
5229
- if (state.drawer) toggleSidebar();
5230
5443
  }
5444
+ }, [state.active, available]);
5445
+ const drawerWanted = (0, react.useRef)(false);
5446
+ (0, react.useEffect)(() => {
5447
+ if (!state.drawer) {
5448
+ drawerWanted.current = false;
5449
+ return;
5450
+ }
5451
+ if (state.active && available && !drawerWanted.current) toggleSidebar();
5231
5452
  }, [
5232
5453
  state.active,
5233
5454
  state.drawer,
@@ -5275,6 +5496,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5275
5496
  toggleSidebar
5276
5497
  ]);
5277
5498
  const openLibrary = () => {
5499
+ panels?.hide();
5278
5500
  if (!available) {
5279
5501
  toggleSidebar();
5280
5502
  return;
@@ -5283,6 +5505,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5283
5505
  setLibrary(true);
5284
5506
  };
5285
5507
  const startSession = () => {
5508
+ panels?.hide();
5286
5509
  presentation.prepareNavigation();
5287
5510
  available?.workspace.startSession();
5288
5511
  if (state.drawer) toggleSidebar();
@@ -5380,11 +5603,22 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5380
5603
  children: t(wire ?? "connecting")
5381
5604
  })]
5382
5605
  }),
5383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5384
- "data-mobile-new": !!available,
5385
- "aria-label": available ? t("newSession") : t("settings"),
5386
- onClick: available ? startSession : openSettings,
5387
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileIcon, { name: available ? "compose" : "settings" })
5606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5607
+ "data-mobile-header-actions": true,
5608
+ children: [panels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobilePanelEntry, {
5609
+ panels,
5610
+ t,
5611
+ beforeOpen: () => {
5612
+ presentation.prepareNavigation();
5613
+ setLibrary(false);
5614
+ if (state.drawer) toggleSidebar();
5615
+ }
5616
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5617
+ "data-mobile-new": !!available,
5618
+ "aria-label": available ? t("newSession") : t("settings"),
5619
+ onClick: available ? startSession : openSettings,
5620
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileIcon, { name: available ? "compose" : "settings" })
5621
+ })]
5388
5622
  }),
5389
5623
  available && rooms && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobileRoomNavigation, {
5390
5624
  rooms,
@@ -5394,6 +5628,12 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5394
5628
  })
5395
5629
  ]
5396
5630
  }),
5631
+ panels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MobilePanelNavigation, {
5632
+ panels,
5633
+ active: state.active,
5634
+ prepareNavigation: presentation.prepareNavigation,
5635
+ t
5636
+ }),
5397
5637
  wire !== "connected" && connectionExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5398
5638
  "data-mobile-connection-status": true,
5399
5639
  role: "status",
@@ -5420,7 +5660,10 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5420
5660
  onClick: () => {
5421
5661
  setSettings(false);
5422
5662
  setLibrary(false);
5423
- if (!state.drawer) toggleSidebar();
5663
+ if (!state.drawer) {
5664
+ drawerWanted.current = true;
5665
+ toggleSidebar();
5666
+ }
5424
5667
  },
5425
5668
  children: t("workspaces")
5426
5669
  }),
@@ -5465,6 +5708,128 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5465
5708
  ] });
5466
5709
  }
5467
5710
  //#endregion
5711
+ //#region src/client/panels.ts
5712
+ const EMPTY = {
5713
+ available: false,
5714
+ managed: false,
5715
+ expanded: false,
5716
+ sessionId: void 0,
5717
+ activeId: "",
5718
+ tabs: [],
5719
+ failed: false
5720
+ };
5721
+ /** Public services do actions; checked DOM anchors supply presentation-only change notifications. */
5722
+ var MobilePanels = class {
5723
+ doc;
5724
+ service;
5725
+ releases = [];
5726
+ listeners = /* @__PURE__ */ new Set();
5727
+ snapshot = EMPTY;
5728
+ observer;
5729
+ disposed = false;
5730
+ scheduled = false;
5731
+ failed = false;
5732
+ constructor(doc) {
5733
+ this.doc = doc;
5734
+ this.observer = new MutationObserver((records) => {
5735
+ if (this.scheduled || !records.some((record) => {
5736
+ return !(record.target instanceof Element ? record.target : record.target.parentElement)?.closest("[data-conversation-scroll] [data-slot=\"conversation.session\"]");
5737
+ })) return;
5738
+ this.scheduled = true;
5739
+ queueMicrotask(() => {
5740
+ this.scheduled = false;
5741
+ if (!this.disposed) this.sync();
5742
+ });
5743
+ });
5744
+ this.observer.observe(doc.body, {
5745
+ subtree: true,
5746
+ childList: true,
5747
+ attributes: true,
5748
+ attributeFilter: [
5749
+ "data-sidebar-right-open",
5750
+ "data-dockkit-pane-active",
5751
+ "data-dockkit-float-active",
5752
+ "aria-selected",
5753
+ "hidden"
5754
+ ]
5755
+ });
5756
+ }
5757
+ getSnapshot = () => this.snapshot;
5758
+ subscribe = (listener) => {
5759
+ this.listeners.add(listener);
5760
+ return () => {
5761
+ this.listeners.delete(listener);
5762
+ };
5763
+ };
5764
+ set(value) {
5765
+ for (const release of this.releases) release();
5766
+ this.releases = [];
5767
+ const service = value;
5768
+ this.service = service && typeof service.toggleExpanded === "function" && typeof service.isExpanded === "function" && typeof service.active === "function" && typeof service.mounted?.subscribe === "function" && typeof service.mounted.getSnapshot === "function" ? service : void 0;
5769
+ this.failed = false;
5770
+ if (this.service) {
5771
+ this.releases.push(this.service.mounted.subscribe(this.sync));
5772
+ if (typeof this.service.openTabs?.subscribe === "function") this.releases.push(this.service.openTabs.subscribe(this.sync));
5773
+ }
5774
+ this.sync();
5775
+ }
5776
+ sync = () => {
5777
+ let next = EMPTY;
5778
+ try {
5779
+ const service = this.service, sessionId = service?.mounted.getSnapshot();
5780
+ if (service && sessionId) {
5781
+ const panel = Array.from(this.doc.querySelectorAll("[data-sidebar-right-panel][data-sidebar-right-session]")).find((element) => element.dataset.sidebarRightSession === sessionId);
5782
+ const tabs = service.tabsIn?.(sessionId) ?? [];
5783
+ const active = service.active();
5784
+ next = {
5785
+ available: true,
5786
+ managed: !!panel && !!panel.querySelector("[data-dockkit-host]") && tabs.length > 0 && typeof service.focus === "function" && typeof service.close === "function" && typeof service.openTab === "function" && !!active && tabs.some((tab) => tab.id === active.id),
5787
+ sessionId,
5788
+ expanded: service.isExpanded(),
5789
+ activeId: active?.id ?? "",
5790
+ tabs: tabs.map(({ id, title, kind }) => ({
5791
+ id,
5792
+ title,
5793
+ kind
5794
+ })),
5795
+ failed: this.failed
5796
+ };
5797
+ }
5798
+ } catch {}
5799
+ if (JSON.stringify(next) !== JSON.stringify(this.snapshot)) {
5800
+ this.snapshot = next;
5801
+ for (const listener of this.listeners) listener();
5802
+ }
5803
+ };
5804
+ run(action) {
5805
+ if (!this.service || !this.snapshot.available) return;
5806
+ try {
5807
+ action(this.service);
5808
+ this.failed = false;
5809
+ } catch {
5810
+ this.failed = true;
5811
+ }
5812
+ this.sync();
5813
+ }
5814
+ toggle = () => this.run((service) => service.toggleExpanded());
5815
+ hide = () => this.run((service) => {
5816
+ if (service.isExpanded()) service.toggleExpanded();
5817
+ });
5818
+ select = (id) => this.run((service) => {
5819
+ if (this.snapshot.tabs.some((tab) => tab.id === id)) service.focus?.(id);
5820
+ });
5821
+ close = () => this.run((service) => {
5822
+ if (this.snapshot.activeId) service.close?.(this.snapshot.activeId);
5823
+ });
5824
+ add = () => this.run((service) => service.openTab?.("guide"));
5825
+ dispose() {
5826
+ this.disposed = true;
5827
+ this.observer.disconnect();
5828
+ this.set(void 0);
5829
+ this.listeners.clear();
5830
+ }
5831
+ };
5832
+ //#endregion
5468
5833
  //#region src/client/MobileQueue.tsx
5469
5834
  /** Room's takeover hides the official QueueDock. Restore its mutation verbs
5470
5835
  * through the same scoped Conversation API, without touching the Room composer. */
@@ -5626,20 +5991,17 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5626
5991
  }
5627
5992
  //#endregion
5628
5993
  //#region src/client/messageMenu.ts
5629
- /** A touch presentation of the rendered message's own actions. The source
5994
+ /** A pointer context-menu presentation of the rendered message's own actions. The source
5630
5995
  * button stays mounted; its plugin still owns edits, confirmations and errors. */
5631
5996
  var MessageMenu = class {
5632
5997
  doc;
5633
- timer;
5634
- point;
5998
+ touch = false;
5635
5999
  menu;
5636
6000
  constructor(doc) {
5637
6001
  this.doc = doc;
5638
6002
  doc.addEventListener("pointerdown", this.down, true);
5639
- doc.addEventListener("pointermove", this.move, true);
5640
- doc.addEventListener("pointerup", this.cancel, true);
5641
- doc.addEventListener("pointercancel", this.cancel, true);
5642
- doc.addEventListener("scroll", this.cancel, true);
6003
+ doc.addEventListener("selectionchange", this.selectionChanged);
6004
+ doc.addEventListener("scroll", this.close, true);
5643
6005
  doc.addEventListener("contextmenu", this.context, true);
5644
6006
  }
5645
6007
  row(target) {
@@ -5652,28 +6014,15 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5652
6014
  actions(row) {
5653
6015
  return [...row.querySelectorAll(row.hasAttribute("data-turn-tail") ? "button[aria-label]" : ":scope > div > button[aria-label], :scope > div > span > button[aria-label]")].filter((b) => /^(复制|已复制|编辑|撤回|点赞|点踩|不喜欢|分支|好的回答|有问题的回答|在新对话中分支|Copy|Copied|Edit|Withdraw|Like|Dislike|Branch)/i.test(b.getAttribute("aria-label") ?? ""));
5654
6016
  }
5655
- down = (e) => {
5656
- this.cancel();
5657
- const row = this.row(e.target);
5658
- if (!row || e.pointerType === "mouse" || !this.actions(row).length) return;
5659
- this.point = {
5660
- x: e.clientX,
5661
- y: e.clientY,
5662
- row
5663
- };
5664
- this.timer = setTimeout(() => {
5665
- if (this.point) this.open(row, e.clientX, e.clientY);
5666
- }, 500);
5667
- };
5668
- move = (e) => {
5669
- if (this.point && Math.hypot(e.clientX - this.point.x, e.clientY - this.point.y) > 10) this.cancel();
6017
+ down = (event) => {
6018
+ this.touch = event.pointerType === "touch" || event.pointerType === "pen";
5670
6019
  };
5671
- cancel = () => {
5672
- clearTimeout(this.timer);
5673
- this.timer = void 0;
5674
- this.point = void 0;
6020
+ hasSelection = () => !!this.doc.getSelection()?.toString().trim();
6021
+ selectionChanged = () => {
6022
+ if (this.hasSelection()) this.close();
5675
6023
  };
5676
6024
  context = (e) => {
6025
+ if (this.touch || this.hasSelection()) return;
5677
6026
  const row = this.row(e.target);
5678
6027
  if (row && this.actions(row).length) {
5679
6028
  e.preventDefault();
@@ -5686,7 +6035,6 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5686
6035
  this.menu = void 0;
5687
6036
  };
5688
6037
  open(row, x, y) {
5689
- this.cancel();
5690
6038
  this.close();
5691
6039
  const actions = this.actions(row);
5692
6040
  if (!actions.length) return;
@@ -5722,13 +6070,10 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5722
6070
  menu.style.top = `${Math.max(12, Math.min(y + 12, height - bounds.height - 12))}px`;
5723
6071
  }
5724
6072
  dispose() {
5725
- this.cancel();
5726
6073
  this.close();
5727
6074
  this.doc.removeEventListener("pointerdown", this.down, true);
5728
- this.doc.removeEventListener("pointermove", this.move, true);
5729
- this.doc.removeEventListener("pointerup", this.cancel, true);
5730
- this.doc.removeEventListener("pointercancel", this.cancel, true);
5731
- this.doc.removeEventListener("scroll", this.cancel, true);
6075
+ this.doc.removeEventListener("selectionchange", this.selectionChanged);
6076
+ this.doc.removeEventListener("scroll", this.close, true);
5732
6077
  this.doc.removeEventListener("contextmenu", this.context, true);
5733
6078
  }
5734
6079
  };
@@ -5872,6 +6217,12 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
5872
6217
  /** Copy for controls owned by mobile; all conversation copy stays with its owner. */
5873
6218
  const NS = "mobile";
5874
6219
  const en = {
6220
+ panels: "Panels",
6221
+ backToChat: "Back to conversation",
6222
+ switchPanel: "Switch panel",
6223
+ addPanel: "Add panel",
6224
+ closePanel: "Close current panel",
6225
+ panelError: "Could not change this panel. Try again.",
5875
6226
  connectPhone: "Connect phone",
5876
6227
  connectIntro: "Continue your conversations in DSH Mobile.",
5877
6228
  connectQR: "Scan with DSH Mobile to sign in",
@@ -6009,6 +6360,12 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
6009
6360
  cancel: "Cancel"
6010
6361
  };
6011
6362
  const zh = {
6363
+ panels: "面板",
6364
+ backToChat: "返回聊天",
6365
+ switchPanel: "切换面板",
6366
+ addPanel: "添加面板",
6367
+ closePanel: "关闭当前面板",
6368
+ panelError: "暂时无法操作面板,请重试。",
6012
6369
  connectPhone: "连接手机",
6013
6370
  connectIntro: "在 DSH Mobile 中继续你的会话。",
6014
6371
  connectQR: "使用 DSH Mobile 扫码登录",
@@ -6172,6 +6529,12 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
6172
6529
  const messages = new MessageMenu(document);
6173
6530
  ctx.effect(() => () => messages.dispose(), "mobile: message actions");
6174
6531
  const navigation = new MobileNavigation();
6532
+ const panels = new MobilePanels(document);
6533
+ ctx.effect(() => () => panels.dispose(), "mobile: optional panels");
6534
+ ctx.inject(["sidebarRight"], (scoped) => {
6535
+ panels.set(scoped.get("sidebarRight"));
6536
+ scoped.effect(() => () => panels.set(void 0), "mobile: sidebar capability");
6537
+ });
6175
6538
  const directory = new MobileDirectory();
6176
6539
  const presentation = new MobilePresentation(window, window.__DSH_MOBILE_SHELL__?.bridgeVersion === 1);
6177
6540
  ctx.inject([
@@ -6236,6 +6599,7 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
6236
6599
  connection,
6237
6600
  navigation,
6238
6601
  rooms,
6602
+ panels,
6239
6603
  toggleSidebar: () => {
6240
6604
  ctx.layout.toggleSidebar();
6241
6605
  }
@@ -6248,11 +6612,15 @@ html[data-dsh-mobile] [data-mobile-picker=preset] [data-mobile-picker-divider] {
6248
6612
  ctx.slots.inject("conversation.composer", () => {
6249
6613
  let remove;
6250
6614
  let timer;
6615
+ let lastRoom;
6251
6616
  const refresh = () => {
6252
6617
  const list = navigation.getSnapshot()?.sessions.list.getSnapshot();
6253
6618
  const current = list === void 0 ? void 0 : mainSessionId(list);
6254
- if (!presentation.getSnapshot().active || !current || !rooms.get(current)) return;
6619
+ const room = current && rooms.get(current) ? current : void 0;
6620
+ if (!presentation.getSnapshot().active || room === lastRoom) return;
6621
+ lastRoom = room;
6255
6622
  clearTimeout(timer);
6623
+ if (!room) return;
6256
6624
  timer = setTimeout(() => {
6257
6625
  remove?.();
6258
6626
  remove = ctx.slots.register({