dsh-zen-remote 1.0.2 → 1.1.0

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.
Files changed (51) hide show
  1. package/AGENTS.md +9 -1
  2. package/README.md +56 -16
  3. package/docs/interface.md +69 -6
  4. package/docs/remote-access.en.md +7 -5
  5. package/docs/remote-access.md +8 -6
  6. package/dsh-push.mjs +272 -64
  7. package/lib/client.js +550 -28
  8. package/lib/lan-gate-server.cjs +7 -1
  9. package/lib/types/client/MobileHome.d.ts.map +1 -1
  10. package/lib/types/client/MobileSessionHeader.d.ts +1 -1
  11. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  12. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  13. package/lib/types/client/effects/modal-back.d.ts +22 -0
  14. package/lib/types/client/effects/modal-back.d.ts.map +1 -0
  15. package/lib/types/client/history-nav.d.ts +49 -0
  16. package/lib/types/client/history-nav.d.ts.map +1 -0
  17. package/lib/types/client/index.d.ts.map +1 -1
  18. package/lib/types/client/locales.d.ts +4 -2
  19. package/lib/types/client/locales.d.ts.map +1 -1
  20. package/lib/types/client/styles/base.css.d.ts +1 -1
  21. package/lib/types/client/styles/base.css.d.ts.map +1 -1
  22. package/lib/types/client/styles/chips.css.d.ts +1 -1
  23. package/lib/types/client/styles/chips.css.d.ts.map +1 -1
  24. package/lib/types/client/styles/compat.css.d.ts +1 -1
  25. package/lib/types/client/styles/compat.css.d.ts.map +1 -1
  26. package/lib/types/client/styles/composer.css.d.ts +1 -1
  27. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  28. package/lib/types/client/styles/content.css.d.ts +1 -1
  29. package/lib/types/client/styles/content.css.d.ts.map +1 -1
  30. package/lib/types/client/styles/header.css.d.ts +1 -1
  31. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  32. package/lib/types/client/styles/home.css.d.ts +1 -1
  33. package/lib/types/client/styles/home.css.d.ts.map +1 -1
  34. package/llms.txt +1 -1
  35. package/package.json +23 -21
  36. package/pwa/inject.js +1 -0
  37. package/scripts/sync-doc-version.mjs +83 -0
  38. package/src/client/MobileHome.tsx +17 -1
  39. package/src/client/MobileSessionHeader.tsx +134 -1
  40. package/src/client/MobileSessionInfo.tsx +38 -8
  41. package/src/client/effects/modal-back.ts +98 -0
  42. package/src/client/history-nav.ts +121 -0
  43. package/src/client/index.tsx +2 -0
  44. package/src/client/locales.ts +8 -4
  45. package/src/client/styles/base.css.ts +6 -0
  46. package/src/client/styles/chips.css.ts +4 -1
  47. package/src/client/styles/compat.css.ts +8 -1
  48. package/src/client/styles/composer.css.ts +25 -0
  49. package/src/client/styles/content.css.ts +24 -0
  50. package/src/client/styles/header.css.ts +123 -0
  51. package/src/client/styles/home.css.ts +19 -10
@@ -1,2 +1,2 @@
1
- export declare const CONTENT_CSS = "/* ---------- chat markdown content (< 768px) ---------- */\n\n@media (max-width: 767px) {\n /* Inline code: upstream renders it display:inline-flex \u2014 an atomic inline\n box that can never wrap across lines, so one long code span walks off\n the right edge of the screen. Plain inline keeps the background pill\n per line fragment and lets long tokens break anywhere. Deliberately\n unscoped (any :not(pre) > code, not just the _markdown_ module):\n inline code also renders outside that container (user bubbles,\n reasoning rows), and on a phone there is no width at which an\n unwrappable inline box is right. !important beats the upstream\n display:inline-flex on specificity ties. */\n :not(pre) > code {\n display: inline !important;\n overflow-wrap: anywhere;\n /* iOS WebKit: overflow-wrap alone still failed to break long tokens in\n inline code on a real iPhone (screenshot-verified 2026-08-18, li >\n code path case) while desktop Chromium broke them fine. break-all is\n the everywhere-supported hammer; acceptable for code pills. */\n word-break: break-all;\n }\n /* Belt-and-suspenders for engines that compute soft-wrap opportunities\n from the block container rather than the inline itself (historical\n WebKit behavior): inherit anywhere through the whole transcript so the\n block side agrees. Prose is untouched until something would overflow;\n the table is already pinned wide by min-width: max-content below. */\n [data-chat-flow],\n [class*=\"_markdown_\"] {\n overflow-wrap: anywhere;\n }\n\n /* Markdown tables: the official _tableScroll_ wrapper is already a\n horizontal scroll viewport around a width:max-content table, but its\n cells are capped at min(30vw, 320px) \u2014 barely 120px on a phone, which\n crushes every column into a vertical word-stack. Let columns size to\n their content instead and read the overflow by scrolling the wrapper.\n 480px (not a vw cap \u2014 85vw was ~320px on a 375px phone and still\n wrapped mid-length cells) keeps typical cells on one line; only truly\n prose-heavy cells wrap, instead of dragging the table kilometers\n wide. */\n [class*=\"_tableScroll_\"] th,\n [class*=\"_tableScroll_\"] td {\n max-width: 480px !important;\n }\n\n /* iOS WebKit fallback: upstream's own table { width: max-content } is what\n keeps the table at its natural width inside the scroll wrapper, but\n WebKit's max-content support on table boxes is incomplete \u2014 on a real\n iPhone the table fell back to shrink-to-fit and every column got\n crushed to its min-width (the \"ra/nk\" vertical-word-stack screenshot,\n 2026-08-18). Redeclare the natural-width intent with both width and\n min-width so whichever keyword the engine honors wins; the wrapper\n already scrolls the overflow. */\n [class*=\"_tableScroll_\"] table {\n width: max-content !important;\n min-width: max-content !important;\n max-width: none !important;\n }\n\n /* Turn-tail metadata (2026-08-18): the timing stats (\"18:06 \u00B7 \u7528\u65F6 29\u79D2 \u00B7\n \u9996 token 0.9\u79D2 \u00B7 163 tok/s\") are desktop-hover-revealed (official\n [data-time-hover-root] gate holds them at opacity 0 until :hover /\n :focus-within) \u2014 a phone never hovers, so they were simply invisible;\n and even ignoring that, the row is nowrap + overflow:hidden (plus this\n plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the\n text. On phones: always visible, own full-width line under the action\n icons, wrapping freely, one type size down. !important + the extra\n [class$=\"_actions\"] hop out-specifies both upstream and layout.css.ts. */\n /* Pin the tail row's width chain before allowing wrap: with wrap enabled\n the row's intrinsic (shrink-to-fit) width collapses to its widest\n single icon, taking the whole tail down to ~16px. */\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_root\"],\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_actions\"] {\n width: 100%;\n }\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_actions\"] {\n flex-wrap: wrap;\n row-gap: 2px;\n }\n /* Wide companion to the rule below: mirrors the upstream hover-gate\n selector shape directly, with no assumptions about the tail's DOM\n structure, so the stats can't stay invisible even if the phone's tail\n renders differently than desktop. */\n [data-time-hover-root] [class$=\"_timeEnd\"],\n [data-time-hover-root] [class$=\"_timeStart\"] {\n opacity: 1 !important;\n transition: none !important;\n }\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_actions\"] [class$=\"_timeEnd\"] {\n opacity: 1 !important;\n flex-basis: 100% !important;\n white-space: normal !important;\n overflow: visible !important;\n text-overflow: clip !important;\n padding-left: 0 !important;\n font-size: 12px;\n line-height: 18px;\n }\n}\n";
1
+ export declare const CONTENT_CSS = "/* ---------- chat markdown content (< 768px) ---------- */\n\n@media (max-width: 767px) {\n /* Inline code: upstream renders it display:inline-flex \u2014 an atomic inline\n box that can never wrap across lines, so one long code span walks off\n the right edge of the screen. Plain inline keeps the background pill\n per line fragment and lets long tokens break anywhere. Deliberately\n unscoped (any :not(pre) > code, not just the _markdown_ module):\n inline code also renders outside that container (user bubbles,\n reasoning rows), and on a phone there is no width at which an\n unwrappable inline box is right. !important beats the upstream\n display:inline-flex on specificity ties. */\n :not(pre) > code {\n display: inline !important;\n overflow-wrap: anywhere;\n /* iOS WebKit: overflow-wrap alone still failed to break long tokens in\n inline code on a real iPhone (screenshot-verified 2026-08-18, li >\n code path case) while desktop Chromium broke them fine. break-all is\n the everywhere-supported hammer; acceptable for code pills. */\n word-break: break-all;\n }\n /* Belt-and-suspenders for engines that compute soft-wrap opportunities\n from the block container rather than the inline itself (historical\n WebKit behavior): inherit anywhere through the whole transcript so the\n block side agrees. Prose is untouched until something would overflow;\n the table is already pinned wide by min-width: max-content below. */\n [data-chat-flow],\n [class*=\"_markdown_\"] {\n overflow-wrap: anywhere;\n }\n\n /* Transcript density (2026-08-20): the desktop prose rhythm is 28px of\n leading on 15px text \u2014 a 1.87 ratio that reads generously on a wide\n column but wastes a phone screen, where the same paragraph is three\n times as many lines. Unitless 1.65 so nested blocks (code, quotes,\n lists) recompute from their OWN font-size instead of inheriting a fixed\n 28px that is far too loose for 12-13px chrome text. 1.65 is the floor I\n am willing to go to for mixed CJK/Latin: the glyphs are full-height with\n no descender relief, so tighter starts to look stacked. Phone only \u2014\n the desktop column keeps the official rhythm. */\n [class*=\"_markdown_\"] {\n line-height: 1.65;\n }\n /* Paragraph rhythm. Each paragraph is its own markdown block and the\n separation comes from the flex container's row-gap, not from margins \u2014\n at the stock 16px the paragraph baseline step was 40.75px against a\n 24.75px line step, i.e. 1.65x, which is what reads as airy. 12px brings\n the step to ~36.75px: still clearly more than one line so paragraphs\n never run together, but noticeably denser. Scoped by :has() to\n containers that actually hold prose blocks, so tool cards and other\n flex bodies with the same hashed suffix keep their own spacing. */\n [data-phase] [class$=\"_body\"]:has(> [class*=\"_markdown_\"]) {\n row-gap: 12px !important;\n }\n\n /* Markdown tables: the official _tableScroll_ wrapper is already a\n horizontal scroll viewport around a width:max-content table, but its\n cells are capped at min(30vw, 320px) \u2014 barely 120px on a phone, which\n crushes every column into a vertical word-stack. Let columns size to\n their content instead and read the overflow by scrolling the wrapper.\n 480px (not a vw cap \u2014 85vw was ~320px on a 375px phone and still\n wrapped mid-length cells) keeps typical cells on one line; only truly\n prose-heavy cells wrap, instead of dragging the table kilometers\n wide. */\n [class*=\"_tableScroll_\"] th,\n [class*=\"_tableScroll_\"] td {\n max-width: 480px !important;\n }\n\n /* iOS WebKit fallback: upstream's own table { width: max-content } is what\n keeps the table at its natural width inside the scroll wrapper, but\n WebKit's max-content support on table boxes is incomplete \u2014 on a real\n iPhone the table fell back to shrink-to-fit and every column got\n crushed to its min-width (the \"ra/nk\" vertical-word-stack screenshot,\n 2026-08-18). Redeclare the natural-width intent with both width and\n min-width so whichever keyword the engine honors wins; the wrapper\n already scrolls the overflow. */\n [class*=\"_tableScroll_\"] table {\n width: max-content !important;\n min-width: max-content !important;\n max-width: none !important;\n }\n\n /* Turn-tail metadata (2026-08-18): the timing stats (\"18:06 \u00B7 \u7528\u65F6 29\u79D2 \u00B7\n \u9996 token 0.9\u79D2 \u00B7 163 tok/s\") are desktop-hover-revealed (official\n [data-time-hover-root] gate holds them at opacity 0 until :hover /\n :focus-within) \u2014 a phone never hovers, so they were simply invisible;\n and even ignoring that, the row is nowrap + overflow:hidden (plus this\n plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the\n text. On phones: always visible, own full-width line under the action\n icons, wrapping freely, one type size down. !important + the extra\n [class$=\"_actions\"] hop out-specifies both upstream and layout.css.ts. */\n /* Pin the tail row's width chain before allowing wrap: with wrap enabled\n the row's intrinsic (shrink-to-fit) width collapses to its widest\n single icon, taking the whole tail down to ~16px. */\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_root\"],\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_actions\"] {\n width: 100%;\n }\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_actions\"] {\n flex-wrap: wrap;\n row-gap: 2px;\n }\n /* Wide companion to the rule below: mirrors the upstream hover-gate\n selector shape directly, with no assumptions about the tail's DOM\n structure, so the stats can't stay invisible even if the phone's tail\n renders differently than desktop. */\n [data-time-hover-root] [class$=\"_timeEnd\"],\n [data-time-hover-root] [class$=\"_timeStart\"] {\n opacity: 1 !important;\n transition: none !important;\n }\n [data-chat-flow-kind=\"turn-tail\"] [class$=\"_actions\"] [class$=\"_timeEnd\"] {\n opacity: 1 !important;\n flex-basis: 100% !important;\n white-space: normal !important;\n overflow: visible !important;\n text-overflow: clip !important;\n padding-left: 0 !important;\n font-size: 12px;\n line-height: 18px;\n }\n}\n";
2
2
  //# sourceMappingURL=content.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"content.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/content.css.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW,i6JAmGvB,CAAA"}
1
+ {"version":3,"file":"content.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/content.css.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW,0zMA2HvB,CAAA"}
@@ -1,2 +1,2 @@
1
- export declare const HEADER_CSS = "/* ---------- session header five-piece reflow (< 768px) ---------- */\n\n/* New elements render unconditionally (React does not know about media\n queries); default them to hidden so 768px+ never sees them, then\n re-enable inside the phone block below. Belt-and-braces alongside the\n scoped rules \u2014 mirrors the existing [data-mobile-nav=\"*\"] desktop no-op\n list in misc.css.ts. */\n[data-mobile-nav=\"header-back\"],\n[data-mobile-nav=\"header-viewrow\"],\n[data-mobile-nav=\"header-info\"],\n[data-mobile-nav=\"header-workbench\"],\n/* The workbench close pill's default-hidden used to live in compat.css.ts \u2014\n but EVERYTHING in layout/compat/misc sits inside one shared\n (max-width: 1023px) block (opened at the top of layout, closed at the end\n of misc), so that \"top-level\" rule silently never applied at >=1024 and\n the pill rendered as an unstyled button on desktop (2026-08-17 report).\n This file is appended after the shared block closes, so here it is truly\n global; the <=767px :has() show rule in compat overrides it with\n !important. */\n[data-mobile-nav=\"better-sidebar-close\"] {\n display: none;\n}\n\n@media (max-width: 767px) {\n /* The Files action targeted the dsh-web-ui-all (aionui) explorer sheet \u2014\n a different, unrelated suite from the workbench entry this slice adds\n (dsh-better-sidebar). Hidden here only; 768-1023px keeps it exactly as\n v1.0.0 shipped it. */\n [data-mobile-nav=\"files\"] {\n display: none !important;\n }\n\n /* --- three-column header: [back 92px] [title, truly centered] [utilities 92px] --- */\n /* The bottom padding is the view-switch row's seat: that row is\n position:absolute (see the bottom of this file \u2014 it renders two levels\n deep inside headerActions and can never be a grid item of titleRow), so\n without a reserve it would hang over the message list. 28px = its\n height. Unconditional rather than header:has([data-mobile-nav=\n \"header-viewrow\"]) on purpose: :has() is silently dropped by pre-105\n WebViews (see AGENTS.md) and a 28px overlap on the first message is a\n worse failure than 28px of empty band in the (never observed in\n practice) single-view session. Total header chrome: 48 + 28 = 76px.\n Left/right 8px (S2.1 report leftover, fixed in S4): the official header\n had zero horizontal padding, so the utilities button cluster's right\n edge sat flush against the screen edge (x=390 at 390px width, no\n margin). The 92px grid columns absorb this fine \u2014 they're fixed track\n widths, only the center 1fr column shrinks by 16px. */\n [data-phase] header {\n position: relative;\n padding: 0 8px 28px !important;\n }\n /* No header bottom line \u2014 fade instead (real-device round 2, 2026-08-17).\n The official header draws its border as a `::after` 1px bar (the\n `border-bottom` on the header itself is transparent, just a layout\n reserve \u2014 dsh-client-ui-conversation lib/client.js \".wSkVaW_header{\n border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;\n position:absolute;bottom:1px...}\", verified live 2026-08-17), so\n hiding the pseudo-element is enough; the header's own border-bottom\n never had a visible color to begin with. The message scroller now\n fades in from under the header instead (styles/composer.css.ts, \"no\n divider above OR below the message list\" \u2014 the two edges share one\n mask-image on [class$=\"_scrollBody\"]). */\n [data-phase] header::after {\n display: none !important;\n }\n /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for\n the \"\u6807\u9898\u88AB\u6324\u4E0B\u53BB\" report). The three items are placed with explicit\n grid-column but arrive in DOM order crumbs(2) \u2192 headerActions(1) \u2192\n headerUtilities(3): sparse auto-placement never moves its cursor\n backwards, so headerActions and headerUtilities were pushed onto an\n implicit SECOND row. Measured at 390px: row1 = crumbs 28px, row2 =\n back button 44px, titleRow = 72px with the title stuck at the top\n instead of centered. Pinning the row makes it one 48px band again. */\n [data-phase] header [class$=\"_titleRow\"] {\n position: relative;\n display: grid;\n grid-template-columns: 92px 1fr 92px;\n grid-template-rows: minmax(48px, auto);\n align-items: center;\n min-height: 48px;\n padding: 0 !important;\n }\n /* Flatten the official cluster so its two children (crumbs, headerActions)\n become direct grid items of titleRow instead of a nested flex box. */\n [data-phase] header [class$=\"_titleCluster\"] {\n display: contents;\n }\n /* margin-left: 0 cancels layout.css.ts's shared <=1023px\n \"margin-left: auto !important\" on this exact selector (it pushed the\n Files button to the row's right edge in the old non-grid header;\n inside our grid column it instead shoves the back button 48px right of\n the column's start \u2014 the auto margin absorbs the leftover space\n between the button's content width and the 92px column). */\n [data-phase] header [class$=\"_headerActions\"] {\n grid-column: 1;\n grid-row: 1;\n justify-self: start;\n margin-left: 0 !important;\n display: flex;\n align-items: center;\n min-width: 0;\n }\n /* layout.css.ts's shared <=1023px block hides this element outright\n (header > :first-child > :last-child { display: none !important },\n specificity (0,3,1) \u2014 v1.0.0 relocated the desktop Session-log capsule\n to the drawer footer on every narrow width). That selector is repeated\n here at equal specificity so the later source order wins the display\n property; the class-suffix selector alone (0,2,1) would silently lose. */\n [data-phase] header [class$=\"_headerUtilities\"],\n [data-phase] header > :first-child > :last-child {\n grid-column: 3;\n grid-row: 1;\n justify-self: end;\n display: flex !important;\n align-items: center;\n gap: 4px;\n min-width: 0;\n }\n /* justify-self: stretch (the grid default) so this fills the whole 1fr\n column as a definite-width box \u2014 center/center-content happens inside\n it. justify-self: center (tried first) sizes the item to its content\n with no automatic column cap, and the obvious fix \u2014 max-width: 100% \u2014\n resolves against the WRONG box here (measured 93.6px at 390px, where\n the column is ~206px): the item's DOM parent (titleCluster) is\n display:contents, and percentage max-width does not reliably resolve\n through a boxless ancestor to the grid track. Stretching sidesteps the\n percentage entirely \u2014 but layout.css.ts's shared <=1023px block ALSO\n caps this exact selector at max-width: 24vw !important (93.6px at\n 390px \u2014 the value this rule used to silently inherit once its own\n max-width was dropped in favor of stretch), so that must be reset to\n none here too. */\n [data-phase] header [class$=\"_crumbs\"] {\n grid-column: 2;\n grid-row: 1;\n justify-self: stretch;\n max-width: none !important;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n overflow: hidden;\n }\n /* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header\n lets you tap the parent session's title to switch back \u2014 that affordance\n was lost on phone when the whole parent chain was hidden as \"\u591A\u4F59\u9879\".\n Show only the immediate parent (for agent-teams member sessions that IS\n the captain/main session; deeper ancestors stay hidden \u2014 a 3-level chain\n cannot fit the ~206px center column at 390px). The parent crumb is the\n official breadcrumb <button> (dsh-client-ui-conversation renders\n open(parentId) as its onClick), so tapping needs no JS from this plugin,\n and main sessions (chain length 1) have no :nth-last-child(2) seg at\n all \u2014 the centered single title is pixel-identical to before. */\n [data-phase] header [class$=\"_crumbSeg\"]:not(:nth-last-child(-n+2)) {\n display: none !important;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) {\n display: inline-flex;\n align-items: center;\n flex: none;\n min-width: 0;\n max-width: 38%;\n }\n /* Ancestors BEFORE the parent seg are hidden, which would leave the\n parent seg's own leading \"/\" dangling at the start of the title. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumbSep\"] {\n display: none !important;\n }\n /* Parent crumb: small tertiary text so the 19px current title stays the\n visual anchor; min-height keeps a finger-sized target inside the 48px\n row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumb\"] {\n font-size: 13px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));\n min-height: 44px;\n display: inline-flex;\n align-items: center;\n padding: 0 2px 0 6px;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:last-child {\n display: flex;\n align-items: center;\n min-width: 0;\n max-width: 100%;\n }\n /* The last seg's own \"/\" is the divider between parent and current title \u2014\n it only exists when there IS an ancestor (index > 0), so it can simply\n stay visible now that the parent seg shows. (It used to be hidden here:\n with the parent hidden it dangled before the title.) */\n /* BOTH selectors are required: [class$=] matches the whole class\n ATTRIBUTE's suffix, and the current-session title button's attribute is\n \"wSkVaW_crumb wSkVaW_crumbCurrent\" \u2014 it ends in _crumbCurrent, so the _crumb\n suffix selector alone silently missed the one element that matters\n (measured live: the title stayed 14px while the rule sat in the bundle,\n 2026-08-17). */\n [data-phase] header [class$=\"_crumb\"],\n [data-phase] header [class$=\"_crumbCurrent\"] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n max-width: 100%;\n /* Real-device feedback (2026-08-17): the session title read too small\n next to the enlarged header icons \u2014 19px sits just under the home\n screen's 22px workspace title, keeping the two-level hierarchy. */\n font-size: 19px;\n font-weight: 600;\n }\n\n /* Running-status dot: no official element to reposition (the header\n renders only the crumb title, the two action slots, and the tablist \u2014\n no status indicator), so this is the plan's documented fallback \u2014\n read the data ourselves and draw it (effects/header-status.ts stamps\n data-mobile-nav-dot on the frame). A ::after on the title crumb keeps\n it truly inline with the (centered, truncated) text instead of a\n separately positioned box that would fight the centering math. */\n [data-phase] header [class$=\"_crumbCurrent\"]::after {\n content: '';\n display: none;\n width: 6px;\n height: 6px;\n margin-left: 6px;\n border-radius: 50%;\n vertical-align: middle;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot] header [class$=\"_crumbCurrent\"]::after {\n display: inline-block;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"ongoing\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"warning\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"done\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n\n /* Everything else the official header puts in these two list slots \u2014 the\n agent-preset mode badge, the jobs/subagent trigger, the desktop\n Session-log download capsule, and this plugin's own legacy directory\n toggle \u2014 is the \"\u4E0D\u5E38\u9A7B\" set the design defers to the S4 info card.\n Scoped by the slots' own [data-slot] wrapper (a stable, non-hashed\n contract marker) rather than enumerating each registrant, so a future\n third-party header action is hidden by default too. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > *,\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > * {\n display: none !important;\n }\n /* The agent-preset mode badge specifically survives the blanket hide\n above: layout.css.ts's shared <=1023px block targets it directly\n (header [class$=\"_label\"]:has(> svg) { display: block !important }),\n specificity (0,3,1), higher than the [data-slot] wrapper rule (0,2,1) \u2014\n display wins on specificity before source order, so a same-selector\n override is needed here too. */\n [data-phase] header [class$=\"_label\"]:has(> svg) {\n display: none !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-back\"] {\n display: inline-flex !important;\n }\n /* header-viewrow is position:absolute (see below), so its re-shown\n display value must match the flex layout its own rule declares. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-viewrow\"] {\n display: flex !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-info\"],\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex !important;\n }\n\n /* Back button: 44pt touch target, leftmost column. */\n [data-mobile-nav=\"header-back\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-back\"]:active {\n opacity: .6;\n }\n\n /* Info + workbench utility buttons, rightmost column. */\n [data-mobile-nav=\"header-info\"],\n [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-info\"]:active,\n [data-mobile-nav=\"header-workbench\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n /* Icon family unification (real-device round 2, 2026-08-17): the \u24D8 text\n glyph is gone (MobileSessionHeader.tsx now renders IconInfoOutline16,\n a local 16px SVG built to match the primitives icon family) and the\n workbench button's IconPanelLeftOutline16 is mirrored into a\n panel-RIGHT glyph \u2014 there's no IconPanelRightOutline16 in primitives\n (checked lib/types/icons/index.d.ts), and the plugin's own right-side\n panel semantics are exactly the left icon flipped. Both buttons now\n carry a same-size (16px), same-stroke-weight icon. */\n [data-mobile-nav=\"header-workbench\"] svg {\n transform: scaleX(-1);\n }\n\n /* Official Chat/Trajectory tablist: removed from layout entirely (S2.1 \u2014\n visibility:hidden still held a 27px row, so the header carried the\n view-switch row's band TWICE: 72 + 27 + gap = 104px measured at 390px,\n and a real iPhone added ~54px of notch on top of that). The header's own\n padding-bottom above is now the row's seat.\n display:none does NOT break the view switch: HTMLElement.click()\n dispatches synthetically and fires React's handler regardless of\n rendering \u2014 only real pointer hit-testing needs a box, and this plugin\n never relies on it (MobileSessionHeader always calls .click()). */\n [data-phase] header [class$=\"_tabs\"][role=\"tablist\"] {\n display: none;\n }\n\n /* View-switch row: \"current view name + dots\", replacing the (still\n present, still clickable) official tablist visually. It renders inside\n headerActions/headerUtilities, but titleRow's own position:relative\n (set above) makes IT the containing block for absolute descendants \u2014\n display:contents on titleCluster does not break that search \u2014 so\n top:100% sits directly under the title regardless of the title row's\n actual height, with no hardcoded offset to keep in sync. */\n [data-mobile-nav=\"header-viewrow\"] {\n position: absolute;\n left: 0;\n right: 0;\n top: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i {\n display: block;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: currentColor;\n opacity: .35;\n font-style: normal;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i[data-active] {\n opacity: 1;\n }\n}\n";
1
+ export declare const HEADER_CSS = "/* ---------- session header five-piece reflow (< 768px) ---------- */\n\n/* New elements render unconditionally (React does not know about media\n queries); default them to hidden so 768px+ never sees them, then\n re-enable inside the phone block below. Belt-and-braces alongside the\n scoped rules \u2014 mirrors the existing [data-mobile-nav=\"*\"] desktop no-op\n list in misc.css.ts. */\n[data-mobile-nav=\"header-back\"],\n[data-mobile-nav=\"header-viewrow\"],\n[data-mobile-nav=\"header-activity\"],\n[data-mobile-nav=\"header-info\"],\n[data-mobile-nav=\"header-workbench\"],\n/* The workbench close pill's default-hidden used to live in compat.css.ts \u2014\n but EVERYTHING in layout/compat/misc sits inside one shared\n (max-width: 1023px) block (opened at the top of layout, closed at the end\n of misc), so that \"top-level\" rule silently never applied at >=1024 and\n the pill rendered as an unstyled button on desktop (2026-08-17 report).\n This file is appended after the shared block closes, so here it is truly\n global; the <=767px :has() show rule in compat overrides it with\n !important. */\n[data-mobile-nav=\"better-sidebar-close\"] {\n display: none;\n}\n\n@media (max-width: 767px) {\n /* The Files action targeted the dsh-web-ui-all (aionui) explorer sheet \u2014\n a different, unrelated suite from the workbench entry this slice adds\n (dsh-better-sidebar). Hidden here only; 768-1023px keeps it exactly as\n v1.0.0 shipped it. */\n [data-mobile-nav=\"files\"] {\n display: none !important;\n }\n\n /* --- three-column header: [back 92px] [title, truly centered] [utilities 92px] --- */\n /* The bottom padding is the view-switch row's seat: that row is\n position:absolute (see the bottom of this file \u2014 it renders two levels\n deep inside headerActions and can never be a grid item of titleRow), so\n without a reserve it would hang over the message list. 28px = its\n height. Unconditional rather than header:has([data-mobile-nav=\n \"header-viewrow\"]) on purpose: :has() is silently dropped by pre-105\n WebViews (see AGENTS.md) and a 28px overlap on the first message is a\n worse failure than 28px of empty band in the (never observed in\n practice) single-view session. Total header chrome: 48 + 28 = 76px.\n Left/right 8px (S2.1 report leftover, fixed in S4): the official header\n had zero horizontal padding, so the utilities button cluster's right\n edge sat flush against the screen edge (x=390 at 390px width, no\n margin). The 92px grid columns absorb this fine \u2014 they're fixed track\n widths, only the center 1fr column shrinks by 16px. */\n [data-phase] header {\n position: relative;\n padding: 0 8px 28px !important;\n }\n /* No header bottom line \u2014 fade instead (real-device round 2, 2026-08-17).\n The official header draws its border as a `::after` 1px bar (the\n `border-bottom` on the header itself is transparent, just a layout\n reserve \u2014 dsh-client-ui-conversation lib/client.js \".wSkVaW_header{\n border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;\n position:absolute;bottom:1px...}\", verified live 2026-08-17), so\n hiding the pseudo-element is enough; the header's own border-bottom\n never had a visible color to begin with. The message scroller now\n fades in from under the header instead (styles/composer.css.ts, \"no\n divider above OR below the message list\" \u2014 the two edges share one\n mask-image on [class$=\"_scrollBody\"]). */\n [data-phase] header::after {\n display: none !important;\n }\n /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for\n the \"\u6807\u9898\u88AB\u6324\u4E0B\u53BB\" report). The three items are placed with explicit\n grid-column but arrive in DOM order crumbs(2) \u2192 headerActions(1) \u2192\n headerUtilities(3): sparse auto-placement never moves its cursor\n backwards, so headerActions and headerUtilities were pushed onto an\n implicit SECOND row. Measured at 390px: row1 = crumbs 28px, row2 =\n back button 44px, titleRow = 72px with the title stuck at the top\n instead of centered. Pinning the row makes it one 48px band again. */\n [data-phase] header [class$=\"_titleRow\"] {\n position: relative;\n display: grid;\n grid-template-columns: 92px 1fr 92px;\n grid-template-rows: minmax(48px, auto);\n align-items: center;\n min-height: 48px;\n padding: 0 !important;\n }\n /* Flatten the official cluster so its two children (crumbs, headerActions)\n become direct grid items of titleRow instead of a nested flex box. */\n [data-phase] header [class$=\"_titleCluster\"] {\n display: contents;\n }\n /* margin-left: 0 cancels layout.css.ts's shared <=1023px\n \"margin-left: auto !important\" on this exact selector (it pushed the\n Files button to the row's right edge in the old non-grid header;\n inside our grid column it instead shoves the back button 48px right of\n the column's start \u2014 the auto margin absorbs the leftover space\n between the button's content width and the 92px column). */\n [data-phase] header [class$=\"_headerActions\"] {\n grid-column: 1;\n grid-row: 1;\n justify-self: start;\n margin-left: 0 !important;\n display: flex;\n align-items: center;\n min-width: 0;\n }\n /* layout.css.ts's shared <=1023px block hides this element outright\n (header > :first-child > :last-child { display: none !important },\n specificity (0,3,1) \u2014 v1.0.0 relocated the desktop Session-log capsule\n to the drawer footer on every narrow width). That selector is repeated\n here at equal specificity so the later source order wins the display\n property; the class-suffix selector alone (0,2,1) would silently lose. */\n [data-phase] header [class$=\"_headerUtilities\"],\n [data-phase] header > :first-child > :last-child {\n grid-column: 3;\n grid-row: 1;\n justify-self: end;\n display: flex !important;\n align-items: center;\n gap: 4px;\n min-width: 0;\n }\n /* justify-self: stretch (the grid default) so this fills the whole 1fr\n column as a definite-width box \u2014 center/center-content happens inside\n it. justify-self: center (tried first) sizes the item to its content\n with no automatic column cap, and the obvious fix \u2014 max-width: 100% \u2014\n resolves against the WRONG box here (measured 93.6px at 390px, where\n the column is ~206px): the item's DOM parent (titleCluster) is\n display:contents, and percentage max-width does not reliably resolve\n through a boxless ancestor to the grid track. Stretching sidesteps the\n percentage entirely \u2014 but layout.css.ts's shared <=1023px block ALSO\n caps this exact selector at max-width: 24vw !important (93.6px at\n 390px \u2014 the value this rule used to silently inherit once its own\n max-width was dropped in favor of stretch), so that must be reset to\n none here too. */\n [data-phase] header [class$=\"_crumbs\"] {\n grid-column: 2;\n grid-row: 1;\n justify-self: stretch;\n max-width: none !important;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n overflow: hidden;\n }\n /* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header\n lets you tap the parent session's title to switch back \u2014 that affordance\n was lost on phone when the whole parent chain was hidden as \"\u591A\u4F59\u9879\".\n Show only the immediate parent (for agent-teams member sessions that IS\n the captain/main session; deeper ancestors stay hidden \u2014 a 3-level chain\n cannot fit the ~206px center column at 390px). The parent crumb is the\n official breadcrumb <button> (dsh-client-ui-conversation renders\n open(parentId) as its onClick), so tapping needs no JS from this plugin,\n and main sessions (chain length 1) have no :nth-last-child(2) seg at\n all \u2014 the centered single title is pixel-identical to before. */\n [data-phase] header [class$=\"_crumbSeg\"]:not(:nth-last-child(-n+2)) {\n display: none !important;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) {\n display: inline-flex;\n align-items: center;\n flex: none;\n min-width: 0;\n max-width: 38%;\n }\n /* Ancestors BEFORE the parent seg are hidden, which would leave the\n parent seg's own leading \"/\" dangling at the start of the title. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumbSep\"] {\n display: none !important;\n }\n /* Parent crumb: small tertiary text so the 19px current title stays the\n visual anchor; min-height keeps a finger-sized target inside the 48px\n row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumb\"] {\n font-size: 13px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));\n min-height: 44px;\n display: inline-flex;\n align-items: center;\n padding: 0 2px 0 6px;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:last-child {\n display: flex;\n align-items: center;\n min-width: 0;\n max-width: 100%;\n }\n /* The last seg's own \"/\" is the divider between parent and current title \u2014\n it only exists when there IS an ancestor (index > 0), so it can simply\n stay visible now that the parent seg shows. (It used to be hidden here:\n with the parent hidden it dangled before the title.) */\n /* BOTH selectors are required: [class$=] matches the whole class\n ATTRIBUTE's suffix, and the current-session title button's attribute is\n \"wSkVaW_crumb wSkVaW_crumbCurrent\" \u2014 it ends in _crumbCurrent, so the _crumb\n suffix selector alone silently missed the one element that matters\n (measured live: the title stayed 14px while the rule sat in the bundle,\n 2026-08-17). */\n [data-phase] header [class$=\"_crumb\"],\n [data-phase] header [class$=\"_crumbCurrent\"] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n max-width: 100%;\n /* Real-device feedback (2026-08-17): the session title read too small\n next to the enlarged header icons \u2014 19px sits just under the home\n screen's 22px workspace title, keeping the two-level hierarchy. */\n font-size: 19px;\n font-weight: 600;\n }\n\n /* Running-status dot: no official element to reposition (the header\n renders only the crumb title, the two action slots, and the tablist \u2014\n no status indicator), so this is the plan's documented fallback \u2014\n read the data ourselves and draw it (effects/header-status.ts stamps\n data-mobile-nav-dot on the frame). A ::after on the title crumb keeps\n it truly inline with the (centered, truncated) text instead of a\n separately positioned box that would fight the centering math. */\n [data-phase] header [class$=\"_crumbCurrent\"]::after {\n content: '';\n display: none;\n width: 6px;\n height: 6px;\n margin-left: 6px;\n border-radius: 50%;\n vertical-align: middle;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot] header [class$=\"_crumbCurrent\"]::after {\n display: inline-block;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"ongoing\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"warning\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"done\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n\n /* Everything else the official header puts in these two list slots \u2014 the\n agent-preset mode badge, the jobs/subagent trigger, the desktop\n Session-log download capsule, and this plugin's own legacy directory\n toggle \u2014 is the \"\u4E0D\u5E38\u9A7B\" set the design defers to the S4 info card.\n Scoped by the slots' own [data-slot] wrapper (a stable, non-hashed\n contract marker) rather than enumerating each registrant, so a future\n third-party header action is hidden by default too. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > *,\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > * {\n display: none !important;\n }\n /* Native background-task / subagent entries: kept mounted, but parked as\n ZERO-WIDTH INVISIBLE ANCHORS at the right end of the view-switch band.\n Our activity pill is the visible control and forwards the tap to the\n official trigger, so the official popover \u2014 a child of this root \u2014 still\n mounts and positions from here.\n\n Why not just leave them visible in the header: each is ~103px wide while\n headerActions' grid column is 92px, and flex children that cannot shrink\n simply overflow \u2014 measured at 375px the subagent entry ran x=60..163\n straight across the centred title at x=100..275.\n\n Why not display:none them either: the menu lives inside the root, so\n hiding the root takes the popover with it and the pill would have\n nothing to open. Hide the TRIGGER (below) and keep the root as a box. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [class$=\"_root\"]:has(> button[class$=\"_trigger\"]) {\n display: block !important;\n position: absolute !important;\n right: 0 !important;\n top: 100% !important;\n width: 0 !important;\n height: 28px !important;\n overflow: visible !important;\n order: 0 !important;\n }\n /* The official trigger never shows \u2014 the pill replaced it. Programmatic\n .click() still reaches React's handler on a display:none button. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [class$=\"_root\"] > button[class$=\"_trigger\"] {\n display: none !important;\n }\n /* Anchor the popover to the RIGHT edge and clamp it to the viewport. The\n root now sits at the right edge with zero width, so the official\n `left: 0` would start the 336px panel off-screen; layout.css.ts's\n `left: 8px` re-anchor was written for a viewport-width containing block\n that this element is not. Right-anchoring is the one that holds for both\n (2026-08-20: the old pairing put the panel 29px past the right edge). */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] [class$=\"_menu\"] {\n left: auto !important;\n right: 0 !important;\n width: min(336px, calc(100vw - 32px)) !important;\n max-width: none !important;\n max-height: min(420px, calc(100dvh - 140px)) !important;\n }\n /* The agent-preset mode badge specifically survives the blanket hide\n above: layout.css.ts's shared <=1023px block targets it directly\n (header [class$=\"_label\"]:has(> svg) { display: block !important }),\n specificity (0,3,1), higher than the [data-slot] wrapper rule (0,2,1) \u2014\n display wins on specificity before source order, so a same-selector\n override is needed here too. */\n [data-phase] header [class$=\"_label\"]:has(> svg) {\n display: none !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-back\"] {\n display: inline-flex !important;\n }\n /* header-viewrow is position:absolute (see below), so its re-shown\n display value must match the flex layout its own rule declares. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-viewrow\"] {\n display: flex !important;\n }\n /* Our own chip lives in the same slot, so the blanket hide above swallows\n it unless it is named here too (it was, on the first build: the chip\n rendered with the right counts and painted nothing). */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-activity\"] {\n display: inline-flex !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-info\"],\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex !important;\n }\n\n /* Back button: 44pt touch target, leftmost column. */\n [data-mobile-nav=\"header-back\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-back\"]:active {\n opacity: .6;\n }\n\n /* Info + workbench utility buttons, rightmost column. */\n [data-mobile-nav=\"header-info\"],\n [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-info\"]:active,\n [data-mobile-nav=\"header-workbench\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n /* Icon family unification (real-device round 2, 2026-08-17): the \u24D8 text\n glyph is gone (MobileSessionHeader.tsx now renders IconInfoOutline16,\n a local 16px SVG built to match the primitives icon family) and the\n workbench button's IconPanelLeftOutline16 is mirrored into a\n panel-RIGHT glyph \u2014 there's no IconPanelRightOutline16 in primitives\n (checked lib/types/icons/index.d.ts), and the plugin's own right-side\n panel semantics are exactly the left icon flipped. Both buttons now\n carry a same-size (16px), same-stroke-weight icon. */\n [data-mobile-nav=\"header-workbench\"] svg {\n transform: scaleX(-1);\n }\n\n /* Official Chat/Trajectory tablist: removed from layout entirely (S2.1 \u2014\n visibility:hidden still held a 27px row, so the header carried the\n view-switch row's band TWICE: 72 + 27 + gap = 104px measured at 390px,\n and a real iPhone added ~54px of notch on top of that). The header's own\n padding-bottom above is now the row's seat.\n display:none does NOT break the view switch: HTMLElement.click()\n dispatches synthetically and fires React's handler regardless of\n rendering \u2014 only real pointer hit-testing needs a box, and this plugin\n never relies on it (MobileSessionHeader always calls .click()). */\n [data-phase] header [class$=\"_tabs\"][role=\"tablist\"] {\n display: none;\n }\n\n /* View-switch row: \"current view name + dots\", replacing the (still\n present, still clickable) official tablist visually. It renders inside\n headerActions/headerUtilities, but titleRow's own position:relative\n (set above) makes IT the containing block for absolute descendants \u2014\n display:contents on titleCluster does not break that search \u2014 so\n top:100% sits directly under the title regardless of the title row's\n actual height, with no hardcoded offset to keep in sync. */\n /* Activity chip (subagents / background tasks) rides the right end of the\n same band. It is a SIBLING of the switch button, not a child \u2014 that\n button spans the full row, so nesting would make every tap on the chip\n switch views too \u2014 and it shares titleRow as its containing block, so\n top:100% lands both on the same 28px line. z-index puts it above the\n row it overlaps. */\n [data-mobile-nav=\"header-activity\"] {\n position: absolute;\n right: 0;\n top: 100%;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n gap: 9px;\n height: 28px;\n padding: 0 2px 0 10px;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"activity-pill\"] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"activity-pill\"]:active {\n opacity: .55;\n }\n [data-mobile-nav=\"activity-count\"] {\n font-variant-numeric: tabular-nums;\n }\n /* Steady green = everything settled. */\n [data-mobile-nav=\"activity-dot\"] {\n display: block;\n width: 6px;\n height: 6px;\n margin-left: 1px;\n border-radius: 50%;\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n /* A job that failed or was killed is not a success \u2014 never paint it green. */\n [data-activity-state=\"warning\"] [data-mobile-nav=\"activity-dot\"] {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n /* Pulsing blue = still running. Same state palette as the session dot. */\n [data-activity-state=\"running\"] [data-mobile-nav=\"activity-dot\"] {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n animation: dsh-zen-activity-pulse 1.4s ease-in-out infinite;\n }\n @keyframes dsh-zen-activity-pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: .45; transform: scale(.72); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-activity-state=\"running\"] [data-mobile-nav=\"activity-dot\"] {\n animation: none;\n }\n }\n\n [data-mobile-nav=\"header-viewrow\"] {\n position: absolute;\n left: 0;\n right: 0;\n top: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i {\n display: block;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: currentColor;\n opacity: .35;\n font-style: normal;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i[data-active] {\n opacity: 1;\n }\n}\n";
2
2
  //# sourceMappingURL=header.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"header.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/header.css.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,UAAU,6sjBAgYtB,CAAA"}
1
+ {"version":3,"file":"header.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/header.css.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,UAAU,+2tBA2ftB,CAAA"}
@@ -1,2 +1,2 @@
1
- export declare const HOME_CSS = "/* ---------- phone app shell (< 768px) ---------- */\n\n@media (max-width: 767px) {\n /* --- the document never scrolls (S1.1, 2026-08-17) ---\n Real-device symptom: the workspace title bar, the session header, the\n composer and the FAB all slid with the finger \u2014 every fixed surface\n \"followed the drag\". They are not misplaced; the whole DOCUMENT was\n rubber-banding under them (they are absolute/in-flow inside the frame,\n so a document-level bounce moves them all as one).\n\n After the S2.1 box-sizing fix the document has no overflow at all\n (scrollHeight === clientHeight, measured with ?mobile-nav-inset=54), so\n this is not scrolling \u2014 it is iOS's elastic overscroll, which happens on\n an unscrollable document too, and which an inner scroller chains into as\n soon as it hits its own end.\n\n overscroll-behavior: none on the viewport kills both halves at once: the\n document itself gets no bounce, and overscroll chained up from the\n message flow / session list is absorbed without moving anything.\n overflow: hidden then hard-locks the document scroller so future content\n can never reintroduce a real scroll. Set on html AND body: the spec\n propagates the viewport's value from html, but engines have historically\n read body, and neither is a scroll container we ever want.\n\n Deliberately NOT position: fixed on body \u2014 the app shell does not need\n it, and it is the variant that strands iOS's fixed elements behind the\n on-screen keyboard. With plain overflow: hidden, iOS still pans the\n visual viewport to reveal a focused textarea, so the composer stays\n visible while typing without any visualViewport JS. */\n html,\n body {\n overflow: hidden !important;\n overscroll-behavior: none !important;\n }\n\n /* The message flow is the one scroller that regularly hits its end under a\n finger. Root-level `none` already absorbs the chain, but declaring it\n at the source keeps the guarantee if the root rule is ever weakened\n (the home list and both sheets already declare it).\n\n `none`, NOT `contain` (S4.1 fix, 2026-08-17). The two differ in exactly\n the way that bit us: `contain` stops overscroll from CHAINING OUT to the\n document but deliberately KEEPS this element's own local elastic bounce,\n while `none` suppresses that local bounce too. S1.1 only reasoned about\n the chain, so it picked `contain` \u2014 and the composer went on twitching\n upward on every drag-past-the-end while the header and FAB stayed put.\n\n Why only the composer moved: the official composer seat\n (`[class$=\"_composerSeat\"]`) is `position: sticky; bottom: 0` and lives\n INSIDE this scroller (measured 2026-08-17: seat 0,710 390x134 inside\n _scrollBody 0,131 390x713, contains() === true). A sticky box is laid out\n against its scroll container's content, so the container's own rubber-band\n drags it along. The header/FAB are absolute on the frame, outside this\n scroller entirely \u2014 which is precisely why S1.1's document-level fix\n pinned them and left this one surface behind.\n\n Deliberately NOT re-pinning the seat with position: fixed: sticky inside\n the scroller is what keeps the focused textarea visible when iOS pans the\n visual viewport for the keyboard, and fixed is the variant that strands\n it behind the keyboard (same trap as the body rule above). Kill the\n bounce, keep the sticky. */\n [data-phase] [class$=\"_scrollBody\"] {\n overscroll-behavior: none !important;\n }\n\n /* The official sidebar is no longer a drawer on a phone \u2014 the home screen\n replaced it. Hidden outright (not translated off-screen) so it cannot\n capture taps or hold layout. */\n [data-mobile-nav=\"frame\"] > :first-child {\n display: none !important;\n }\n /* The header's drawer toggle opened that sidebar; with it gone the button\n has nothing to open. (S2 owns the rest of the header layout \u2014\n styles/header.css.ts \u2014 including the Files button's replacement.) */\n [data-mobile-nav=\"toggle\"] {\n display: none !important;\n }\n\n /* --- level 1: the session list ---\n Renders inside the shell overlay layer (absolute, pointer-events: none),\n so the page re-enables pointer events for itself. The layer's containing\n block is the frame's padding box, which still starts under the status\n bar \u2014 hence the safe-area padding here. */\n [data-mobile-nav=\"home\"] {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n flex-direction: column;\n pointer-events: auto;\n /* dsw-specific-sidebar-fill, not an alias bg-layer-* token (real-device\n round 2 follow-up, 2026-08-17): fetched and diffed the live theme\n CSS (both light/dark blocks in /assets/index-*.css) because computed\n values, not source-read guesses, are what actually matter here \u2014\n --dsw-alias-bg-base/-layer-1/-layer-2 all resolve to the exact same\n color in the LIGHT theme (neutral-bluish-00, i.e. plain white); they\n only diverge in dark mode. A layer-* token would have made the page\n and its cards indistinguishable in light mode specifically \u2014 the\n opposite of what was asked. --dsw-specific-sidebar-fill differs from\n bg-base in BOTH themes (bluish-50 vs -00 light, bluish-900 vs -950\n dark) and is the exact token dsh-better-sidebar's own panel already\n uses for this same \"secondary surface next to bg-base content\"\n role, so it is the correct reuse rather than a new hardcoded gray. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: var(--dsw-alias-label-primary, inherit);\n padding-top: var(--mnav-sat);\n transform: translateX(0);\n opacity: 1;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out);\n }\n /* Push transition: the list slides out to the left as the session takes\n the screen, and comes back from the left. It stays mounted (visibility,\n not display) so the slide has something to animate; the delayed\n visibility keeps it non-interactive the moment the transition ends. */\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transform: translateX(-100%);\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out),\n visibility 0s .3s;\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home\"],\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transition: none !important;\n }\n }\n\n /* Title bar: the workspace name IS the switcher. */\n [data-mobile-nav=\"home-top\"] {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n min-height: 52px;\n padding: 4px 16px 6px;\n }\n /* Site logo (real-device round 2, 2026-08-17): rendered only when\n document.head actually has a <link rel=\"icon\"> (MobileHome.tsx never\n ships a placeholder box), so this rule only ever needs to size the\n image, not reserve space for its absence. */\n [data-mobile-nav=\"home-logo\"] {\n width: 24px;\n height: 24px;\n margin-right: 6px;\n flex: none;\n border-radius: 6px;\n object-fit: contain;\n }\n /* Dark theme (real-device report, 2026-08-17): the runtime favicon is a\n dark-on-transparent whale mark, illegible on the dark page. Force a\n white silhouette regardless of the source image's own colors \u2014\n brightness(0) collapses every opaque pixel to black first, invert(1)\n then flips that to white; transparent pixels stay transparent either\n way. Same body[data-ds-dark-theme] marker as the home-row hairline\n above (AGENTS.md: prefers-color-scheme never fires, the app switches\n themes via this attribute with color-scheme hardcoded to light). */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-logo\"] {\n filter: brightness(0) invert(1);\n }\n [data-mobile-nav=\"ws-switch\"] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n min-height: 44px;\n max-width: 100%;\n padding: 0 6px 0 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 22px;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"ws-switch\"] > span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"ws-switch\"] > svg {\n flex: none;\n opacity: .55;\n }\n [data-mobile-nav=\"ws-switch\"]:active {\n opacity: .6;\n }\n\n /* Session list: rounded cards, not bordered rows (real-device round 2\n follow-up, 2026-08-17 \u2014 reference: Claude Code mobile app's session\n list). `gap` on the flex column IS the inter-card spacing; no\n per-row margin bookkeeping. */\n [data-mobile-nav=\"home-list\"] {\n flex: 1 1 auto;\n min-height: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 16px calc(var(--mnav-sab) + 96px);\n list-style: none;\n overflow-y: auto;\n overscroll-behavior: contain;\n -webkit-overflow-scrolling: touch;\n }\n [data-mobile-nav=\"home-row\"] {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 60px;\n padding: 14px 16px;\n border: none;\n border-radius: 22px;\n background: var(--dsw-alias-bg-base, #ffffff);\n color: inherit;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n /* Card edge definition (2026-08-17). Light theme only \u2014 see the dark\n override below. Two stacked shadows in the iOS idiom: a tight 1px\n contact shadow that draws the edge itself, plus a wide soft one that\n lifts the card off the page. Both are deliberately weak (.06/.05):\n the page sits on --dsw-specific-sidebar-fill and the card on\n --dsw-alias-bg-base, so there is already a slight tonal step here and\n the shadow only has to sharpen it, not carry it alone. */\n box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);\n }\n [data-mobile-nav=\"home-row\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n /* Press = settle toward the page. Dropping the wide shadow and keeping a\n tighter contact one reads as the card pushing in, which is what the\n darkening background already implies; leaving the lifted shadow under\n a pressed card is the combination that looks wrong. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n }\n /* Dark theme: a black shadow on a near-black page is invisible, so the\n edge is drawn instead of cast. A 1px hairline at --dsw-alias-border-l2\n weight, delivered as an INSET box-shadow rather than a real border so\n the card's geometry is untouched (a real 1px border would grow every\n row by 2px unless box-sizing cooperated) and so it overrides the light\n shadow through the same property instead of fighting it.\n\n Chosen over \"brighten the card face\" because in dark theme the card\n (--dsw-alias-bg-base, bluish-950) is already DARKER than the page\n (--dsw-specific-sidebar-fill, bluish-900); lightening the card past the\n page would invert the layering that the light theme establishes, while\n a hairline keeps both themes reading as \"page behind, card in front\".\n\n Theme is read from body[data-ds-dark-theme], never prefers-color-scheme:\n the app hardcodes color-scheme: light on <html> and switches themes with\n this attribute, so a media query would never fire (AGENTS.md). This\n selector also outranks the :active rule above (0,2,1 vs 0,2,0), so one\n declaration covers the pressed state too. */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-row\"] {\n box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(255, 255, 255, .12));\n }\n [data-mobile-nav=\"home-row\"][data-current] [data-mobile-nav=\"home-row-title\"] {\n font-weight: 600;\n }\n /* Avatar: a running/warning/done session shows its existing StateDot\n (same component, same semantics as the old inline dot \u2014 just bigger\n and re-homed); otherwise the title's own first character stands in\n for it, so every row has a mark even when idle. interactive-bg-hover,\n not a bg-layer-* token: same reason as the page background above \u2014\n bg-layer-2 is IDENTICAL to the card's own bg-base in light theme\n (verified against the live theme CSS), so it would render invisible\n there. interactive-bg-hover is a translucent rgba tint rather than a\n solid layer color, so it always reads as \"a shade over the card\" in\n both themes regardless of what the card's base color resolves to. */\n [data-mobile-nav=\"home-row-avatar\"] {\n flex: none;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n border-radius: 12px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .55));\n font-size: 16px;\n font-weight: 600;\n }\n [data-mobile-nav=\"home-row-body\"] {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n [data-mobile-nav=\"home-row-title\"] {\n font-size: 16px;\n line-height: 21px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-status\"] {\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 12.5px;\n line-height: 17px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-time\"] {\n flex: none;\n align-self: flex-start;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .4));\n font-size: 12.5px;\n line-height: 18px;\n }\n [data-mobile-nav=\"home-empty\"] {\n margin: 12px 16px;\n padding: 48px 24px;\n border-radius: 22px;\n background: var(--dsw-alias-bg-base, #ffffff);\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 15px;\n text-align: center;\n }\n\n /* New-session FAB: a labeled pill (real-device round 2 follow-up), not a\n bare circle \u2014 tap starts in the shown workspace, long press picks\n one. Inverted-surface tokens (not a hardcoded accent color): the same\n pair the official git-commit button in dsh-better-sidebar uses for\n its own solid CTA. */\n [data-mobile-nav=\"home-fab\"] {\n position: absolute;\n right: 18px;\n bottom: calc(var(--mnav-sab) + 22px);\n z-index: 6;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 48px;\n padding: 0 20px 0 16px;\n border: none;\n border-radius: 999px;\n background: var(--dsw-alias-button-primary-fill, #1a1a1a);\n color: var(--dsw-alias-label-primary-inverted, #ffffff);\n font-family: inherit;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .24);\n touch-action: manipulation;\n user-select: none;\n -webkit-user-select: none;\n -webkit-touch-callout: none;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-fab\"]:active {\n transform: scale(.96);\n }\n\n /* Workspace sheet (switcher and long-press New Session share it). */\n [data-mobile-nav=\"home-sheet-layer\"] {\n position: absolute;\n inset: 0;\n z-index: 7;\n }\n [data-mobile-nav=\"home-sheet-mask\"] {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-3, rgba(0, 0, 0, .45));\n border: none;\n animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);\n }\n /* Docked to the screen edge, matching the composer's permission/model\n sheets (styles/composer.css.ts section 4) rather than floating.\n\n It used to sit at `bottom: calc(var(--mnav-sab) + 8px)` with all four\n corners rounded, which left a 42px strip of bare mask under it at\n sab: 34 (measured). Docking removes the strip instead of dimming it, and\n the safe area moves INSIDE the card as bottom padding \u2014 so the white\n card body runs all the way to the physical screen edge and the last row\n still clears the home indicator. Same reason the corners go top-only:\n a rounded bottom corner on a card flush with the screen edge reads as a\n rendering mistake. */\n [data-mobile-nav=\"home-sheet\"] {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n max-height: 70%;\n overflow-y: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n padding: 6px 6px calc(var(--mnav-sab) + 14px);\n border-radius: 20px 20px 0 0;\n /* layer-2, not bg-base: in dark mode the sheet must lift off a page that\n shares bg-base, or only the shadow separates them. */\n background: var(--dsw-alias-bg-layer-2, #ffffff);\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .28);\n animation: dsh-mobile-nav-sheet-up .22s var(--ds-ease-out, ease-in-out);\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home-sheet-mask\"],\n [data-mobile-nav=\"home-sheet\"] {\n animation: none !important;\n }\n }\n [data-mobile-nav=\"home-sheet-title\"] {\n padding: 10px 12px 6px;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 13px;\n }\n [data-mobile-nav=\"home-sheet-item\"] {\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 48px;\n padding: 0 12px;\n border: none;\n border-radius: 12px;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 16px;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-sheet-item\"][data-selected] {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n font-weight: 600;\n }\n [data-mobile-nav=\"home-sheet-item\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n\n /* --- hero fallback back button ---\n The hero (new blank session) page renders no conversation.session.header,\n so the header-slot back button (S2) does not exist there. This floating\n fallback covers that page and hides itself as soon as the real header\n back mounts. */\n [data-mobile-nav=\"hero-back\"] {\n position: absolute;\n top: calc(var(--mnav-sat) + 8px);\n left: 8px;\n z-index: 6;\n width: 44px;\n height: 44px;\n display: grid;\n place-items: center;\n border: none;\n border-radius: 14px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n pointer-events: auto;\n }\n [data-mobile-nav=\"hero-back\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n [data-mobile-nav=\"frame\"]:has([data-mobile-nav=\"header-back\"]) ~ [data-mobile-nav=\"hero-back\"],\n body:has([data-mobile-nav=\"header-back\"]) [data-mobile-nav=\"hero-back\"] {\n display: none;\n }\n}\n";
1
+ export declare const HOME_CSS = "/* ---------- phone app shell (< 768px) ---------- */\n\n@media (max-width: 767px) {\n /* --- the document never scrolls (S1.1, 2026-08-17) ---\n Real-device symptom: the workspace title bar, the session header, the\n composer and the FAB all slid with the finger \u2014 every fixed surface\n \"followed the drag\". They are not misplaced; the whole DOCUMENT was\n rubber-banding under them (they are absolute/in-flow inside the frame,\n so a document-level bounce moves them all as one).\n\n After the S2.1 box-sizing fix the document has no overflow at all\n (scrollHeight === clientHeight, measured with ?mobile-nav-inset=54), so\n this is not scrolling \u2014 it is iOS's elastic overscroll, which happens on\n an unscrollable document too, and which an inner scroller chains into as\n soon as it hits its own end.\n\n overscroll-behavior: none on the viewport kills both halves at once: the\n document itself gets no bounce, and overscroll chained up from the\n message flow / session list is absorbed without moving anything.\n overflow: hidden then hard-locks the document scroller so future content\n can never reintroduce a real scroll. Set on html AND body: the spec\n propagates the viewport's value from html, but engines have historically\n read body, and neither is a scroll container we ever want.\n\n Deliberately NOT position: fixed on body \u2014 the app shell does not need\n it, and it is the variant that strands iOS's fixed elements behind the\n on-screen keyboard. With plain overflow: hidden, iOS still pans the\n visual viewport to reveal a focused textarea, so the composer stays\n visible while typing without any visualViewport JS. */\n html,\n body {\n overflow: hidden !important;\n overscroll-behavior: none !important;\n }\n\n /* The message flow is the one scroller that regularly hits its end under a\n finger. Root-level `none` already absorbs the chain, but declaring it\n at the source keeps the guarantee if the root rule is ever weakened\n (the home list and both sheets already declare it).\n\n `none`, NOT `contain` (S4.1 fix, 2026-08-17). The two differ in exactly\n the way that bit us: `contain` stops overscroll from CHAINING OUT to the\n document but deliberately KEEPS this element's own local elastic bounce,\n while `none` suppresses that local bounce too. S1.1 only reasoned about\n the chain, so it picked `contain` \u2014 and the composer went on twitching\n upward on every drag-past-the-end while the header and FAB stayed put.\n\n Why only the composer moved: the official composer seat\n (`[class$=\"_composerSeat\"]`) is `position: sticky; bottom: 0` and lives\n INSIDE this scroller (measured 2026-08-17: seat 0,710 390x134 inside\n _scrollBody 0,131 390x713, contains() === true). A sticky box is laid out\n against its scroll container's content, so the container's own rubber-band\n drags it along. The header/FAB are absolute on the frame, outside this\n scroller entirely \u2014 which is precisely why S1.1's document-level fix\n pinned them and left this one surface behind.\n\n Deliberately NOT re-pinning the seat with position: fixed: sticky inside\n the scroller is what keeps the focused textarea visible when iOS pans the\n visual viewport for the keyboard, and fixed is the variant that strands\n it behind the keyboard (same trap as the body rule above). Kill the\n bounce, keep the sticky. */\n [data-phase] [class$=\"_scrollBody\"] {\n overscroll-behavior: none !important;\n }\n\n /* The official sidebar is no longer a drawer on a phone \u2014 the home screen\n replaced it. Hidden outright (not translated off-screen) so it cannot\n capture taps or hold layout. */\n [data-mobile-nav=\"frame\"] > :first-child {\n display: none !important;\n }\n /* The header's drawer toggle opened that sidebar; with it gone the button\n has nothing to open. (S2 owns the rest of the header layout \u2014\n styles/header.css.ts \u2014 including the Files button's replacement.) */\n [data-mobile-nav=\"toggle\"] {\n display: none !important;\n }\n\n /* --- level 1: the session list ---\n Renders inside the shell overlay layer (absolute, pointer-events: none),\n so the page re-enables pointer events for itself. The layer's containing\n block is the frame's padding box, which still starts under the status\n bar \u2014 hence the safe-area padding here. */\n [data-mobile-nav=\"home\"] {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n flex-direction: column;\n pointer-events: auto;\n /* dsw-specific-sidebar-fill, not an alias bg-layer-* token (real-device\n round 2 follow-up, 2026-08-17): fetched and diffed the live theme\n CSS (both light/dark blocks in /assets/index-*.css) because computed\n values, not source-read guesses, are what actually matter here \u2014\n --dsw-alias-bg-base/-layer-1/-layer-2 all resolve to the exact same\n color in the LIGHT theme (neutral-bluish-00, i.e. plain white); they\n only diverge in dark mode.\n\n 2026-08-20: the page and the CARDS swapped tokens. This surface used\n to be --dsw-specific-sidebar-fill, which made the list page a\n different shade from the session page \u2014 measured on-device in dark\n theme, rgb(27,27,28) here against rgb(21,21,23) there, a visible step\n every time you opened a session. The page now takes bg-base, the same\n token the session page and <body> resolve to, so the two screens are\n literally the same colour; the cards took sidebar-fill in exchange, so\n the contrast that separates a row from the page is preserved \u2014 it\n just runs the other way (raised card on a base page, the more\n conventional reading of the two anyway). */\n background: var(--dsw-alias-bg-base, #ffffff);\n color: var(--dsw-alias-label-primary, inherit);\n padding-top: var(--mnav-sat);\n transform: translateX(0);\n opacity: 1;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out);\n }\n /* Push transition: the list slides out to the left as the session takes\n the screen, and comes back from the left. It stays mounted (visibility,\n not display) so the slide has something to animate; the delayed\n visibility keeps it non-interactive the moment the transition ends. */\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transform: translateX(-100%);\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out),\n visibility 0s .3s;\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home\"],\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transition: none !important;\n }\n }\n\n /* Title bar: the workspace name IS the switcher. */\n [data-mobile-nav=\"home-top\"] {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n min-height: 52px;\n padding: 4px 16px 6px;\n }\n /* Site logo (real-device round 2, 2026-08-17): rendered only when\n document.head actually has a <link rel=\"icon\"> (MobileHome.tsx never\n ships a placeholder box), so this rule only ever needs to size the\n image, not reserve space for its absence. */\n [data-mobile-nav=\"home-logo\"] {\n width: 24px;\n height: 24px;\n margin-right: 6px;\n flex: none;\n border-radius: 6px;\n object-fit: contain;\n }\n /* Dark theme (real-device report, 2026-08-17): the runtime favicon is a\n dark-on-transparent whale mark, illegible on the dark page. Force a\n white silhouette regardless of the source image's own colors \u2014\n brightness(0) collapses every opaque pixel to black first, invert(1)\n then flips that to white; transparent pixels stay transparent either\n way. Same body[data-ds-dark-theme] marker as the home-row hairline\n above (AGENTS.md: prefers-color-scheme never fires, the app switches\n themes via this attribute with color-scheme hardcoded to light). */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-logo\"] {\n filter: brightness(0) invert(1);\n }\n [data-mobile-nav=\"ws-switch\"] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n min-height: 44px;\n max-width: 100%;\n padding: 0 6px 0 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 22px;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"ws-switch\"] > span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"ws-switch\"] > svg {\n flex: none;\n opacity: .55;\n }\n [data-mobile-nav=\"ws-switch\"]:active {\n opacity: .6;\n }\n\n /* Session list: rounded cards, not bordered rows (real-device round 2\n follow-up, 2026-08-17 \u2014 reference: Claude Code mobile app's session\n list). `gap` on the flex column IS the inter-card spacing; no\n per-row margin bookkeeping. */\n [data-mobile-nav=\"home-list\"] {\n flex: 1 1 auto;\n min-height: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 16px calc(var(--mnav-sab) + 96px);\n list-style: none;\n overflow-y: auto;\n overscroll-behavior: contain;\n -webkit-overflow-scrolling: touch;\n }\n [data-mobile-nav=\"home-row\"] {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 60px;\n padding: 14px 16px;\n border: none;\n border-radius: 22px;\n /* The page's old colour (see the swap note on [data-mobile-nav=\"home\"]):\n differs from bg-base in BOTH themes, and is the token\n dsh-better-sidebar's panel already uses for a secondary surface. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: inherit;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n /* Card edge definition (2026-08-17). Light theme only \u2014 see the dark\n override below. Two stacked shadows in the iOS idiom: a tight 1px\n contact shadow that draws the edge itself, plus a wide soft one that\n lifts the card off the page. Both are deliberately weak (.06/.05):\n the page sits on --dsw-specific-sidebar-fill and the card on\n --dsw-alias-bg-base, so there is already a slight tonal step here and\n the shadow only has to sharpen it, not carry it alone. */\n box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);\n }\n [data-mobile-nav=\"home-row\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n /* Press = settle toward the page. Dropping the wide shadow and keeping a\n tighter contact one reads as the card pushing in, which is what the\n darkening background already implies; leaving the lifted shadow under\n a pressed card is the combination that looks wrong. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n }\n /* Dark theme: a black shadow on a near-black page is invisible, so the\n edge is drawn instead of cast. A 1px hairline at --dsw-alias-border-l2\n weight, delivered as an INSET box-shadow rather than a real border so\n the card's geometry is untouched (a real 1px border would grow every\n row by 2px unless box-sizing cooperated) and so it overrides the light\n shadow through the same property instead of fighting it.\n\n Chosen over \"brighten the card face\" because in dark theme the card\n (--dsw-alias-bg-base, bluish-950) is already DARKER than the page\n (--dsw-specific-sidebar-fill, bluish-900); lightening the card past the\n page would invert the layering that the light theme establishes, while\n a hairline keeps both themes reading as \"page behind, card in front\".\n\n Theme is read from body[data-ds-dark-theme], never prefers-color-scheme:\n the app hardcodes color-scheme: light on <html> and switches themes with\n this attribute, so a media query would never fire (AGENTS.md). This\n selector also outranks the :active rule above (0,2,1 vs 0,2,0), so one\n declaration covers the pressed state too. */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-row\"] {\n box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(255, 255, 255, .12));\n }\n [data-mobile-nav=\"home-row\"][data-current] [data-mobile-nav=\"home-row-title\"] {\n font-weight: 600;\n }\n /* Avatar: a running/warning/done session shows its existing StateDot\n (same component, same semantics as the old inline dot \u2014 just bigger\n and re-homed); otherwise the title's own first character stands in\n for it, so every row has a mark even when idle. interactive-bg-hover,\n not a bg-layer-* token: same reason as the page background above \u2014\n bg-layer-2 is IDENTICAL to the card's own bg-base in light theme\n (verified against the live theme CSS), so it would render invisible\n there. interactive-bg-hover is a translucent rgba tint rather than a\n solid layer color, so it always reads as \"a shade over the card\" in\n both themes regardless of what the card's base color resolves to. */\n [data-mobile-nav=\"home-row-avatar\"] {\n flex: none;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n border-radius: 12px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .55));\n font-size: 16px;\n font-weight: 600;\n }\n [data-mobile-nav=\"home-row-body\"] {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n [data-mobile-nav=\"home-row-title\"] {\n font-size: 16px;\n line-height: 21px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-status\"] {\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 12.5px;\n line-height: 17px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-time\"] {\n flex: none;\n align-self: flex-start;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .4));\n font-size: 12.5px;\n line-height: 18px;\n }\n [data-mobile-nav=\"home-empty\"] {\n margin: 12px 16px;\n padding: 48px 24px;\n border-radius: 22px;\n /* Stands in for the rows, so it carries the rows' surface. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 15px;\n text-align: center;\n }\n\n /* New-session FAB: a labeled pill (real-device round 2 follow-up), not a\n bare circle \u2014 tap starts in the shown workspace, long press picks\n one. Inverted-surface tokens (not a hardcoded accent color): the same\n pair the official git-commit button in dsh-better-sidebar uses for\n its own solid CTA. */\n [data-mobile-nav=\"home-fab\"] {\n position: absolute;\n right: 18px;\n bottom: calc(var(--mnav-sab) + 22px);\n z-index: 6;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 48px;\n padding: 0 20px 0 16px;\n border: none;\n border-radius: 999px;\n background: var(--dsw-alias-button-primary-fill, #1a1a1a);\n color: var(--dsw-alias-label-primary-inverted, #ffffff);\n font-family: inherit;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .24);\n touch-action: manipulation;\n user-select: none;\n -webkit-user-select: none;\n -webkit-touch-callout: none;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-fab\"]:active {\n transform: scale(.96);\n }\n\n /* Workspace sheet (switcher and long-press New Session share it). */\n [data-mobile-nav=\"home-sheet-layer\"] {\n position: absolute;\n inset: 0;\n z-index: 7;\n }\n [data-mobile-nav=\"home-sheet-mask\"] {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-3, rgba(0, 0, 0, .45));\n border: none;\n animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);\n }\n /* Docked to the screen edge, matching the composer's permission/model\n sheets (styles/composer.css.ts section 4) rather than floating.\n\n It used to sit at `bottom: calc(var(--mnav-sab) + 8px)` with all four\n corners rounded, which left a 42px strip of bare mask under it at\n sab: 34 (measured). Docking removes the strip instead of dimming it, and\n the safe area moves INSIDE the card as bottom padding \u2014 so the white\n card body runs all the way to the physical screen edge and the last row\n still clears the home indicator. Same reason the corners go top-only:\n a rounded bottom corner on a card flush with the screen edge reads as a\n rendering mistake. */\n [data-mobile-nav=\"home-sheet\"] {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n max-height: 70%;\n overflow-y: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n padding: 6px 6px calc(var(--mnav-sab) + 14px);\n border-radius: 20px 20px 0 0;\n /* layer-2, not bg-base: in dark mode the sheet must lift off a page that\n shares bg-base, or only the shadow separates them. */\n background: var(--dsw-alias-bg-layer-2, #ffffff);\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .28);\n animation: dsh-mobile-nav-sheet-up .22s var(--ds-ease-out, ease-in-out);\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home-sheet-mask\"],\n [data-mobile-nav=\"home-sheet\"] {\n animation: none !important;\n }\n }\n [data-mobile-nav=\"home-sheet-title\"] {\n padding: 10px 12px 6px;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 13px;\n }\n [data-mobile-nav=\"home-sheet-item\"] {\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 48px;\n padding: 0 12px;\n border: none;\n border-radius: 12px;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 16px;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-sheet-item\"][data-selected] {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n font-weight: 600;\n }\n [data-mobile-nav=\"home-sheet-item\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n\n /* --- hero fallback back button ---\n The hero (new blank session) page renders no conversation.session.header,\n so the header-slot back button (S2) does not exist there. This floating\n fallback covers that page and hides itself as soon as the real header\n back mounts. */\n [data-mobile-nav=\"hero-back\"] {\n position: absolute;\n top: calc(var(--mnav-sat) + 8px);\n left: 8px;\n z-index: 6;\n width: 44px;\n height: 44px;\n display: grid;\n place-items: center;\n border: none;\n border-radius: 14px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n pointer-events: auto;\n }\n [data-mobile-nav=\"hero-back\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n [data-mobile-nav=\"frame\"]:has([data-mobile-nav=\"header-back\"]) ~ [data-mobile-nav=\"hero-back\"],\n body:has([data-mobile-nav=\"header-back\"]) [data-mobile-nav=\"hero-back\"] {\n display: none;\n }\n}\n";
2
2
  //# sourceMappingURL=home.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"home.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/home.css.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,QAAQ,4smBAqdpB,CAAA"}
1
+ {"version":3,"file":"home.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/home.css.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,QAAQ,uunBA8dpB,CAAA"}
package/llms.txt CHANGED
@@ -8,7 +8,7 @@
8
8
  `dsh-zen-remote` (host upload route + browser client via `dsh.client`), `dsh-zen-remote-gateway` (`lan-gate.mjs`), `dsh-zen-remote-push` (`dsh-push.mjs`).
9
9
  - Interface half: browser client bundle (`lib/client.js`, registered as `__ModuleLoader__.load({id:"dsh-zen-remote"})`); <768px app shell, 768–1023px drawer, ≥1024px strict no-op. Host route `POST /_dsh/mobile-nav/upload` writes phone-picked files into the session workspace's `.dsh-uploads/`.
10
10
  - Gateway half: standalone child-process Node gateway (`lib/lan-gate-server.cjs`, Node stdlib + one runtime dep `web-push`), listens on `127.0.0.1:3088`, sits behind a user-run TLS-terminating reverse proxy (nginx/Caddy/Lucky). Device identity = pairing token (cookie `lg_device`), not source IP. Reverse-proxies paired devices to DSH on `127.0.0.1:3080`; DSH webserver untouched. HTML injection adds the PWA manifest link, service-worker bootstrap, `viewport-fit=cover`, shell CSS, randomUUID polyfill.
11
- - Push half: listens on `agent/turn-stopping` (configurable) and registers the model-callable `push_notify` tool; both deliver through the gateway's local `/pwa/push/send` (VAPID + aes128gcm).
11
+ - Push half: two legs. Event leg (always on, debounce-exempt) fires when a tool is waiting on approval (`approval/asked` with no `approval/decided` within 1.5s) or `ask_user_question` is unanswered; turn end (`agent/turn-stopping`, configurable) is OPT-IN via `DSH_PUSH_TURN_END` and top-level-only. Also registers the model-callable `push_notify` tool. All deliver through the gateway's local `/pwa/push/send` (VAPID + aes128gcm).
12
12
  - Env: `LAN_GATE_PORT` (3088), `LAN_GATE_HOST` (127.0.0.1), `LAN_GATE_TARGET_PORT` (3080), `LAN_GATE_RATE_LIMIT` (120), `LAN_GATE_TRUSTED_PROXIES`, `LAN_GATE_VAPID_SUBJECT`, `DSH_PUSH_EVENTS`, `DSH_PUSH_DEBOUNCE_MS`, `DSH_PUSH_SUMMARY`, `DSH_PUSH_TOOL`. Same keys (lowerCamelCase) in `~/.dsh/lan-gate.config.json`.
13
13
  - Admin (local-direct-connection only, 403 otherwise): `http://127.0.0.1:3088/lan-gate/admin` — generate pairing codes, rename/set-kind/revoke devices, trigger a push.
14
14
  - Security: 8-char one-time pairing code (10 min TTL), 5-wrong-code lockout (15 min), revocable per-device tokens, 120 req/min per resolved client IP, admin surface rejects any request carrying `X-Forwarded-*`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "把 DeepSeek Harness 变成手机 App:移动端界面重排(两级页面栈、会话列表主屏、composer 重排、手势、附件上传)+ 配对码认证网关 + PWA 安装 + 真 Web Push。Complete mobile plugin for DeepSeek Harness (DSH): app-shell mobile UI, pairing-code gateway for public-internet access behind your own reverse proxy, installable PWA, offline service worker, and real Web Push agent-done notifications.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,7 +34,8 @@
34
34
  "llms.txt",
35
35
  "AGENTS.md",
36
36
  "README.md",
37
- "LICENSE"
37
+ "LICENSE",
38
+ "!docs/push-policy.md"
38
39
  ],
39
40
  "dsh": {
40
41
  "bundle": {
@@ -80,16 +81,16 @@
80
81
  },
81
82
  "peerDependencies": {
82
83
  "@deepseek-ai/cordis": "^4.0.1",
83
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
84
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
85
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
86
- "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.6",
87
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
88
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
89
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
90
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
91
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
92
- "@deepseek-ai/dsh-session-log-export": "^0.1.0-rc.6",
84
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
85
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
86
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
87
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.7",
88
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
89
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.7",
90
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
91
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
92
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
93
+ "@deepseek-ai/dsh-session-log-export": "^0.1.0-rc.7",
93
94
  "react": "^18.2.0",
94
95
  "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
95
96
  },
@@ -105,22 +106,23 @@
105
106
  }
106
107
  },
107
108
  "devDependencies": {
108
- "@deepseek-ai/dsh-api-remotes": "0.1.0-rc.6",
109
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.6",
110
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.6",
111
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.6",
112
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
113
- "@deepseek-ai/dsh-session-stats": "0.1.0-rc.6",
114
- "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
109
+ "@deepseek-ai/dsh-api-remotes": "0.1.0-rc.7",
110
+ "@deepseek-ai/dsh-client-connection": "0.1.0-rc.7",
111
+ "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.7",
112
+ "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
113
+ "@deepseek-ai/dsh-session": "0.1.0-rc.7",
114
+ "@deepseek-ai/dsh-session-stats": "0.1.0-rc.7",
115
+ "@deepseek-ai/dsh-token-meter": "0.1.0-rc.7",
115
116
  "@types/node": "^26.2.0",
116
117
  "@types/react": "~18.3.1",
117
118
  "@types/react-dom": "~18.3.0",
118
119
  "typescript": "^6.0.3",
119
- "@deepseek-ai/dsh-tools": "0.1.0-rc.6"
120
+ "@deepseek-ai/dsh-tools": "0.1.0-rc.7"
120
121
  },
121
122
  "scripts": {
122
123
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
123
124
  "verify": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
124
- "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-attach-upload.mjs && node scripts/check-upload-endpoint.mjs"
125
+ "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-attach-upload.mjs && node scripts/check-upload-endpoint.mjs && node scripts/sync-doc-version.mjs --check",
126
+ "version": "node scripts/sync-doc-version.mjs && git add README.md"
125
127
  }
126
128
  }
package/pwa/inject.js CHANGED
@@ -188,3 +188,4 @@
188
188
  }
189
189
  }
190
190
  })()
191
+
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Rewrite the version strings embedded in README.md to match package.json.
3
+ *
4
+ * Runs from the `version` npm lifecycle script, which fires AFTER the version
5
+ * bump and BEFORE the release commit — so `npm version patch` carries the
6
+ * refreshed README into the same commit instead of leaving the badge stuck on
7
+ * whatever it said at 1.0.0 (it sat there through 1.0.1 and 1.0.2).
8
+ *
9
+ * Every target is required: an unmatched marker exits non-zero rather than
10
+ * quietly succeeding, because a silent no-op is exactly the failure this
11
+ * script exists to prevent. Idempotent — running it when nothing changed is a
12
+ * no-op with exit 0.
13
+ *
14
+ * Also usable standalone: `node scripts/sync-doc-version.mjs [--check]`.
15
+ * `--check` reports drift without writing, for CI.
16
+ */
17
+
18
+ import { readFileSync, writeFileSync } from 'node:fs'
19
+ import { dirname, join } from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
23
+ const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version
24
+ const checkOnly = process.argv.includes('--check')
25
+
26
+ /** Each target names the file, what it looks for, and what it becomes. */
27
+ const targets = [
28
+ {
29
+ file: 'README.md',
30
+ label: 'release badge',
31
+ pattern: /badge\/release-v\d+\.\d+\.\d+-/,
32
+ replace: `badge/release-v${version}-`,
33
+ },
34
+ {
35
+ file: 'README.md',
36
+ label: 'release badge alt text',
37
+ pattern: /alt="v\d+\.\d+\.\d+"/,
38
+ replace: `alt="v${version}"`,
39
+ },
40
+ {
41
+ file: 'README.md',
42
+ label: 'profile dependency example',
43
+ pattern: /"dsh-zen-remote": "\^\d+\.\d+\.\d+"/,
44
+ replace: `"dsh-zen-remote": "^${version}"`,
45
+ },
46
+ ]
47
+
48
+ const edits = new Map()
49
+ const missing = []
50
+ const drifted = []
51
+
52
+ for (const target of targets) {
53
+ const path = join(root, target.file)
54
+ const before = edits.get(target.file) ?? readFileSync(path, 'utf8')
55
+ if (!target.pattern.test(before)) {
56
+ missing.push(`${target.file}: ${target.label} (${target.pattern})`)
57
+ continue
58
+ }
59
+ const after = before.replace(target.pattern, target.replace)
60
+ if (after !== before) drifted.push(`${target.file}: ${target.label} -> ${target.replace}`)
61
+ edits.set(target.file, after)
62
+ }
63
+
64
+ if (missing.length > 0) {
65
+ console.error(`sync-doc-version: marker not found — the docs moved, update this script:\n ${missing.join('\n ')}`)
66
+ process.exit(1)
67
+ }
68
+
69
+ if (checkOnly) {
70
+ if (drifted.length === 0) {
71
+ console.log(`sync-doc-version: docs already at ${version}`)
72
+ process.exit(0)
73
+ }
74
+ console.error(`sync-doc-version: docs are out of date (package.json is ${version}):\n ${drifted.join('\n ')}`)
75
+ process.exit(1)
76
+ }
77
+
78
+ for (const [file, content] of edits) writeFileSync(join(root, file), content)
79
+ console.log(
80
+ drifted.length === 0
81
+ ? `sync-doc-version: docs already at ${version}`
82
+ : `sync-doc-version: synced to ${version}\n ${drifted.join('\n ')}`,
83
+ )
@@ -11,6 +11,10 @@ import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
11
11
  import type { SessionId, SessionSummary, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
12
12
  import { NS } from './locales.ts'
13
13
  import { GO_HOME_EVENT } from './nav-store.ts'
14
+ import { hasLayer, popLayer, pushLayer } from './history-nav.ts'
15
+
16
+ /** The phone shell's one page-stack step: session list -> session. */
17
+ const SESSION_LAYER = 'session'
14
18
  import type { createNavStore } from './nav-store.ts'
15
19
  import type { WorkspaceFilter } from './nav-store.ts'
16
20
  import type { MobileNavKey } from './locales.ts'
@@ -162,8 +166,19 @@ export function MobileHome({
162
166
  // store directly — a handle mounts under exactly one scope, and this one
163
167
  // is already root-scoped here (see nav-store.ts) — so it dispatches
164
168
  // GO_HOME_EVENT instead and this, the store's actual owner, applies it.
169
+ //
170
+ // Both directions go through the history layer (history-nav.ts) so
171
+ // Android's system back gesture lands on the session→list step instead of
172
+ // exiting the PWA. GO_HOME_EVENT rewinds rather than setting the store: the
173
+ // store is only ever moved home by the layer's own close callback, so the
174
+ // history stack and the page stack cannot drift apart. The direct
175
+ // `show('home')` fallback covers a session view that was somehow entered
176
+ // without a layer (a restored session, a host that blocks pushState).
165
177
  useEffect(() => {
166
- const onGoHome = (): void => actions.show('home')
178
+ const onGoHome = (): void => {
179
+ if (hasLayer(SESSION_LAYER)) popLayer(SESSION_LAYER)
180
+ else actions.show('home')
181
+ }
167
182
  window.addEventListener(GO_HOME_EVENT, onGoHome)
168
183
  return () => window.removeEventListener(GO_HOME_EVENT, onGoHome)
169
184
  }, [actions])
@@ -172,6 +187,7 @@ export function MobileHome({
172
187
  start()
173
188
  setSheet(null)
174
189
  actions.show('session')
190
+ pushLayer({ id: SESSION_LAYER, close: () => actions.show('home') })
175
191
  }
176
192
 
177
193
  // Opens the OFFICIAL settings modal (dsh-client-ui-settings-general's