dsh-zen-remote 1.1.7 → 1.1.9

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/AGENTS.md CHANGED
@@ -57,3 +57,23 @@ pnpm test # 网关 node:test 用例 + 三个界面自检脚本 + 文档版
57
57
  - **`lib/` 是产物,不手改**:改 `src/` → `pnpm build` → 提交 `lib/`。
58
58
  - **桌面必须 no-op**:≥1024px 逐像素与未安装时一致。
59
59
  是从真机事故里攒出来的,改相关代码前先读。
60
+ - **要藏宿主刚渲染出来的东西,别用 `requestAnimationFrame`**:React 在该帧的
61
+ rAF 阶段之后才提交 DOM,所以从 mutation 里排的 rAF 落在**下一帧**——中间那
62
+ 一帧已经画到屏幕上,用户就看见闪。两个月内三次「晚一帧」的 bug(2026-08-18
63
+ 回复要等回合结束才出现、08-22 工具调用行先显后折、08-25 issue #3 流式抖动)
64
+ 都是这个根子。两条出路:能用选择器表达的交给样式表(`turn-fold.css.ts` 的
65
+ `BORN_FOLDED`,读宿主自己的 `data-chat-flow-kind` / `data-variant`,React
66
+ 插入节点的同一次提交里就带着,第一帧就是隐藏的);表达不了的放进
67
+ MutationObserver 回调里同步做(`turn-fold.ts` 的 `markWholeRows`)——回调是
68
+ 微任务,跑在这一帧渲染更新之前,两个方向都不会晚。rAF 只留给能容忍晚一帧的
69
+ 活(全量重扫、插 chip、改文案)。
70
+ - **别用 `:has()` 写死宿主的 DOM 层数**:包裹层数不等于组件源码看上去的层数
71
+ ——槽位会额外套一层(真实是 `flowItem > seat > AssistantMarkdown root >
72
+ body > 块`,比组件自身结构多一层)。深度猜错的选择器**静默失效**:测试全绿、
73
+ 控制台干净,只有拿真界面量才看得出来。需要结构判断就用 JS 遍历
74
+ (`foldsWholeRow` 那种沿父节点上溯的写法),不假设层数。
75
+ - **改了界面就去真界面上量**:`turn-fold` 这类规则用自建的静态页面验证会给出
76
+ 假阳性(合成 DOM 猜不对宿主的包裹层)。打开 `127.0.0.1:3080` 读现有会话的
77
+ `getComputedStyle` 即可,**不要为了验证去建会话或发消息**(耗真实 token,见
78
+ 工作区 AGENTS.md 的硬约束)。注意浏览器标签在后台时 rAF 不触发,chip 会是 0
79
+ 个——那是观测假象,不是回归,截图把窗口唤到前台再看。
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  <p align="center">
5
5
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="MIT"></a>
6
- <img src="https://img.shields.io/badge/release-v1.1.7-5B4CF0?style=flat-square" alt="v1.1.7">
6
+ <img src="https://img.shields.io/badge/release-v1.1.9-5B4CF0?style=flat-square" alt="v1.1.9">
7
7
  <img src="https://img.shields.io/badge/DSH-Web%20Profile-5B4CF0?style=flat-square" alt="DSH Web Profile">
8
8
  </p>
9
9
 
@@ -39,7 +39,7 @@ dsh plugin add dsh-zen-remote
39
39
  ```jsonc
40
40
  {
41
41
  "dependencies": {
42
- "dsh-zen-remote": "^1.1.7" // 本地开发换成 "link:/path/to/dsh-zen-remote"
42
+ "dsh-zen-remote": "^1.1.9" // 本地开发换成 "link:/path/to/dsh-zen-remote"
43
43
  },
44
44
  "dsh": { "profile": { "bundles": [
45
45
  "@deepseek-ai/dsh-base",
package/docs/interface.md CHANGED
@@ -239,13 +239,12 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
239
239
 
240
240
  ## 兼容插件
241
241
 
242
- - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(移动端全宽抽屉,本插件在会话页头部给它留入口位)——本机实测 **0.15.0**
242
+ - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(移动端全宽抽屉,本插件在会话页头部给它留入口位;手机上文件树里点 @ 引用后代点开关自动收起面板——面板全屏盖住会话页,草稿变化看不见,收起本身就是点击反馈)——本机实测 **0.15.0**
243
243
  - [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.9**
244
244
  - [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开;旧包名 `dsh-usage-stats` 已废弃,两个名字同时留在 profile 里会因重复的 `usage-stats` 行让 DSH 起不来)——本机实测 **0.2.9**
245
245
  - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.3**
246
246
  - dsh-at-file(@文件引用;它和本插件的附件 chip 读的是同一份草稿 token,手机端会为一个上传文件同时画出缩略图和文件名两条,故隐藏它 `.dsh-uploads/` 下那几行,其余 @ 引用不动;桌面端不隐藏——那边本插件的 chip 是关掉的,它是唯一的呈现)——本机实测 **0.6.7**
247
247
  - [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market)(插件市场;它的弹窗顶栏在手机上放不下,关闭的 × 被挤出面板外,故隐藏标语/版本号/官网链接三个装饰位并让标题省略号收缩,语言切换和 × 保留;认插件自己渲染的 `.dshm-ver[title="@ace-zone/dsh-market"]` 版本片,同名的其他"市场"插件不会被误伤)——本机实测 **0.1.66**
248
- - [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit)(图像 Q&A/OCR,配合手机端附件上传使用)
249
248
  - [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all)(文件树 / 预览 / 任务看板 / SSH / 宠物 / 会话统计 / 远程配对 / 设置)——沿用上游兼容规则,本次未扩展
250
249
 
251
250
  ## 安装
package/lib/client.js CHANGED
@@ -885,6 +885,99 @@ const session_dot_ts_1 = require("./session-dot.js");
885
885
  const MobileHomeChips_tsx_1 = require("./MobileHomeChips.js");
886
886
  /** Phone breakpoint: below the tablet range, where the app-shell layout applies. */
887
887
  const PHONE_QUERY = '(max-width: 767px)';
888
+ /** Swipe geometry: reveal width of the archive action / settle threshold. */
889
+ const SWIPE_REVEAL = 88;
890
+ const SWIPE_SETTLE = 44;
891
+ /**
892
+ * Direction-lock slop: total displacement before the gesture commits to
893
+ * horizontal (card drag) or vertical (native scroll) — decided ONCE per
894
+ * touch, the way native swipe rows do it. Before the lock nothing moves.
895
+ */
896
+ const SWIPE_SLOP = 10;
897
+ /** Horizontal wins only when clearly flatter than ~35° (|dx| > 1.4·|dy|). */
898
+ const SWIPE_SLOPE = 1.4;
899
+ /**
900
+ * One session row with swipe-left-to-archive (user request, 2026-08-25;
901
+ * interaction redone after real-device feedback the same day).
902
+ *
903
+ * Native-pattern mechanics:
904
+ * - **One-shot direction lock.** The first ~10px of displacement decides the
905
+ * whole gesture: clearly-flat movement (|dx| > 1.4·|dy|) locks to the card
906
+ * drag, anything else locks to native vertical scroll and the card never
907
+ * moves again for that touch. The first version re-judged every move
908
+ * sample, so a curvy upward scroll could flip mid-gesture into a drag.
909
+ * - **Progressive reveal, iOS-mail style.** The wrapper clips (overflow:
910
+ * hidden) and the archive button is PARKED past the right edge, sliding in
911
+ * lock-step with the card (button shift = reveal + card shift). Nothing is
912
+ * pre-painted under the card, so a 1px drag shows a 1px slice of button
913
+ * edge — not a fully drawn button popping through a gap.
914
+ * - At most one row open at a time (parent owns `open`); tapping an open or
915
+ * mid-drag card closes it instead of navigating.
916
+ *
917
+ * Touch-only by design — the list only exists on the phone breakpoint.
918
+ */
919
+ function SwipeRow({ open, onOpenChange, onArchive, archiveLabel, children }) {
920
+ const start = (0, react_1.useRef)(null);
921
+ const lock = (0, react_1.useRef)(null);
922
+ const [drag, setDrag] = (0, react_1.useState)(null);
923
+ // Ref mirror of `drag`: touchend must read the LATEST position even when
924
+ // the browser delivers move+end in one task (coalesced touch events) —
925
+ // the state value in this render's closure can be a frame stale.
926
+ const dragRef = (0, react_1.useRef)(null);
927
+ const moveTo = (value) => {
928
+ dragRef.current = value;
929
+ setDrag(value);
930
+ };
931
+ const base = open ? -SWIPE_REVEAL : 0;
932
+ const shift = drag ?? base;
933
+ const dragging = drag !== null;
934
+ const onTouchStart = (e) => {
935
+ const t = e.touches[0];
936
+ if (t === undefined)
937
+ return;
938
+ start.current = { x: t.clientX, y: t.clientY };
939
+ lock.current = null;
940
+ };
941
+ const onTouchMove = (e) => {
942
+ const s = start.current;
943
+ const t = e.touches[0];
944
+ if (s === null || t === undefined)
945
+ return;
946
+ const dx = t.clientX - s.x;
947
+ const dy = t.clientY - s.y;
948
+ if (lock.current === null) {
949
+ if (Math.hypot(dx, dy) < SWIPE_SLOP)
950
+ return;
951
+ lock.current = Math.abs(dx) > SWIPE_SLOPE * Math.abs(dy) ? 'h' : 'v';
952
+ if (lock.current === 'v')
953
+ return;
954
+ // Re-anchor so the card starts moving from here, not with a 10px jump.
955
+ start.current = { x: t.clientX, y: t.clientY };
956
+ return;
957
+ }
958
+ if (lock.current === 'v')
959
+ return;
960
+ moveTo(Math.max(-SWIPE_REVEAL, Math.min(0, base + dx)));
961
+ };
962
+ const onTouchEnd = () => {
963
+ start.current = null;
964
+ if (lock.current !== 'h')
965
+ return;
966
+ const settled = (dragRef.current ?? base) < -SWIPE_SETTLE;
967
+ moveTo(null);
968
+ onOpenChange(settled);
969
+ };
970
+ const guardClick = (navigate) => {
971
+ // A tap on an open (or mid-drag) card closes it; navigation needs a
972
+ // second, clean tap. Matches every list app's swipe convention.
973
+ if (open || lock.current === 'h')
974
+ onOpenChange(false);
975
+ else
976
+ navigate();
977
+ };
978
+ const motion = dragging ? { transition: 'none' } : {};
979
+ return ((0, jsx_runtime_1.jsxs)("li", { "data-mobile-nav": "home-swipe", onTouchStart: onTouchStart, onTouchMove: onTouchMove, onTouchEnd: onTouchEnd, onTouchCancel: onTouchEnd, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "home-archive", tabIndex: open ? 0 : -1, "aria-hidden": !open, style: { transform: `translateX(${SWIPE_REVEAL + shift}px)`, ...motion }, onClick: onArchive, children: archiveLabel }), (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "home-swipe-card", style: { transform: `translateX(${shift}px)`, ...motion }, children: children(guardClick) })] }));
980
+ }
888
981
  /** Live matchMedia hook for the phone breakpoint. */
889
982
  function usePhone() {
890
983
  const [phone, setPhone] = (0, react_1.useState)(() => window.matchMedia(PHONE_QUERY).matches);
@@ -959,7 +1052,7 @@ function statusLine(row, dot, t) {
959
1052
  * all navigation from the injected official actions; nothing here reads the
960
1053
  * official DOM.
961
1054
  */
962
- function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession, startSession, downloadSessionLog, t, }) {
1055
+ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession, startSession, downloadSessionLog, archiveSession, t, }) {
963
1056
  const phone = usePhone();
964
1057
  const iconHref = useSiteIconHref();
965
1058
  const [iconBroken, setIconBroken] = (0, react_1.useState)(false);
@@ -970,6 +1063,19 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
970
1063
  const sessions = useSessions((s) => s);
971
1064
  const workspaces = useWorkspaces((s) => s);
972
1065
  const [sheet, setSheet] = (0, react_1.useState)(null);
1066
+ /** The one row whose archive action is swiped open (null = none). */
1067
+ const [swipeOpen, setSwipeOpen] = (0, react_1.useState)(null);
1068
+ /** Archive with the same confirm gate the session-info card uses. */
1069
+ const onArchiveRow = (id) => {
1070
+ if (!window.confirm(t('infoArchiveConfirm'))) {
1071
+ setSwipeOpen(null);
1072
+ return;
1073
+ }
1074
+ setSwipeOpen(null);
1075
+ // The store removes the row reactively (archivedSessionIds frame echo);
1076
+ // failures leave the row in place, which is the honest outcome.
1077
+ void archiveSession(id);
1078
+ };
973
1079
  // Workspace of the current session — the untouched filter default.
974
1080
  const currentWorkspaceId = (0, react_1.useMemo)(() => {
975
1081
  const current = sessions.current;
@@ -994,8 +1100,23 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
994
1100
  .filter((row) => !row.blank && row.parentId === undefined && row.origin !== 'subagent')
995
1101
  .filter((row) => !archived.has(row.id))
996
1102
  .filter((row) => scope === null || scope.has(row.id))
1103
+ // A session that never completed a single turn is not established yet —
1104
+ // creating one from the FAB and backing out leaves these behind, and
1105
+ // they pile up as basename-titled rows (user report, 2026-08-25). The
1106
+ // host's sessionStats projection rides every list row; `turns` counts
1107
+ // turns with at least one CLOSED step, and cancelled steps count too,
1108
+ // so an interrupted first reply still shows. Keep when: it is the
1109
+ // current session (highlight row), it is running (first reply in
1110
+ // flight), it earned a title, or the projection is absent (no signal —
1111
+ // show rather than silently hide).
1112
+ .filter((row) => {
1113
+ if (row.id === sessions.current || row.running || row.title !== undefined)
1114
+ return true;
1115
+ const stats = row.projectionValues?.sessionStats;
1116
+ return stats?.turns === undefined || stats.turns > 0;
1117
+ })
997
1118
  .sort((a, b) => b.updatedAt - a.updatedAt);
998
- }, [sessions.ids, sessions.byId, workspaces.archivedSessionIds, selectedWorkspace]);
1119
+ }, [sessions.ids, sessions.byId, sessions.current, workspaces.archivedSessionIds, selectedWorkspace]);
999
1120
  // The session header's back button (session scope) cannot hold this
1000
1121
  // store directly — a handle mounts under exactly one scope, and this one
1001
1122
  // is already root-scoped here (see nav-store.ts) — so it dispatches
@@ -1046,7 +1167,7 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
1046
1167
  const dot = (0, session_dot_ts_1.dotState)(row);
1047
1168
  const status = statusLine(row, dot, t);
1048
1169
  const initial = row.displayTitle.trim().charAt(0).toUpperCase();
1049
- return ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-row", "data-current": row.id === sessions.current ? '' : undefined, onClick: () => enter(() => openSession(row.id)), children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-avatar", "aria-hidden": "true", children: dot !== undefined ? (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.StateDot, { state: dot, size: 10 }) : initial }), (0, jsx_runtime_1.jsxs)("span", { "data-mobile-nav": "home-row-body", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-title", children: row.displayTitle }), status !== undefined && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-status", children: status }))] }), (0, jsx_runtime_1.jsx)("time", { "data-mobile-nav": "home-row-time", dateTime: new Date(row.updatedAt).toISOString(), children: relativeTime(row.updatedAt) })] }) }, row.id));
1170
+ return ((0, jsx_runtime_1.jsx)(SwipeRow, { open: swipeOpen === row.id, onOpenChange: (open) => setSwipeOpen(open ? row.id : null), onArchive: () => onArchiveRow(row.id), archiveLabel: t('infoArchive'), children: (guardClick) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-row", "data-current": row.id === sessions.current ? '' : undefined, onClick: () => guardClick(() => enter(() => openSession(row.id))), children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-avatar", "aria-hidden": "true", children: dot !== undefined ? (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.StateDot, { state: dot, size: 10 }) : initial }), (0, jsx_runtime_1.jsxs)("span", { "data-mobile-nav": "home-row-body", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-title", children: row.displayTitle }), status !== undefined && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-status", children: status }))] }), (0, jsx_runtime_1.jsx)("time", { "data-mobile-nav": "home-row-time", dateTime: new Date(row.updatedAt).toISOString(), children: relativeTime(row.updatedAt) })] })) }, row.id));
1050
1171
  }), rows.length === 0 && (0, jsx_runtime_1.jsx)("li", { "data-mobile-nav": "home-empty", children: t('noSessions') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-fab", "aria-label": t('newSession'), title: t('newSession'), onClick: () => enter(() => startSession(selectedWorkspace?.workspaceId)), children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconPlusOutline16, { size: 18 }), (0, jsx_runtime_1.jsx)("span", { children: t('newSession') })] }), sheet !== null && ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "home-sheet-layer", children: [(0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "home-sheet-mask", role: "button", tabIndex: -1, "aria-label": t('close'), onClick: () => setSheet(null) }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "home-sheet", role: sheet === 'filter' ? 'menu' : undefined, children: [sheet === 'filter' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "home-sheet-title", children: t('switchWorkspace') }), (0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", "data-mobile-nav": "home-sheet-item", "data-selected": selected === 'all' ? '' : undefined, onClick: () => {
1051
1172
  actions.filter('all');
1052
1173
  setSheet(null);
@@ -3293,6 +3414,59 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
3293
3414
  padding: 6px 12px !important;
3294
3415
  }
3295
3416
  }
3417
+
3418
+ /* ---------- dsh-auto-approve compatibility ----------
3419
+ The composer.css.ts phone rules collapse the permission trigger to an
3420
+ icon-only pill, but the official permissionGlyphs table only draws
3421
+ icons for read-only / workspace-write — the auto-approve plugin's
3422
+ "auto" preset gets none, so its pill showed a bare chevron. The plugin
3423
+ injects its shield+sparkle svg into the MENU items itself (DOM,
3424
+ content-anchored), but the collapsed TRIGGER re-renders on every
3425
+ preset change, so a CSS pseudo-element is the survives-re-render fix
3426
+ (same reasoning as the old model Sparkle ::before above).
3427
+
3428
+ Presence gates, honestly stated (2026-08-25 review):
3429
+ - Trigger rule: aria-label is t('input.accessMode', {name}) with the
3430
+ raw preset display name embedded untranslated and (zh/en verified)
3431
+ at the END of the string — [aria-label$="Auto"] matches exactly the
3432
+ auto-approve preset being selected, and cannot match a future
3433
+ "Autopilot"-style preset. Plugin absent → the name never renders →
3434
+ inert. This is a DIRECT gate on the plugin's own preset name.
3435
+ - Menu-item rule: INDIRECT gate only. It matches "permission-menu item
3436
+ without an official icon span"; the official three presets all ship
3437
+ icon spans in the menu (verified live), so with the plugin absent
3438
+ nothing matches. Known boundary: another plugin's icon-less preset,
3439
+ or a host redesign dropping icon spans, would wrongly receive this
3440
+ shield. A direct gate is not possible: menu items carry no
3441
+ id/data attributes to anchor on, CSS cannot match text, and gating
3442
+ on the plugin's own JS-injected markers would kill this fallback in
3443
+ exactly the JS-did-not-run case it exists for. Paths are the plugin's own
3444
+ shield outline + four-point sparkle (permission-glyph.ts), stroke
3445
+ shield + filled sparkle, drawn as a currentColor mask. */
3446
+ @media (max-width: 767px) {
3447
+ /* Fallback for the MENU items too (real-phone report 2026-08-25: the
3448
+ plugin's DOM injection did not run in that page instance while this
3449
+ CSS did — bundle load timing differs per plugin). Any permission-menu
3450
+ item WITHOUT the official icon span gets the shield drawn by CSS;
3451
+ today that is exactly the plugin "auto" preset (the official three
3452
+ all ship icons in the menu). Self-coordinating with the plugin's own
3453
+ injection: once its svg lands it arrives inside a cloned icon span,
3454
+ the :not() stops matching, no double icon. */
3455
+ [data-slot="conversation.composer.bar"] [class$="_card"] > [class$="_row"] > [class$="_tools"] > [class$="_modes"] [role="menu"] button[role="menuitem"]:not(:has([class*="_itemIcon_"]))::before,
3456
+ [data-slot="conversation.composer.bar"] [class$="_card"] > [class$="_row"] > [class$="_tools"] > [class$="_modes"] button[class$="_trigger"][aria-label$="Auto"]::before {
3457
+ content: '';
3458
+ width: 16px;
3459
+ height: 16px;
3460
+ flex: none;
3461
+ background: currentColor;
3462
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' fill='none' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8.35 5.4C8.35 7.2 7.15 8.4 5.35 8.4C7.15 8.4 8.35 9.6 8.35 11.4C8.35 9.6 9.55 8.4 11.35 8.4C9.55 8.4 8.35 7.2 8.35 5.4Z'/%3E%3C/svg%3E");
3463
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' fill='none' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8.35 5.4C8.35 7.2 7.15 8.4 5.35 8.4C7.15 8.4 8.35 9.6 8.35 11.4C8.35 9.6 9.55 8.4 11.35 8.4C9.55 8.4 8.35 7.2 8.35 5.4Z'/%3E%3C/svg%3E");
3464
+ -webkit-mask-size: contain;
3465
+ mask-size: contain;
3466
+ -webkit-mask-repeat: no-repeat;
3467
+ mask-repeat: no-repeat;
3468
+ }
3469
+ }
3296
3470
  `;
3297
3471
  };
3298
3472
  __modules["styles/misc.css.js"] = function (require, module, exports) {
@@ -3654,6 +3828,70 @@ exports.HOME_CSS = `/* ---------- phone app shell (< 768px) ---------- */
3654
3828
  overscroll-behavior: contain;
3655
3829
  -webkit-overflow-scrolling: touch;
3656
3830
  }
3831
+ /* Swipe-to-archive wrapper (redone 2026-08-25, real-device round): iOS-mail
3832
+ mechanics. The wrapper CLIPS (overflow hidden, same 22px radius as the
3833
+ card) and the archive button is parked past the right edge, sliding in
3834
+ lock-step with the card — a 1px drag reveals a 1px slice, never a
3835
+ fully-drawn button behind a gap. Clipping would eat the card's cast
3836
+ shadow, so the light-theme shadow moves up to this wrapper (identical
3837
+ values; the dark theme's INSET hairline on the card survives clipping
3838
+ untouched). pan-y keeps vertical list scrolling native; the direction
3839
+ lock in MobileHome.tsx decides horizontal-vs-vertical once per touch. */
3840
+ [data-mobile-nav="home-swipe"] {
3841
+ position: relative;
3842
+ overflow: hidden;
3843
+ border-radius: 22px;
3844
+ /* The list is a flex column: without this, an overflowing list SHRINKS
3845
+ every row and the clip above slices the squashed card's bottom edge
3846
+ flat (real-device report, 2026-08-25). Rows keep natural height; the
3847
+ list scrolls. */
3848
+ flex: 0 0 auto;
3849
+ touch-action: pan-y;
3850
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);
3851
+ }
3852
+ body[data-ds-dark-theme] [data-mobile-nav="home-swipe"] {
3853
+ box-shadow: none;
3854
+ }
3855
+ /* Press = settle toward the page (mirrors the card's own :active swap,
3856
+ which the clip made invisible at wrapper level). */
3857
+ [data-mobile-nav="home-swipe"]:has([data-mobile-nav="home-row"]:active) {
3858
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
3859
+ }
3860
+ body[data-ds-dark-theme] [data-mobile-nav="home-swipe"]:has([data-mobile-nav="home-row"]:active) {
3861
+ box-shadow: none;
3862
+ }
3863
+ [data-mobile-nav="home-swipe-card"] {
3864
+ position: relative;
3865
+ z-index: 1;
3866
+ transition: transform .22s var(--ds-ease-out, ease-out);
3867
+ will-change: transform;
3868
+ }
3869
+ /* Inset pill, not a full-bleed slab (real-device feedback: hard edges).
3870
+ 5px breathing room on three sides inside the 88px reveal; its own
3871
+ radius echoes the card family. It still slides in lock-step from past
3872
+ the right edge (transform driven from MobileHome.tsx). */
3873
+ [data-mobile-nav="home-archive"] {
3874
+ position: absolute;
3875
+ z-index: 0;
3876
+ top: 5px;
3877
+ bottom: 5px;
3878
+ right: 5px;
3879
+ width: 78px;
3880
+ border: none;
3881
+ border-radius: 17px;
3882
+ background: var(--dsw-alias-state-error-primary, #d4494c);
3883
+ color: #fff;
3884
+ font-size: 13px;
3885
+ font-weight: 600;
3886
+ cursor: pointer;
3887
+ -webkit-tap-highlight-color: transparent;
3888
+ transition: transform .22s var(--ds-ease-out, ease-out);
3889
+ will-change: transform;
3890
+ }
3891
+ [data-mobile-nav="home-archive"]:active {
3892
+ filter: brightness(.9);
3893
+ }
3894
+
3657
3895
  [data-mobile-nav="home-row"] {
3658
3896
  display: flex;
3659
3897
  align-items: center;
@@ -4502,6 +4740,10 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4502
4740
  justify-content: flex-start !important;
4503
4741
  gap: 5px !important;
4504
4742
  padding: 2px 8px 8px !important;
4743
+ /* One line, always: the model pill is the only shrinkable item (its
4744
+ label ellipsizes), so wrapping would only ever push the send button
4745
+ to a second row (real-device report, 2026-08-25). */
4746
+ flex-wrap: nowrap !important;
4505
4747
  }
4506
4748
  ${ROW} > [class$="_tools"],
4507
4749
  ${ROW} > [class$="_trailing"] {
@@ -4528,8 +4770,14 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4528
4770
  the right edge, so no spacer element is needed. */
4529
4771
  ${MODEL} {
4530
4772
  order: 4 !important;
4531
- flex: 0 1 auto !important;
4773
+ /* A definite flex-basis (not auto) breaks the circular sizing between
4774
+ this seat and the trigger's max-width:100%: the seat's width comes
4775
+ from flex resolution, the trigger fills up to it, the label
4776
+ ellipsizes inside. Under nowrap pressure this is the only
4777
+ min-width:0 seat, so it absorbs all shrink. */
4778
+ flex: 0 1 min(48vw, 200px) !important;
4532
4779
  min-width: 0 !important;
4780
+ max-width: min(48vw, 200px) !important;
4533
4781
  margin-right: auto !important;
4534
4782
  }
4535
4783
  /* Third-party input.right entries park next to the ring rather than
@@ -4546,11 +4794,11 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4546
4794
  order: 7 !important;
4547
4795
  }
4548
4796
 
4549
- /* --- 3. permission + model as icon-only pills (real-device round 2, 2026-08-17) ---
4550
- S3's icon-and-label capsules read as noise on an actual phone — there
4551
- is no room to usefully show a preset name or a model id, so the label
4552
- text is now hidden outright and both triggers collapse to a plain
4553
- ~44x30 icon button. This AGREES with (rather than fights) the official
4797
+ /* --- 3. permission as icon-only pill; model as name+effort pill ---
4798
+ (real-device round 2, 2026-08-17; model pill reworked 2026-08-25:
4799
+ the user wants to SEE which model/effort is active, so only the
4800
+ permission trigger stays icon-only its label is hidden and it
4801
+ collapses to a plain ~44x30 icon button. This AGREES with (rather than fights) the official
4554
4802
  container query (\`@container (width <= 460px) { .trigger:has(.triggerIcon)
4555
4803
  .triggerLabel { display: none } }\`) that S3 had to override — no need
4556
4804
  to override it back. Accessible name is unaffected: both official
@@ -4573,13 +4821,10 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4573
4821
  accessible-name computation (empty generated content). S3's
4574
4822
  rtl-ellipsis trick on the model label is simply inert under
4575
4823
  display:none now; left alone rather than unpicked. */
4576
- ${PERM} [class$="_triggerLabel"],
4577
- ${MODEL} > [class$="_trigger"] > [class$="_triggerLabel"],
4578
- ${MODEL} > [class$="_trigger"] > [class$="_triggerEffort"] {
4824
+ ${PERM} [class$="_triggerLabel"] {
4579
4825
  display: none !important;
4580
4826
  }
4581
- ${PERM} button[class$="_trigger"],
4582
- ${MODEL} > [class$="_trigger"] {
4827
+ ${PERM} button[class$="_trigger"] {
4583
4828
  background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
4584
4829
  width: 44px !important;
4585
4830
  height: 30px !important;
@@ -4591,29 +4836,47 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4591
4836
  border-radius: 999px !important;
4592
4837
  touch-action: manipulation !important;
4593
4838
  }
4839
+ /* Model trigger: name + effort pill (2026-08-25 user request — the
4840
+ icon-only form hid which model was active). The effort text must stay
4841
+ whole; the model NAME is the elastic part and ellipsizes. max-width
4842
+ caps the pill so the row's elastic gap survives extreme model ids;
4843
+ the label rules below override the <=1023px grow rules in
4844
+ layout.css.ts (flex 1 1 auto) that would otherwise pad the pill. */
4845
+ ${MODEL} > [class$="_trigger"] {
4846
+ background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
4847
+ height: 30px !important;
4848
+ width: auto !important;
4849
+ max-width: 100% !important;
4850
+ min-width: 0 !important;
4851
+ padding: 0 10px !important;
4852
+ gap: 4px !important;
4853
+ justify-content: flex-start !important;
4854
+ border-radius: 999px !important;
4855
+ touch-action: manipulation !important;
4856
+ font-size: 12px !important;
4857
+ }
4858
+ ${MODEL} > [class$="_trigger"] > [class$="_triggerLabel"] {
4859
+ display: block !important;
4860
+ flex: 0 1 auto !important;
4861
+ min-width: 0 !important;
4862
+ overflow: hidden !important;
4863
+ text-overflow: ellipsis !important;
4864
+ white-space: nowrap !important;
4865
+ }
4866
+ ${MODEL} > [class$="_trigger"] > [class$="_triggerEffort"] {
4867
+ display: block !important;
4868
+ flex: none !important;
4869
+ white-space: nowrap !important;
4870
+ }
4594
4871
  /* PermissionSelect wraps its trigger in the Menu primitive's root span,
4595
4872
  which must shrink to the icon button's fixed width. */
4596
4873
  ${PERM} > span:has(> button[class$="_trigger"]) {
4597
4874
  flex: 0 0 auto !important;
4598
4875
  min-width: 0 !important;
4599
4876
  }
4600
- /* ic_ds_sparkle_16 (@deepseek-ai/dsh-client-ui-primitives IconSparkle16
4601
- path, copied verbatim) as a mask so it inherits currentColor like every
4602
- other icon in the row — the model trigger has no official icon slot to
4603
- hook into. */
4604
- ${MODEL} > [class$="_trigger"]::before {
4605
- content: '';
4606
- width: 16px;
4607
- height: 16px;
4608
- flex: none;
4609
- background: currentColor;
4610
- -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E");
4611
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E");
4612
- -webkit-mask-size: contain;
4613
- mask-size: contain;
4614
- -webkit-mask-repeat: no-repeat;
4615
- mask-repeat: no-repeat;
4616
- }
4877
+ /* (2026-08-25) The model trigger's CSS-only Sparkle ::before icon is gone:
4878
+ the pill now shows the model name + effort text itself, so an icon would
4879
+ just eat label width. */
4617
4880
 
4618
4881
  /* --- 4. both menus become bottom sheets ---
4619
4882
  The permission menu is the Menu primitive (role=menu, absolute, side=top)
@@ -4731,6 +4994,16 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4731
4994
  overflow-x: auto !important;
4732
4995
  overflow-y: hidden !important;
4733
4996
  scrollbar-width: none !important;
4997
+ /* The dock is itself a flex ITEM of the composer stack, and the
4998
+ new-session hero pins that stack to a fixed height. overflow-x:auto
4999
+ above makes this a scroll container, which drops a flex item's
5000
+ automatic minimum size to 0 — so the hero squeezed the dock to ~5px
5001
+ and the attachment chips slid under the card drawn after it (only the
5002
+ top sliver of a thumbnail showed; reported from a real phone,
5003
+ 2026-08-26). Refuse the squeeze: natural height, the stack centers a
5004
+ few px lower instead. In-chat the column is not height-pinned and this
5005
+ is a no-op. */
5006
+ flex-shrink: 0 !important;
4734
5007
  }
4735
5008
  [data-slot="conversation.input.dock"]::-webkit-scrollbar {
4736
5009
  display: none !important;
@@ -7222,6 +7495,78 @@ function installAionuiCompat(ctx) {
7222
7495
  }, 'dsh-mobile-nav: sheet rise animation replay');
7223
7496
  }
7224
7497
  };
7498
+ __modules["effects/workbench-ref-close.js"] = function (require, module, exports) {
7499
+ "use strict";
7500
+ Object.defineProperty(exports, "__esModule", { value: true });
7501
+ exports.installWorkbenchRefClose = installWorkbenchRefClose;
7502
+ /** Phone breakpoint — same query every phone-only effect in this plugin uses. */
7503
+ const PHONE_QUERY = '(max-width: 767px)';
7504
+ /**
7505
+ * The file tree's per-row @-reference button (dsh-better-sidebar's
7506
+ * `_explorerRef` class suffix — same anchoring convention as every other
7507
+ * better-sidebar hook in this plugin: the plugin's own root marker plus a
7508
+ * stable class suffix, so the selector misses entirely when the plugin is
7509
+ * not installed). Covers the search results too: TreePanel renders both
7510
+ * through the same row-actions slot.
7511
+ */
7512
+ const REF_SELECTOR = '[data-dsh-better-sidebar] [class$="_explorerRef"]';
7513
+ /** The panel's own (phone-hidden) toggle — the one official way to close it.
7514
+ * Same anchor gestures.ts and MobileSessionHeader.tsx already use. */
7515
+ const TOGGLE_SELECTOR = '[data-dsh-better-sidebar] button[class$="_toggleButton"]';
7516
+ /** Open-state read, shared convention with gestures.ts: the class ends in
7517
+ * "_panel" only while open ("_panelHidden" is appended once closed). */
7518
+ const PANEL_OPEN_SELECTOR = '[data-dsh-better-sidebar] [class$="_panel"]';
7519
+ /**
7520
+ * Phone: an @-file reference tap in the workbench closes the workbench.
7521
+ *
7522
+ * On a phone the panel is a full-screen surface over the conversation, so
7523
+ * after tapping a row's @ button the user is still looking at the file tree
7524
+ * — the ONLY feedback for the tap is a draft change on a composer they
7525
+ * cannot see (real-device report, 2026-08-26: reads as "nothing happened",
7526
+ * and the close pill can end up behind the software keyboard on top of it).
7527
+ * Closing the panel IS the feedback: the conversation comes back with the
7528
+ * fresh `@path` sitting in the composer.
7529
+ *
7530
+ * Capture phase on document, deliberately: the button's own React handler
7531
+ * calls stopPropagation() (its row would otherwise open the file), so a
7532
+ * bubble listener never hears the tap. Capture runs on the way DOWN, before
7533
+ * the target handler and its stopPropagation can matter — the aionui-compat
7534
+ * chevron listener set this precedent.
7535
+ *
7536
+ * The close is deferred one macrotask so the reference lands first: the
7537
+ * button's handler (which appends `@path` to the draft) runs at target
7538
+ * phase, synchronously inside this same event dispatch — by the time the
7539
+ * timeout fires the draft is written and the toggle click only changes what
7540
+ * is on screen. Desktop is untouched (checked per tap, not at install: a
7541
+ * resize mid-session must not leave a stale arm either way), where the
7542
+ * panel is a docked column and closing it after every reference would be
7543
+ * hostile to multi-file referencing.
7544
+ */
7545
+ function installWorkbenchRefClose(ctx) {
7546
+ ctx.effect(() => {
7547
+ const phone = window.matchMedia(PHONE_QUERY);
7548
+ const onClick = (event) => {
7549
+ if (!phone.matches)
7550
+ return;
7551
+ const target = event.target;
7552
+ if (!(target instanceof Element))
7553
+ return;
7554
+ if (target.closest(REF_SELECTOR) === null)
7555
+ return;
7556
+ setTimeout(() => {
7557
+ // Re-check open state at fire time: the tap's own handler chain (or
7558
+ // a second tap) may already have closed the panel — the toggle is a
7559
+ // toggle, and blind-clicking it would REOPEN what just closed.
7560
+ if (document.querySelector(PANEL_OPEN_SELECTOR) === null)
7561
+ return;
7562
+ document.querySelector(TOGGLE_SELECTOR)?.click();
7563
+ }, 0);
7564
+ };
7565
+ document.addEventListener('click', onClick, true);
7566
+ return () => document.removeEventListener('click', onClick, true);
7567
+ }, 'dsh-mobile-nav: workbench @-reference closes the panel');
7568
+ }
7569
+ };
7225
7570
  __modules["effects/header-status.js"] = function (require, module, exports) {
7226
7571
  "use strict";
7227
7572
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -8292,6 +8637,7 @@ const index_ts_1 = require("./styles/index.js");
8292
8637
  const debug_ts_1 = require("./debug.js");
8293
8638
  const phone_chrome_ts_1 = require("./effects/phone-chrome.js");
8294
8639
  const aionui_compat_ts_1 = require("./effects/aionui-compat.js");
8640
+ const workbench_ref_close_ts_1 = require("./effects/workbench-ref-close.js");
8295
8641
  const header_status_ts_1 = require("./effects/header-status.js");
8296
8642
  const gestures_ts_1 = require("./effects/gestures.js");
8297
8643
  const turn_fold_ts_1 = require("./effects/turn-fold.js");
@@ -8333,6 +8679,9 @@ function apply(ctx) {
8333
8679
  // stack more blank page on top of it.
8334
8680
  (0, phone_chrome_ts_1.installSunkInset)(ctx);
8335
8681
  (0, aionui_compat_ts_1.installAionuiCompat)(ctx);
8682
+ // Phone: tapping a file's @-reference in the workbench closes the panel —
8683
+ // the conversation returning with the fresh mention IS the tap feedback.
8684
+ (0, workbench_ref_close_ts_1.installWorkbenchRefClose)(ctx);
8336
8685
  // Session header running-status dot (S2): no official element exists to
8337
8686
  // reposition, so this reads ctx.sessions directly and self-draws via CSS.
8338
8687
  (0, header_status_ts_1.installHeaderStatusDot)(ctx);
@@ -8462,6 +8811,8 @@ function apply(ctx) {
8462
8811
  startSession: (workspaceId) => ctx.workspaces.startSession(workspaceId),
8463
8812
  // S5 session-log chip — the same service call MobileDrawerFooter uses.
8464
8813
  downloadSessionLog: (id) => ctx.sessionLogDownload.download(id),
8814
+ // Row swipe action — the same binding MobileSessionInfo archives with.
8815
+ archiveSession: (id) => ctx.workspaces.archiveSession(id),
8465
8816
  }),
8466
8817
  }, MobileHome_tsx_1.MobileHome));
8467
8818
  // Session log download, relocated from the session header to the drawer
@@ -10,6 +10,8 @@ export type MobileHomeProps = PropsRuntime<'shell.overlay'> & PropsStore<ReturnT
10
10
  startSession: (workspaceId?: WorkspaceId) => void;
11
11
  /** Bound ctx.sessionLogDownload.download() — the session-log chip (S5). */
12
12
  downloadSessionLog: (id: SessionId) => void;
13
+ /** Bound ctx.workspaces.archiveSession(id) — the row swipe action. */
14
+ archiveSession: (id: SessionId) => Promise<void>;
13
15
  };
14
16
  /**
15
17
  * Phone home screen: the full-screen session list that owns the first level
@@ -20,5 +22,5 @@ export type MobileHomeProps = PropsRuntime<'shell.overlay'> & PropsStore<ReturnT
20
22
  * all navigation from the injected official actions; nothing here reads the
21
23
  * official DOM.
22
24
  */
23
- export declare function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession, startSession, downloadSessionLog, t, }: MobileHomeProps): import("react").JSX.Element | null;
25
+ export declare function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession, startSession, downloadSessionLog, archiveSession, t, }: MobileHomeProps): import("react").JSX.Element | null;
24
26
  //# sourceMappingURL=MobileHome.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MobileHome.d.ts","sourceRoot":"","sources":["../../../src/client/MobileHome.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAS7F,OAAO,KAAK,EAAE,SAAS,EAAkB,WAAW,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAMjC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAMpD,kEAAkE;AAClE,MAAM,MAAM,eAAe,GACvB,YAAY,CAAC,eAAe,CAAC,GAC7B,UAAU,CAAC,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,GAC7C,WAAW,CAAC,OAAO,EAAE,CAAC,GACtB;IACA,mCAAmC;IACnC,WAAW,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACpC,uDAAuD;IACvD,YAAY,EAAE,CAAC,WAAW,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IACjD,2EAA2E;IAC3E,kBAAkB,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;CAC5C,CAAA;AAuEH;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EACzB,WAAW,EACX,aAAa,EACb,QAAQ,EACR,OAAO,EACP,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,CAAC,GACF,EAAE,eAAe,sCA+OjB"}
1
+ {"version":3,"file":"MobileHome.d.ts","sourceRoot":"","sources":["../../../src/client/MobileHome.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAS7F,OAAO,KAAK,EAAE,SAAS,EAAkB,WAAW,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAMjC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAMpD,kEAAkE;AAClE,MAAM,MAAM,eAAe,GACvB,YAAY,CAAC,eAAe,CAAC,GAC7B,UAAU,CAAC,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,GAC7C,WAAW,CAAC,OAAO,EAAE,CAAC,GACtB;IACA,mCAAmC;IACnC,WAAW,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACpC,uDAAuD;IACvD,YAAY,EAAE,CAAC,WAAW,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IACjD,2EAA2E;IAC3E,kBAAkB,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAC3C,sEAAsE;IACtE,cAAc,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACjD,CAAA;AAkMH;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EACzB,WAAW,EACX,aAAa,EACb,QAAQ,EACR,OAAO,EACP,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,CAAC,GACF,EAAE,eAAe,sCAgRjB"}