@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portrait-touch adaptation of the OFFICIAL desktop Web GUI (the dsh-LAN
|
|
3
|
+
* approach): while a touch device is in portrait, inject a CSS + gesture
|
|
4
|
+
* layer on top of the running SPA instead of maintaining a second UI. The
|
|
5
|
+
* official layout already auto-collapses the sidebar below 1024px; this
|
|
6
|
+
* layer adds touch-target sizing, 16px inputs (iOS focus zoom), safe-area
|
|
7
|
+
* padding, a floating whale button as the collapsed-sidebar entry (with a
|
|
8
|
+
* verified toggle fallback for cohorts whose layout face mounts inert),
|
|
9
|
+
* swipe gestures, long-press session menus, a bottom-sheet composer picker
|
|
10
|
+
* (model / effort / permission menus), desktop-surface suppression, and
|
|
11
|
+
* composer/header compaction.
|
|
12
|
+
* Landscape, desktop, and wide viewports stay untouched; a manual
|
|
13
|
+
* sessionStorage opt-out (dsh-remote-force-desktop=1) disables the layer.
|
|
14
|
+
*
|
|
15
|
+
* Selector strategy: CSS Modules class names are hash-prefixed with the
|
|
16
|
+
* semantic name as a stable suffix (`pI_x6G_centerCol`), so attribute
|
|
17
|
+
* suffix selectors survive official rebuilds that only change the hash. The
|
|
18
|
+
* semantic names were read off the 0.1.2-alpha.1 official client build and
|
|
19
|
+
* are re-verified on every GUI QA round; rules borrowed from the dsh-LAN
|
|
20
|
+
* reference (MIT, v47-v78) keep their version comments.
|
|
21
|
+
* @module @linxin666/dsh-remote-web-ui/client/mobile-adapt
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The window global the plugin apply() wires the layout service into. */
|
|
25
|
+
export interface RemoteAdaptGlobal {
|
|
26
|
+
evaluate: () => void
|
|
27
|
+
/** Wired by the plugin apply once ctx.layout is live. */
|
|
28
|
+
toggleSidebar: (() => void) | null
|
|
29
|
+
/** Wired by the plugin apply once ctx.layout is live. */
|
|
30
|
+
closeDetails: (() => void) | null
|
|
31
|
+
/** Plugin master switch: false reverts the layer, true re-evaluates. */
|
|
32
|
+
setEnabled(on: boolean): void
|
|
33
|
+
/** Replays a pending closeDetails once the layout face is wired (the first apply ran before the wiring). */
|
|
34
|
+
flushCloseDetails(): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Storage key for the manual desktop opt-out. */
|
|
38
|
+
const FORCE_DESKTOP_KEY = 'dsh-remote-force-desktop'
|
|
39
|
+
/** Storage key for the dragged whale position. */
|
|
40
|
+
const WHALE_POS_KEY = 'dsh-remote-whale-pos'
|
|
41
|
+
/** Injected stylesheet identity. */
|
|
42
|
+
const ADAPT_CSS_ID = 'dsh-remote-web-ui/mobile-adapt.css'
|
|
43
|
+
/** Body class while the adaptation is active. */
|
|
44
|
+
const ACTIVE_CLASS = 'dsh-remote-portrait'
|
|
45
|
+
/** Body class while the collapsed rail is hidden behind the whale. */
|
|
46
|
+
const RAIL_HIDDEN_CLASS = 'dsh-remote-rail-hidden'
|
|
47
|
+
/** Whale button id. */
|
|
48
|
+
const WHALE_ID = 'dshRemoteWhale'
|
|
49
|
+
/** Compact picker: synthesized model button id. */
|
|
50
|
+
const MODEL_BTN_ID = 'dshRemoteModelPick'
|
|
51
|
+
/** Compact picker: synthesized effort button id. */
|
|
52
|
+
const EFFORT_BTN_ID = 'dshRemoteEffortPick'
|
|
53
|
+
/** Body class while the compact picker buttons are wired. */
|
|
54
|
+
const COMPACT_CLASS = 'dsh-remote-compact-picker'
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether the current viewport is a portrait touch device small enough to
|
|
58
|
+
* need the adaptation.
|
|
59
|
+
*/
|
|
60
|
+
function isMobilePortrait(): boolean {
|
|
61
|
+
if (typeof window === 'undefined') return false
|
|
62
|
+
if (typeof window.matchMedia !== 'function') return false
|
|
63
|
+
if (!window.matchMedia('(orientation: portrait)').matches) return false
|
|
64
|
+
if (!window.matchMedia('(pointer: coarse)').matches) return false
|
|
65
|
+
if (window.innerWidth >= 1100) return false
|
|
66
|
+
if (window.sessionStorage.getItem(FORCE_DESKTOP_KEY) === '1') return false
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The injected rules. Grouped by surface; version comments trace rules back
|
|
72
|
+
* to the dsh-LAN reference they were ported from.
|
|
73
|
+
*/
|
|
74
|
+
const ADAPT_CSS: readonly string[] = [
|
|
75
|
+
'html,body{height:100%}',
|
|
76
|
+
// The app frame fills the dynamic viewport (browser chrome collapse).
|
|
77
|
+
'[class$="_frame"]{width:100%;height:100dvh}',
|
|
78
|
+
// Collapsed rail: bigger touch targets.
|
|
79
|
+
'[class$="_railFish"] button,[class$="_panelIcon"],[class$="_newSession"]{min-width:44px;min-height:44px}',
|
|
80
|
+
// Message list padding on narrow screens.
|
|
81
|
+
'[class$="_scroll"]{padding:8px 10px}',
|
|
82
|
+
// 16px inputs prevent iOS focus zoom; keep the send button touchy.
|
|
83
|
+
'[class$="_input"],textarea,input{font-size:16px}',
|
|
84
|
+
'[class$="_composer"]{padding-bottom:calc(4px + env(safe-area-inset-bottom))}',
|
|
85
|
+
// v50: slightly smaller type on portrait phones.
|
|
86
|
+
'[class$="_scrollBody"] [class$="_root"]{font-size:14.5px}',
|
|
87
|
+
'[class$="_scrollBody"] [class$="_bubble"]{font-size:14.5px}',
|
|
88
|
+
'[class$="_titleRow"] *{font-size:13px}',
|
|
89
|
+
'[class$="_sidebarCol"] [class$="_root"],[class$="_sidebarCol"] [class$="_newSession"],[class$="_sidebarCol"] [class$="_trigger"],[class$="_sidebarCol"] [class$="_title"]{font-size:13px}',
|
|
90
|
+
'[class$="_sidebarCol"] [class$="_meta"],[class$="_sidebarCol"] [class$="_time"]{font-size:11.5px}',
|
|
91
|
+
// v50: collapsed rail hidden; the whale button is the entry. The frame's
|
|
92
|
+
// grid is inline-styled (56px rail track when collapsed); pin the first
|
|
93
|
+
// track to 0 so content uses the full width. On portrait phones the
|
|
94
|
+
// details track is always 0 (computeColumns drops it when there is no
|
|
95
|
+
// room), so the three-track override is safe here.
|
|
96
|
+
'[class$="_frame"][data-sidebar-collapsed]{grid-template-columns:0 minmax(0,1fr) 0 !important}',
|
|
97
|
+
// The official collapsed sidebar is an absolutely-positioned overlay rail
|
|
98
|
+
// (it leaves the grid flow), so the in-flow centerCol would auto-place
|
|
99
|
+
// into the now-0px FIRST track and collapse the conversation to zero width
|
|
100
|
+
// on an intermittent basis (the rail flips relative <-> absolute as the
|
|
101
|
+
// host overlay mounts). Pin every column to an explicit track so the
|
|
102
|
+
// center always lands on the 1fr track regardless of the sidebar's
|
|
103
|
+
// computed position.
|
|
104
|
+
'[class$="_frame"][data-sidebar-collapsed] [class$="_sidebarCol"]{grid-column:1/2}',
|
|
105
|
+
'[class$="_frame"][data-sidebar-collapsed] [class$="_centerCol"]{grid-column:2/3}',
|
|
106
|
+
'[class$="_frame"][data-sidebar-collapsed] [class$="_detailsCol"]{grid-column:3/4}',
|
|
107
|
+
// Keep the chat header title clear of the floating whale.
|
|
108
|
+
`body.${RAIL_HIDDEN_CLASS} [class$="_titleRow"]{padding-left:52px}`,
|
|
109
|
+
// The whale button itself.
|
|
110
|
+
`#${WHALE_ID}{position:fixed;top:calc(4px + env(safe-area-inset-top));left:calc(8px + env(safe-area-inset-left));z-index:2147482999;width:34px;height:34px;min-width:34px;padding:0;border-radius:10px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 6px rgba(0,0,0,.25)}`,
|
|
111
|
+
`#${WHALE_ID} svg{width:20px;height:15px;display:block}`,
|
|
112
|
+
`#${WHALE_ID}:active{opacity:.72}`,
|
|
113
|
+
`#${WHALE_ID}{touch-action:none}`,
|
|
114
|
+
// v51/v52: smaller collapse toggle in the expanded sidebar (the toggle
|
|
115
|
+
// carries two classes, so match by containment); v47's panelIcon touch
|
|
116
|
+
// rule forces a 44px min on the icon itself — zero it out so the 13px
|
|
117
|
+
// glyph fits the 18px button.
|
|
118
|
+
'[class$="_sidebarCol"] [class$="_logoRow"] [class*="_iconButton"]{width:18px;height:18px}',
|
|
119
|
+
'[class$="_sidebarCol"] [class$="_logoRow"] [class*="_iconButton"] svg{width:13px;height:13px;min-width:0;min-height:0}',
|
|
120
|
+
// v77: workspace rows' right-side actions (menu + new session) are
|
|
121
|
+
// hover-only on desktop; always show them on mobile touch.
|
|
122
|
+
'[class$="_sidebarCol"] [class$="_projectRow"] [class$="_rowActions"]{display:inline-flex}',
|
|
123
|
+
// v78: mobile long-press on a row must not start native drag.
|
|
124
|
+
'[class$="_sidebarCol"] [class$="_sessionRow"],[class$="_sidebarCol"] [class$="_projectRow"]{-webkit-user-drag:none;user-select:none}',
|
|
125
|
+
// Hide the header Session-log download button (no space on phones).
|
|
126
|
+
'[class$="_headerUtilities"]{display:none}',
|
|
127
|
+
// v52 composer: the two lines (permission / model) stay stacked with zero
|
|
128
|
+
// row gap; the command, context-meter and send/stop buttons float at the
|
|
129
|
+
// left/right edges vertically centered over both lines.
|
|
130
|
+
'[class$="_composerSeat"] [class$="_card"]{margin-bottom:1px}',
|
|
131
|
+
'[class$="_composerSeat"] [class$="_row"]{flex-wrap:wrap;row-gap:0;padding:2px 8px 1px;position:relative}',
|
|
132
|
+
'[class$="_composerSeat"] [class$="_add"]{position:absolute;left:8px;top:50%;transform:translateY(-50%)}',
|
|
133
|
+
'[class$="_composerSeat"] [class$="_modes"]{min-width:0;padding-left:38px}',
|
|
134
|
+
// Model line left-aligned with the permission line (same command-button
|
|
135
|
+
// clearance), rows stay tightly stacked.
|
|
136
|
+
'[class$="_composerSeat"] [class$="_trailing"]{flex-basis:100%;position:relative;min-height:32px;justify-content:flex-start;padding-left:38px;padding-right:78px}',
|
|
137
|
+
'[class$="_composerSeat"] [class$="_trailing"] *{font-size:12px}',
|
|
138
|
+
// v54: smaller permission/model buttons (font + height). v79: the
|
|
139
|
+
// permission trigger collapses to its shield icon on phones — the label
|
|
140
|
+
// text is the first thing a narrow row drops (the icon color carries the
|
|
141
|
+
// state), matching the dsh-LAN compact composer.
|
|
142
|
+
'[class$="_composerSeat"] [class$="_modes"] [class$="_trigger"]{height:24px;min-height:24px;font-size:12px}',
|
|
143
|
+
'[class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]{height:24px;min-height:24px;font-size:11px}',
|
|
144
|
+
// Context meter + send/stop float at the right edge, vertically centered
|
|
145
|
+
// over both lines (the meter's root contains the track).
|
|
146
|
+
'[class$="_composerSeat"] [class$="_trailing"] > [class$="_root"]:has([class$="_track"]){position:absolute;right:52px;top:50%;transform:translateY(-50%)}',
|
|
147
|
+
'[class$="_composerSeat"] [class$="_primary"]{position:absolute;right:8px;top:50%;transform:translateY(-50%)}',
|
|
148
|
+
// v79: the model picker menu anchors right:0 to its narrow trigger, so on
|
|
149
|
+
// a phone both the picker menu and the model list fly past the left
|
|
150
|
+
// viewport edge (model names unreadably cut). Turn the picker into a
|
|
151
|
+
// bottom sheet: the composer seat carries an identity transform, which
|
|
152
|
+
// would still become the containing block for fixed children, so free it
|
|
153
|
+
// first; then pin every seat menu to the viewport bottom with real touch
|
|
154
|
+
// targets. Covers the picker, the model list, and the other composer
|
|
155
|
+
// popovers (permission presets, attachments) alike.
|
|
156
|
+
'[class$="_composerSeat"]{transform:none !important}',
|
|
157
|
+
'[class$="_composerSeat"] [class$="_menu"]{position:fixed !important;left:8px !important;right:8px !important;top:auto !important;bottom:calc(8px + env(safe-area-inset-bottom)) !important;width:auto !important;max-width:none !important;max-height:70dvh !important;overflow-y:auto !important;z-index:2147482000}',
|
|
158
|
+
'[class$="_composerSeat"] [class$="_menu"] [class$="_cell"]{height:44px;min-height:44px;font-size:13px}',
|
|
159
|
+
// v79 (compact picker): when the row is too narrow for the desktop text
|
|
160
|
+
// triggers, the phone falls back to icon entries — the context ring stays
|
|
161
|
+
// official, and two synthesized buttons open the picker sheet straight on
|
|
162
|
+
// the model list and the effort list (drill-through, so one tap lands on
|
|
163
|
+
// the same list the user's mock shows). The original text trigger hides
|
|
164
|
+
// only while the wired buttons exist (body class): a failed wiring
|
|
165
|
+
// degrades back to the usable text trigger instead of no picker.
|
|
166
|
+
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"]){display:none}`,
|
|
167
|
+
// The icon buttons sit inline in the tools row (parallel to the
|
|
168
|
+
// permission trigger), so the trailing line collapses to zero and the
|
|
169
|
+
// context ring + send re-anchor to the row itself. The ring shifts a
|
|
170
|
+
// few px right: at its desktop offset its hit box kisses the effort
|
|
171
|
+
// button.
|
|
172
|
+
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"]{flex-basis:auto;position:static;min-height:0;padding:0;width:0}`,
|
|
173
|
+
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] > [class$="_root"]:has([class$="_track"]){right:44px}`,
|
|
174
|
+
`#${MODEL_BTN_ID},#${EFFORT_BTN_ID}{width:26px;height:32px;min-width:26px;padding:0;border-radius:9px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none;margin-left:4px}`,
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
`#${MODEL_BTN_ID} svg,#${EFFORT_BTN_ID} svg{width:16px;height:16px;display:block}`,
|
|
178
|
+
`#${MODEL_BTN_ID}:active,#${EFFORT_BTN_ID}:active{opacity:.7}`,
|
|
179
|
+
// Bottom stats line (N rounds / M steps) two sizes below the tabs; v55
|
|
180
|
+
// wraps freely but clamps at two lines (the official rule is nowrap +
|
|
181
|
+
// single-line ellipsis); v60 drops the official 32px right padding.
|
|
182
|
+
'[class$="_composerSeat"] [data-slot="conversation.composer.dock"] [class$="_root"]{font-size:10px;white-space:normal;word-break:break-word;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-align:left;line-height:13px;letter-spacing:-0.2px;padding-left:0;padding-right:0}',
|
|
183
|
+
// (user bubbles keep the v50 14.5px via the _bubble rule.)
|
|
184
|
+
'[class$="_scrollBody"] [class$="_root"]{font-size:13px}',
|
|
185
|
+
// v58/v65 port note: the dsh-LAN `_body` gap:6px compaction is deliberately
|
|
186
|
+
// NOT ported. The official message row is a shrinkable flex column whose
|
|
187
|
+
// bottom-anchored body then measures shorter than its text (13px root font
|
|
188
|
+
// on a fixed 24px line box), and the extra gap pushed the first text line
|
|
189
|
+
// above the row box — every assistant message clipped its top half on the
|
|
190
|
+
// phone (verified on the real GUI; the removal restores natural heights).
|
|
191
|
+
// v58/v65: on touch, taps leave :hover/:focus stuck, so official Tooltip
|
|
192
|
+
// bubbles stay visible on mobile. The bubble class hash sits AFTER the
|
|
193
|
+
// name, so match by containment, still scoped by role=tooltip so user
|
|
194
|
+
// message bubbles (no tooltip role) stay visible.
|
|
195
|
+
'[class*="_bubble"][role="tooltip"]{display:none}',
|
|
196
|
+
// v65: PlanReviewPanel shares the _frame/_card suffixes and overflows a
|
|
197
|
+
// narrow phone; pin it to the viewport with border-box and cap its height
|
|
198
|
+
// so the card sits fully on screen.
|
|
199
|
+
'[class$="_composerSeat"] [class$="_frame"]{box-sizing:border-box;width:100%;max-width:100%;padding-left:12px;padding-right:12px;height:auto;max-height:calc(100dvh - 96px);align-items:flex-start;overflow-y:auto}',
|
|
200
|
+
'[class$="_composerSeat"] [class$="_frame"] [class$="_card"]{max-width:none;width:100%}',
|
|
201
|
+
// v67: header actions (agent-preset mode label + background-task badge)
|
|
202
|
+
// are re-seated from the title row into the tabs row; hidden in the
|
|
203
|
+
// original spot so React re-renders do not flicker them back.
|
|
204
|
+
'[class$="_header"] [class$="_titleCluster"] [class$="_headerActions"]{display:none}',
|
|
205
|
+
// The official header keeps a 78px right padding on phones; stretch the
|
|
206
|
+
// tabs row so the seated actions sit flush against the right edge
|
|
207
|
+
// (verified 360/390/480 on the reference).
|
|
208
|
+
'[class$="_header"] [class$="_tabs"]{margin-right:-58px}',
|
|
209
|
+
// v70: match the tab text size with the seated mode label (12px); NB
|
|
210
|
+
// [class$="_tab"] misses the active tab (its class ends in "_tabActive")
|
|
211
|
+
// — containment so BOTH tabs match.
|
|
212
|
+
'[class$="_header"] [class$="_tabs"] [class*="_tab"]{font-size:12px;white-space:nowrap}',
|
|
213
|
+
// Text-bottom alignment is dynamic (alignActionsText below); flex seat.
|
|
214
|
+
'[class$="_header"] [class$="_tabs"] [class$="_headerActions"]{margin-left:auto;display:flex;align-items:center;gap:6px;flex:none}',
|
|
215
|
+
// Mobile scope: hide the plugin surfaces that do not fit a phone — the
|
|
216
|
+
// right-hand details column and every desktop-oriented tool surface. The
|
|
217
|
+
// list keys on the L2 semantic roots (data-dsh-plugin, ownership stays
|
|
218
|
+
// with the declaring plugin), so official class churn cannot resurrect
|
|
219
|
+
// them. These are render suppressions: the client bundles still load.
|
|
220
|
+
// The pet is part of the hidden set by design: the mobile remote mirror
|
|
221
|
+
// carries the desktop-less phone surface, and a floating pet would only
|
|
222
|
+
// cover the small viewport. The suppression is the requirement, not a bug.
|
|
223
|
+
`body.${ACTIVE_CLASS} [class$=\"_detailsCol\"]{display:none !important}`,
|
|
224
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"ssh\"],`,
|
|
225
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"skill-explorer\"],`,
|
|
226
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"task-board\"],`,
|
|
227
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"git-graph\"],`,
|
|
228
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"pet\"],`,
|
|
229
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"perf\"],`,
|
|
230
|
+
`body.${ACTIVE_CLASS} [data-dsh-plugin=\"usage\"]{display:none !important}`,
|
|
231
|
+
// The official workbench (Files / source control) mounts into a
|
|
232
|
+
// full-viewport portal layer with no data-dsh-plugin root, so the plugin
|
|
233
|
+
// list above cannot key on it; its open state persists across page loads,
|
|
234
|
+
// and on a phone it covers the entire conversation with no visible close
|
|
235
|
+
// control — the remote-opens-to-an-empty-screen report. Hide the
|
|
236
|
+
// workbench panel only: the same portal layer also hosts the settings
|
|
237
|
+
// modal, which must stay reachable (verified in the GUI QA round).
|
|
238
|
+
`body.${ACTIVE_CLASS} [class$=\"_overlayLayer\"] [class$=\"_workbench\"]{display:none !important}`,
|
|
239
|
+
// v68: settings modal on mobile — the official panel is a fixed 800px
|
|
240
|
+
// two-column layout (nav + content); switch to a column layout: the
|
|
241
|
+
// section nav becomes a horizontal scrollable row on top.
|
|
242
|
+
'[class$="_overlay"] [class$="_panel"]{flex-direction:column;max-height:calc(100dvh - 32px)}',
|
|
243
|
+
'[class$="_overlay"] [class$="_panel"] [class$="_nav"]{flex-direction:row;gap:4px;width:100%;padding:12px 12px 0;overflow-x:auto;overflow-y:hidden}',
|
|
244
|
+
'[class$="_overlay"] [class$="_panel"] [class$="_navTitle"]{display:none}',
|
|
245
|
+
'[class$="_overlay"] [class$="_panel"] [class$="_navList"]{flex-direction:row;gap:4px}',
|
|
246
|
+
'[class$="_overlay"] [class$="_panel"] [class$="_navCell"]{height:34px;padding:0 12px;gap:6px;flex:none;border-radius:10px}',
|
|
247
|
+
'[class$="_overlay"] [class$="_panel"] [class$="_navLabel"]{font-size:13px}',
|
|
248
|
+
'[class$="_overlay"] [class$="_panel"] [class$="_content"]{flex:1;min-height:0}',
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
/** Cube glyph for the compact model button (a plain box outline). */
|
|
252
|
+
const CUBE_ICON = '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>'
|
|
253
|
+
/** Level glyph for the compact effort button (three rising bars). */
|
|
254
|
+
const LEVELS_ICON = '<path d="M4 6h16"/><path d="M7 12h10"/><path d="M10 18h4"/>'
|
|
255
|
+
|
|
256
|
+
/** The DeepSeek fish glyph (the official brand mark path). */
|
|
257
|
+
const FISH_PATH = 'M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018Z'
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Install the adaptation layer. Runs once per page (idempotent); the
|
|
261
|
+
* evaluate loop re-applies on orientation/resize and reverts off-portrait.
|
|
262
|
+
*/
|
|
263
|
+
export function startMobileAdapt(): void {
|
|
264
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return
|
|
265
|
+
if ((window as unknown as { __dshRemoteAdaptInstalled?: boolean }).__dshRemoteAdaptInstalled === true) return
|
|
266
|
+
;(window as unknown as { __dshRemoteAdaptInstalled?: boolean }).__dshRemoteAdaptInstalled = true
|
|
267
|
+
|
|
268
|
+
const w = window as unknown as { __dshRemoteAdapt?: RemoteAdaptGlobal } & Record<string, unknown>
|
|
269
|
+
let active = false
|
|
270
|
+
let savedViewportContent: string | null = null
|
|
271
|
+
let whaleEl: HTMLButtonElement | null = null
|
|
272
|
+
let whaleObserver: MutationObserver | null = null
|
|
273
|
+
let whaleTimer: number | null = null
|
|
274
|
+
let whaleSuppressClick = false
|
|
275
|
+
let whaleShown = false
|
|
276
|
+
let drag: { x: number; y: number; left: number; top: number; moved: boolean } | null = null
|
|
277
|
+
let swipeTouch: { x: number; y: number; id: number } | null = null
|
|
278
|
+
let lastComposerTap = 0
|
|
279
|
+
// Plugin master switch: the module-scope layer installs before any config
|
|
280
|
+
// is readable, so the plugin apply() flips this through setEnabled() once
|
|
281
|
+
// the settings snapshot settles (disabled plugin = no injected surface).
|
|
282
|
+
let adaptEnabled = true
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Idempotently (re-)install the adaptation stylesheet. The rules live in
|
|
286
|
+
* one <style> tag keyed by data-plugin-css; the sync tick re-runs this so
|
|
287
|
+
* a tag lost to any external DOM cleanup is restored within one tick
|
|
288
|
+
* instead of silently dropping the suppressions (portrait pet hiding,
|
|
289
|
+
* rail compaction) while the body class stays.
|
|
290
|
+
*/
|
|
291
|
+
function ensureAdaptStyle(): void {
|
|
292
|
+
if (document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return
|
|
293
|
+
const tag = document.createElement('style')
|
|
294
|
+
tag.dataset.plugin = 'remote-web-ui'
|
|
295
|
+
tag.dataset.pluginCss = ADAPT_CSS_ID
|
|
296
|
+
tag.textContent = ADAPT_CSS.join('')
|
|
297
|
+
document.head.appendChild(tag)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function apply(): void {
|
|
301
|
+
if (active) return
|
|
302
|
+
active = true
|
|
303
|
+
document.body.classList.add(ACTIVE_CLASS)
|
|
304
|
+
// A details panel opened before the viewport rotated into portrait (or
|
|
305
|
+
// restored across reloads) would sit behind the display:none above;
|
|
306
|
+
// closing it through the official face unmounts the surface entirely.
|
|
307
|
+
// The layout face throws by design when the root entry has not mounted
|
|
308
|
+
// yet (boot-order), which is a tolerated no-op here — the plugin apply
|
|
309
|
+
// replays it through flushCloseDetails once the wiring is live.
|
|
310
|
+
try {
|
|
311
|
+
w.__dshRemoteAdapt?.closeDetails?.()
|
|
312
|
+
} catch {}
|
|
313
|
+
ensureAdaptStyle()
|
|
314
|
+
// viewport-fit=cover enables env(safe-area-inset-*); restore on revert.
|
|
315
|
+
const meta = document.querySelector('meta[name=viewport]')
|
|
316
|
+
if (meta instanceof HTMLMetaElement && meta.getAttribute('content') !== null && !(meta.getAttribute('content') ?? '').includes('viewport-fit')) {
|
|
317
|
+
savedViewportContent = meta.getAttribute('content')
|
|
318
|
+
meta.setAttribute('content', `${savedViewportContent}, viewport-fit=cover`)
|
|
319
|
+
}
|
|
320
|
+
ensureWhale()
|
|
321
|
+
syncWhale()
|
|
322
|
+
// React builds nodes with attributes before inserting them, so
|
|
323
|
+
// attribute observers miss the initial collapsed state — a light
|
|
324
|
+
// interval is the reliable sync.
|
|
325
|
+
setWhaleTimer(true)
|
|
326
|
+
seatHeaderActions()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function revert(): void {
|
|
330
|
+
if (!active) return
|
|
331
|
+
active = false
|
|
332
|
+
unseatHeaderActions()
|
|
333
|
+
removeCompactPicker()
|
|
334
|
+
document.body.classList.remove(ACTIVE_CLASS)
|
|
335
|
+
document.body.classList.remove(RAIL_HIDDEN_CLASS)
|
|
336
|
+
const tag = document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`)
|
|
337
|
+
tag?.remove()
|
|
338
|
+
const meta = document.querySelector('meta[name=viewport]')
|
|
339
|
+
if (meta instanceof HTMLMetaElement && savedViewportContent !== null) {
|
|
340
|
+
meta.setAttribute('content', savedViewportContent)
|
|
341
|
+
savedViewportContent = null
|
|
342
|
+
}
|
|
343
|
+
if (whaleEl !== null) whaleEl.style.display = 'none'
|
|
344
|
+
setWhaleTimer(false)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
|
|
348
|
+
function setWhaleTimer(on: boolean): void {
|
|
349
|
+
if (on && whaleTimer === null && active) {
|
|
350
|
+
whaleTimer = window.setInterval(syncWhale, 600)
|
|
351
|
+
} else if (!on && whaleTimer !== null) {
|
|
352
|
+
window.clearInterval(whaleTimer)
|
|
353
|
+
whaleTimer = null
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Compact picker (v79): a phone row cannot fit the desktop text triggers,
|
|
359
|
+
* so the model/effort entries become two icon buttons in the trailing
|
|
360
|
+
* row. Both forward to the official picker trigger (its menu renders as
|
|
361
|
+
* the bottom sheet) and then drill straight into the asked cell — model
|
|
362
|
+
* list or effort list — so one tap lands on the list, matching the
|
|
363
|
+
* cube-model / brain-effort mapping. The official context ring next to
|
|
364
|
+
* the send button keeps its own semantics untouched.
|
|
365
|
+
*/
|
|
366
|
+
function removeCompactPicker(): void {
|
|
367
|
+
document.body.classList.remove(COMPACT_CLASS)
|
|
368
|
+
document.getElementById(MODEL_BTN_ID)?.remove()
|
|
369
|
+
document.getElementById(EFFORT_BTN_ID)?.remove()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function drillIntoPicker(cellPattern: RegExp): void {
|
|
373
|
+
const trigger = document.querySelector('[class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"])') as HTMLElement | null
|
|
374
|
+
if (trigger === null) return
|
|
375
|
+
trigger.click()
|
|
376
|
+
// The sheet mount takes a beat (observed ~0.2-0.6s on a cold phone
|
|
377
|
+
// mirror); poll until the asked cell exists instead of a fixed delay.
|
|
378
|
+
let tries = 0
|
|
379
|
+
const tapCell = (): void => {
|
|
380
|
+
tries += 1
|
|
381
|
+
const cell = Array.from(document.querySelectorAll('[class$="_composerSeat"] [class$="_menu"] [class$="_cell"]'))
|
|
382
|
+
.find((c) => cellPattern.test(c.textContent ?? ''))
|
|
383
|
+
if (cell !== undefined) {
|
|
384
|
+
;(cell as HTMLElement).click()
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
if (tries < 8) window.setTimeout(tapCell, 150)
|
|
388
|
+
}
|
|
389
|
+
window.setTimeout(tapCell, 150)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function makeCompactButton(id: string, title: string, icon: string, cellPattern: RegExp): HTMLButtonElement {
|
|
393
|
+
const btn = document.createElement('button')
|
|
394
|
+
btn.id = id
|
|
395
|
+
btn.type = 'button'
|
|
396
|
+
btn.dataset.dshPlugin = 'remote-web-ui'
|
|
397
|
+
btn.title = title
|
|
398
|
+
btn.setAttribute('aria-label', title)
|
|
399
|
+
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`
|
|
400
|
+
btn.addEventListener('click', () => { drillIntoPicker(cellPattern) })
|
|
401
|
+
return btn
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function syncCompactPicker(): void {
|
|
405
|
+
if (!active) return
|
|
406
|
+
const tools = document.querySelector('[class$="_composerSeat"] [class$="_tools"]')
|
|
407
|
+
const trigger = tools?.parentElement?.querySelector('[class$="_triggerEffort"]')?.parentElement
|
|
408
|
+
if (tools === null || trigger === null) {
|
|
409
|
+
removeCompactPicker()
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
const zh = (navigator.language ?? '').toLowerCase().startsWith('zh')
|
|
413
|
+
if (document.getElementById(MODEL_BTN_ID) === null) {
|
|
414
|
+
tools.appendChild(makeCompactButton(MODEL_BTN_ID, zh ? '选择模型' : 'Pick model', CUBE_ICON, /模型|Model/))
|
|
415
|
+
}
|
|
416
|
+
if (document.getElementById(EFFORT_BTN_ID) === null) {
|
|
417
|
+
tools.appendChild(makeCompactButton(EFFORT_BTN_ID, zh ? '选择推理等级' : 'Pick reasoning effort', LEVELS_ICON, /推理等级|Reasoning|Effort/i))
|
|
418
|
+
}
|
|
419
|
+
document.body.classList.add(COMPACT_CLASS)
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Toggle the sidebar through the wired layout face, then verify the flip:
|
|
423
|
+
* the official LayoutController can be mounted yet inert (its bound store
|
|
424
|
+
* actions race the root entry on this cohort — observed on the running
|
|
425
|
+
* local build, where the face call silently did nothing and the whale
|
|
426
|
+
* was dead). When the frame state did not change shortly after the call,
|
|
427
|
+
* drive the official rail/logo toggle instead: that button owns its own
|
|
428
|
+
* store actions and flips the same state on every cohort we support.
|
|
429
|
+
*/
|
|
430
|
+
function toggleSidebarVerified(): void {
|
|
431
|
+
const frame = document.querySelector('[class$="_frame"]')
|
|
432
|
+
const collapsedBefore = frame instanceof HTMLElement ? frame.hasAttribute('data-sidebar-collapsed') : null
|
|
433
|
+
w.__dshRemoteAdapt?.toggleSidebar?.()
|
|
434
|
+
if (collapsedBefore === null) return
|
|
435
|
+
window.setTimeout(() => {
|
|
436
|
+
if (!active) return
|
|
437
|
+
const frameNow = document.querySelector('[class$="_frame"]')
|
|
438
|
+
if (!(frameNow instanceof HTMLElement)) return
|
|
439
|
+
if (frameNow.hasAttribute('data-sidebar-collapsed') !== collapsedBefore) return
|
|
440
|
+
;(document.querySelector('[class$="_railFish"] button, [class$="_logoRow"] [class*="_iconButton"]') as HTMLElement | null)?.click()
|
|
441
|
+
}, 150)
|
|
442
|
+
}
|
|
443
|
+
function ensureWhale(): void {
|
|
444
|
+
if (whaleEl !== null || !document.body) return
|
|
445
|
+
const whale = document.createElement('button')
|
|
446
|
+
whale.id = WHALE_ID
|
|
447
|
+
whale.type = 'button'
|
|
448
|
+
whale.dataset.dshPlugin = 'remote-web-ui'
|
|
449
|
+
// Outside the slots i18n system (module-scope overlay), so the label
|
|
450
|
+
// picks the browser language directly.
|
|
451
|
+
const whaleLabel = (navigator.language ?? '').toLowerCase().startsWith('zh') ? '打开侧边栏' : 'Open sidebar'
|
|
452
|
+
whale.title = whaleLabel
|
|
453
|
+
whale.setAttribute('aria-label', whaleLabel)
|
|
454
|
+
whale.innerHTML = `<svg viewBox="0 0 23.16 17.04" fill="none" aria-hidden="true"><path d="${FISH_PATH}" fill="currentColor"/></svg>`
|
|
455
|
+
whale.addEventListener('click', () => {
|
|
456
|
+
if (whaleSuppressClick) {
|
|
457
|
+
whaleSuppressClick = false
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
toggleSidebarVerified()
|
|
461
|
+
})
|
|
462
|
+
// v51: the whale floats — pointer-drag repositions it (persisted).
|
|
463
|
+
whale.addEventListener('pointerdown', (e) => {
|
|
464
|
+
if (whaleEl === null) return
|
|
465
|
+
if (e.button !== 0 && e.pointerType === 'mouse') return
|
|
466
|
+
// v61: opening the sidebar must not leave a focused composer input
|
|
467
|
+
// behind — on phones a still-focused input makes the first tap re-pop
|
|
468
|
+
// the keyboard (iOS) or keeps it open. Blur at pointerdown (before the
|
|
469
|
+
// tap completes; click is too late for iOS) and block pending
|
|
470
|
+
// programmatic refocus.
|
|
471
|
+
if (active) {
|
|
472
|
+
const ta = document.querySelector('[class$="_composerSeat"] textarea, [class$="_composerSeat"] input')
|
|
473
|
+
if (ta !== null && document.activeElement === ta) {
|
|
474
|
+
(ta as HTMLElement).blur()
|
|
475
|
+
lastComposerTap = 0
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
drag = { x: e.clientX, y: e.clientY, left: whale.offsetLeft, top: whale.offsetTop, moved: false }
|
|
479
|
+
try { whale.setPointerCapture(e.pointerId) } catch {}
|
|
480
|
+
e.preventDefault()
|
|
481
|
+
e.stopPropagation()
|
|
482
|
+
})
|
|
483
|
+
whale.addEventListener('pointermove', (e) => {
|
|
484
|
+
if (whaleEl === null || drag === null) return
|
|
485
|
+
const dx = e.clientX - drag.x
|
|
486
|
+
const dy = e.clientY - drag.y
|
|
487
|
+
drag.moved = drag.moved || Math.abs(dx) > 3 || Math.abs(dy) > 3
|
|
488
|
+
whaleEl.style.left = `${Math.min(Math.max(4, drag.left + dx), window.innerWidth - 38)}px`
|
|
489
|
+
whaleEl.style.top = `${Math.min(Math.max(4, drag.top + dy), window.innerHeight - 38)}px`
|
|
490
|
+
e.preventDefault()
|
|
491
|
+
e.stopPropagation()
|
|
492
|
+
})
|
|
493
|
+
const endWhaleDrag = (): void => {
|
|
494
|
+
if (whaleEl === null || drag === null) return
|
|
495
|
+
const moved = drag.moved
|
|
496
|
+
drag = null
|
|
497
|
+
whaleSuppressClick = moved
|
|
498
|
+
try {
|
|
499
|
+
window.localStorage.setItem(WHALE_POS_KEY, JSON.stringify({ x: whaleEl.offsetLeft, y: whaleEl.offsetTop }))
|
|
500
|
+
} catch {}
|
|
501
|
+
}
|
|
502
|
+
whale.addEventListener('pointerup', endWhaleDrag)
|
|
503
|
+
whale.addEventListener('pointercancel', endWhaleDrag)
|
|
504
|
+
whaleEl = whale
|
|
505
|
+
document.body.appendChild(whale)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Restore a dragged position when the whale becomes visible again. */
|
|
509
|
+
function applyWhalePos(): void {
|
|
510
|
+
if (whaleEl === null) return
|
|
511
|
+
let pos: { x?: unknown; y?: unknown } | null = null
|
|
512
|
+
try {
|
|
513
|
+
pos = JSON.parse(window.localStorage.getItem(WHALE_POS_KEY) ?? 'null') as { x?: unknown; y?: unknown } | null
|
|
514
|
+
} catch {}
|
|
515
|
+
if (pos !== null && typeof pos.x === 'number' && typeof pos.y === 'number') {
|
|
516
|
+
// Clamp to the current viewport: a position saved on a larger screen
|
|
517
|
+
// (resize, split-screen, rotation) must not park the whale — the only
|
|
518
|
+
// portrait sidebar entry — out of reach.
|
|
519
|
+
const x = Math.min(Math.max(4, pos.x), window.innerWidth - 38)
|
|
520
|
+
const y = Math.min(Math.max(4, pos.y), window.innerHeight - 38)
|
|
521
|
+
whaleEl.style.left = `${x}px`
|
|
522
|
+
whaleEl.style.top = `${y}px`
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// v78: on touch, official session/project rows are draggable for
|
|
527
|
+
// reordering; a long-press would start native drag instead of our action
|
|
528
|
+
// menu. Force them non-draggable while the adapt is active (React may
|
|
529
|
+
// re-create rows, so re-apply on the sync tick).
|
|
530
|
+
function disableRowDrag(): void {
|
|
531
|
+
if (!active) return
|
|
532
|
+
const rows = document.querySelectorAll('[class$="_sidebarCol"] [class$="_sessionRow"], [class$="_sidebarCol"] [class$="_projectRow"]')
|
|
533
|
+
for (const row of rows) {
|
|
534
|
+
if (row.getAttribute('draggable') !== 'false') row.setAttribute('draggable', 'false')
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function syncWhale(): void {
|
|
539
|
+
// Keep the stylesheet present while the layer is active (the tick runs
|
|
540
|
+
// every 600ms; see ensureAdaptStyle for why the tag can need a re-assert).
|
|
541
|
+
if (active) ensureAdaptStyle()
|
|
542
|
+
if (whaleEl === null) return
|
|
543
|
+
if (!active) {
|
|
544
|
+
whaleEl.style.display = 'none'
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
const collapsed = document.querySelector('[class$="_frame"][data-sidebar-collapsed]') !== null
|
|
548
|
+
const overlayUp = document.querySelector('[class$="_overlay"]') !== null
|
|
549
|
+
const show = collapsed && !overlayUp
|
|
550
|
+
// Restore on every hidden-to-shown transition — including the very
|
|
551
|
+
// first show on a fresh page load, where the inline display is still
|
|
552
|
+
// '' and a CSS-read guard would never fire.
|
|
553
|
+
if (show && !whaleShown) applyWhalePos()
|
|
554
|
+
whaleEl.style.display = show ? '' : 'none'
|
|
555
|
+
whaleShown = show
|
|
556
|
+
document.body.classList.toggle(RAIL_HIDDEN_CLASS, collapsed)
|
|
557
|
+
disableRowDrag()
|
|
558
|
+
seatHeaderActions()
|
|
559
|
+
alignActionsText()
|
|
560
|
+
syncCompactPicker()
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// v67: on mobile the header actions (agent-preset mode label + background
|
|
564
|
+
// task badge) move from the title row into the tabs row. React re-creates
|
|
565
|
+
// the node in its original spot on re-render, so the interval re-seats it;
|
|
566
|
+
// stale seated copies are removed first.
|
|
567
|
+
function seatHeaderActions(): void {
|
|
568
|
+
if (!active) return
|
|
569
|
+
const tabs = document.querySelector('[class$="_header"] [class$="_tabs"]')
|
|
570
|
+
const fresh = document.querySelector('[class$="_titleCluster"] [class$="_headerActions"]')
|
|
571
|
+
const seated = tabs !== null ? tabs.querySelector(':scope > [class$="_headerActions"]') : null
|
|
572
|
+
// fresh === null means the node is either not rendered yet or already
|
|
573
|
+
// seated — the seated copy IS the moved node, so never remove it.
|
|
574
|
+
if (fresh !== null && tabs !== null) {
|
|
575
|
+
seated?.remove()
|
|
576
|
+
tabs.appendChild(fresh)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Align the mode/badge text bottom edge with the tab text. The tabs row
|
|
581
|
+
// height is not stable (a background task badge grows it), so a static
|
|
582
|
+
// transform would be wrong half the time — measure the real text boxes
|
|
583
|
+
// each tick and compensate (converges; the previous transform is
|
|
584
|
+
// subtracted from the measurement).
|
|
585
|
+
function alignActionsText(): void {
|
|
586
|
+
if (!active) return
|
|
587
|
+
const tabs = document.querySelector('[class$="_header"] [class$="_tabs"]')
|
|
588
|
+
if (tabs === null) return
|
|
589
|
+
const actions = tabs.querySelector(':scope > [class$="_headerActions"]')
|
|
590
|
+
const tabBtn = tabs.querySelector(':scope > [class*="_tab"]')
|
|
591
|
+
if (actions === null || tabBtn === null) return
|
|
592
|
+
const textBottom = (el: Element | null): number | null => {
|
|
593
|
+
if (el === null) return null
|
|
594
|
+
const text = Array.from(el.childNodes).find((n) => n.nodeType === 3 && n.textContent !== null && n.textContent.trim() !== '')
|
|
595
|
+
if (text === undefined) return null
|
|
596
|
+
try {
|
|
597
|
+
const range = document.createRange()
|
|
598
|
+
range.selectNode(text)
|
|
599
|
+
return range.getBoundingClientRect().bottom
|
|
600
|
+
} catch {
|
|
601
|
+
return null
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const tabBottom = textBottom(tabBtn)
|
|
605
|
+
if (tabBottom === null) return
|
|
606
|
+
const curMatch = /translateY\((-?[\d.]+)px\)/.exec((actions as HTMLElement).style.transform ?? '')
|
|
607
|
+
const cur = curMatch !== null ? parseFloat(curMatch[1] ?? '0') : 0
|
|
608
|
+
let maxBottom: number | null = null
|
|
609
|
+
for (const sel of ['[class$="_label"]', '[class$="_count"]']) {
|
|
610
|
+
const b = textBottom(actions.querySelector(sel))
|
|
611
|
+
if (b !== null && (maxBottom === null || b - cur > maxBottom)) maxBottom = b - cur
|
|
612
|
+
}
|
|
613
|
+
if (maxBottom === null) return
|
|
614
|
+
const delta = Math.round((tabBottom - maxBottom) * 10) / 10
|
|
615
|
+
if (Math.abs(delta) < 0.5) {
|
|
616
|
+
if ((actions as HTMLElement).style.transform !== '') (actions as HTMLElement).style.transform = ''
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
;(actions as HTMLElement).style.transform = `translateY(${delta}px)`
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function unseatHeaderActions(): void {
|
|
623
|
+
const tabs = document.querySelector('[class$="_header"] [class$="_tabs"]')
|
|
624
|
+
const seated = tabs !== null ? tabs.querySelector(':scope > [class$="_headerActions"]') : null
|
|
625
|
+
const wrap = document.querySelector('[class$="_titleCluster"] > div')
|
|
626
|
+
if (seated !== null) {
|
|
627
|
+
if (wrap !== null) wrap.appendChild(seated)
|
|
628
|
+
else seated.remove()
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function ensureWhaleObserver(): void {
|
|
633
|
+
if (whaleObserver !== null || typeof MutationObserver === 'undefined' || !document.body) return
|
|
634
|
+
whaleObserver = new MutationObserver(() => { syncWhale() })
|
|
635
|
+
whaleObserver.observe(document.body, { attributes: true, attributeFilter: ['data-sidebar-collapsed'], subtree: true })
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function evaluate(): void {
|
|
639
|
+
if (!adaptEnabled) {
|
|
640
|
+
revert()
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
if (isMobilePortrait()) {
|
|
644
|
+
apply()
|
|
645
|
+
ensureWhaleObserver()
|
|
646
|
+
} else {
|
|
647
|
+
revert()
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
evaluate()
|
|
651
|
+
window.addEventListener('orientationchange', evaluate)
|
|
652
|
+
window.addEventListener('resize', evaluate)
|
|
653
|
+
// The 600ms tick is a page-lifetime wake source; pause it while the page
|
|
654
|
+
// is hidden (backgrounded phone tab) and resume on return.
|
|
655
|
+
document.addEventListener('visibilitychange', () => { setWhaleTimer(document.visibilityState !== 'hidden') })
|
|
656
|
+
|
|
657
|
+
// v51: Enter only inserts a newline on mobile (send goes through the send
|
|
658
|
+
// button). Captured at document level so the official Enter-to-send
|
|
659
|
+
// handler never sees the event.
|
|
660
|
+
function onKeydownCapture(e: KeyboardEvent): void {
|
|
661
|
+
if (!active) return
|
|
662
|
+
if (e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return
|
|
663
|
+
const t = e.target
|
|
664
|
+
if (!(t instanceof HTMLElement)) return
|
|
665
|
+
if (t.closest('[class$="_input"]') === null) return
|
|
666
|
+
// keyCode 229 is the classic IME-confirm signal (Safari fires
|
|
667
|
+
// compositionend before the Enter keydown) — never treat it as a
|
|
668
|
+
// plain newline.
|
|
669
|
+
if (e.isComposing || e.keyCode === 229) return
|
|
670
|
+
e.preventDefault()
|
|
671
|
+
e.stopPropagation()
|
|
672
|
+
try { document.execCommand('insertText', false, '\n') } catch {}
|
|
673
|
+
}
|
|
674
|
+
document.addEventListener('keydown', onKeydownCapture, true)
|
|
675
|
+
|
|
676
|
+
function collapseSidebar(): void {
|
|
677
|
+
toggleSidebarVerified()
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Auto-collapse the expanded sidebar on mobile: after clicking a session
|
|
681
|
+
// row, or when tapping anywhere outside the sidebar (overlays/dialogs and
|
|
682
|
+
// the whale excluded).
|
|
683
|
+
function onClickCapture(e: MouseEvent): void {
|
|
684
|
+
if (!active) return
|
|
685
|
+
const frame = document.querySelector('[class$="_frame"]')
|
|
686
|
+
if (frame === null || frame.hasAttribute('data-sidebar-collapsed')) return
|
|
687
|
+
const t = e.target
|
|
688
|
+
if (!(t instanceof Element)) return
|
|
689
|
+
if (t.closest('[class$="_sidebarCol"]') !== null) {
|
|
690
|
+
// v68: opening settings also folds the sidebar so the modal closes
|
|
691
|
+
// back into a clean conversation view. Clicks on the session
|
|
692
|
+
// row-actions ellipsis (or its opened menu anchor) must NOT collapse
|
|
693
|
+
// the sidebar — they open the action menu. v76: clicking the top-level
|
|
694
|
+
// New Session also folds. v77: clicking a workspace row's new-session
|
|
695
|
+
// button also folds.
|
|
696
|
+
const topNewSessionClicked = t.closest('[class$="_newSession"], [class$="_brand"]') !== null
|
|
697
|
+
let projectNewSessionClicked = false
|
|
698
|
+
const projectActions = t.closest('[class$="_projectRow"] [class$="_rowActions"]')
|
|
699
|
+
if (projectActions !== null) {
|
|
700
|
+
const btn = t.closest('button')
|
|
701
|
+
const btns = projectActions.querySelectorAll('button')
|
|
702
|
+
projectNewSessionClicked = btn !== null && btns.length > 0 && btn === btns[btns.length - 1]
|
|
703
|
+
}
|
|
704
|
+
let shouldCollapse = false
|
|
705
|
+
if (t.closest('[class$="_sessionRow"]') !== null) {
|
|
706
|
+
shouldCollapse = t.closest('[class$="_rowActions"]') === null
|
|
707
|
+
} else if (t.closest('[class$="_settingsArea"]') !== null || topNewSessionClicked || projectNewSessionClicked) {
|
|
708
|
+
shouldCollapse = true
|
|
709
|
+
}
|
|
710
|
+
if (shouldCollapse) collapseSidebar()
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
if (t.closest('[class$="_overlay"], [class$="_dialog"], [class$="_menu"], [class*="_portal"], [id="dshRemoteWhale"], [id="dshLanGate"]') !== null) return
|
|
714
|
+
collapseSidebar()
|
|
715
|
+
}
|
|
716
|
+
document.addEventListener('click', onClickCapture, true)
|
|
717
|
+
|
|
718
|
+
// v65/v66: swipe gestures. Swiping LEFT while the sidebar is expanded
|
|
719
|
+
// collapses it; swiping RIGHT in the conversation view opens it. Vertical
|
|
720
|
+
// swipes (list scrolling), swipes starting on inputs, on the whale, or
|
|
721
|
+
// inside horizontally scrollable containers are ignored.
|
|
722
|
+
function insideHScrollable(el: Element): boolean {
|
|
723
|
+
let n: Element | null = el
|
|
724
|
+
while (n !== null && n !== document.body) {
|
|
725
|
+
const cs = getComputedStyle(n)
|
|
726
|
+
if ((cs.overflowX === 'auto' || cs.overflowX === 'scroll') && n.scrollWidth > n.clientWidth + 4) return true
|
|
727
|
+
n = n.parentElement
|
|
728
|
+
}
|
|
729
|
+
return false
|
|
730
|
+
}
|
|
731
|
+
document.addEventListener('touchstart', (e) => {
|
|
732
|
+
if (!active) return
|
|
733
|
+
const t = e.target
|
|
734
|
+
if (t instanceof Element && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.closest(`#${WHALE_ID}`) !== null || insideHScrollable(t) || t.closest('table, [class$="_table"], [class$="_tablePane"]') !== null)) {
|
|
735
|
+
swipeTouch = null
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
const ct = e.changedTouches[0]
|
|
739
|
+
if (ct === undefined) return
|
|
740
|
+
swipeTouch = { x: ct.clientX, y: ct.clientY, id: ct.identifier }
|
|
741
|
+
}, { capture: true, passive: true })
|
|
742
|
+
document.addEventListener('touchend', (e) => {
|
|
743
|
+
if (swipeTouch === null) return
|
|
744
|
+
const ct = e.changedTouches[0]
|
|
745
|
+
if (ct === undefined || ct.identifier !== swipeTouch.id) return
|
|
746
|
+
const dx = ct.clientX - swipeTouch.x
|
|
747
|
+
const dy = ct.clientY - swipeTouch.y
|
|
748
|
+
swipeTouch = null
|
|
749
|
+
if (!active) return
|
|
750
|
+
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return
|
|
751
|
+
const frame = document.querySelector('[class$="_frame"]')
|
|
752
|
+
if (frame === null) return
|
|
753
|
+
if (document.querySelector('[class$="_overlay"], [class$="_dialog"], [class$="_menu"], [class*="_portal"]') !== null) return
|
|
754
|
+
const collapsed = frame.hasAttribute('data-sidebar-collapsed')
|
|
755
|
+
if (dx < 0 && !collapsed) collapseSidebar()
|
|
756
|
+
else if (dx > 0 && collapsed) toggleSidebarVerified()
|
|
757
|
+
}, { capture: true, passive: true })
|
|
758
|
+
document.addEventListener('touchcancel', () => {
|
|
759
|
+
swipeTouch = null
|
|
760
|
+
}, true)
|
|
761
|
+
|
|
762
|
+
// Long-press a session row opens the same action menu as the desktop's
|
|
763
|
+
// right-side ellipsis button (rename / fork / archive). On touch the
|
|
764
|
+
// row-actions button is hover-only, so a long press reveals + clicks it.
|
|
765
|
+
const LONG_PRESS_MS = 500
|
|
766
|
+
const LONG_PRESS_MOVE = 10
|
|
767
|
+
let longPress: { row: Element; x: number; y: number; timer: number | null; triggered: boolean } | null = null
|
|
768
|
+
let suppressSessionClickUntil = 0
|
|
769
|
+
let suppressSessionClickRow: Element | null = null
|
|
770
|
+
let longPressMenuGuardUntil = 0
|
|
771
|
+
|
|
772
|
+
function clearLongPress(): void {
|
|
773
|
+
if (longPress !== null) {
|
|
774
|
+
if (longPress.timer !== null) window.clearTimeout(longPress.timer)
|
|
775
|
+
longPress = null
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function openSessionMenu(row: Element): void {
|
|
780
|
+
const actions = row.querySelector('[class$="_rowActions"]')
|
|
781
|
+
const btn = actions?.querySelector('button')
|
|
782
|
+
if (actions === null || btn === null) return
|
|
783
|
+
// The official row hides actions until hover/menuOpen; force the anchor
|
|
784
|
+
// visible just long enough for .click() to be accepted.
|
|
785
|
+
longPressMenuGuardUntil = Date.now() + 1200
|
|
786
|
+
;(actions as HTMLElement).style.display = 'inline-flex'
|
|
787
|
+
try {
|
|
788
|
+
(btn as HTMLElement).click()
|
|
789
|
+
} finally {
|
|
790
|
+
window.setTimeout(() => { (actions as HTMLElement).style.display = '' }, 50)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
document.addEventListener('touchstart', (e) => {
|
|
794
|
+
if (!active) return
|
|
795
|
+
const t = e.target
|
|
796
|
+
if (!(t instanceof Element)) {
|
|
797
|
+
clearLongPress()
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
const row = t.closest('[class$="_sessionRow"]')
|
|
801
|
+
if (row === null || t.closest('[class$="_rowActions"]') !== null) {
|
|
802
|
+
clearLongPress()
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
const ct = e.changedTouches[0]
|
|
806
|
+
if (ct === undefined) return
|
|
807
|
+
clearLongPress()
|
|
808
|
+
const lp = { row, x: ct.clientX, y: ct.clientY, timer: null as number | null, triggered: false }
|
|
809
|
+
lp.timer = window.setTimeout(() => {
|
|
810
|
+
if (longPress !== lp) return
|
|
811
|
+
lp.triggered = true
|
|
812
|
+
openSessionMenu(lp.row)
|
|
813
|
+
try { navigator.vibrate?.(10) } catch {}
|
|
814
|
+
}, LONG_PRESS_MS)
|
|
815
|
+
longPress = lp
|
|
816
|
+
}, { capture: true, passive: true })
|
|
817
|
+
document.addEventListener('touchmove', (e) => {
|
|
818
|
+
if (!active || longPress === null) return
|
|
819
|
+
const ct = e.changedTouches[0]
|
|
820
|
+
if (ct === undefined) return
|
|
821
|
+
if (Math.abs(ct.clientX - longPress.x) > LONG_PRESS_MOVE || Math.abs(ct.clientY - longPress.y) > LONG_PRESS_MOVE) {
|
|
822
|
+
clearLongPress()
|
|
823
|
+
}
|
|
824
|
+
}, { capture: true, passive: true })
|
|
825
|
+
document.addEventListener('touchend', (e) => {
|
|
826
|
+
if (!active || longPress === null) return
|
|
827
|
+
const ct = e.changedTouches[0]
|
|
828
|
+
if (ct === undefined) return
|
|
829
|
+
const lp = longPress
|
|
830
|
+
clearLongPress()
|
|
831
|
+
if (lp.triggered) {
|
|
832
|
+
// Stop the finger-lift from also opening the session; the menu is
|
|
833
|
+
// already open and the sidebar should stay put.
|
|
834
|
+
e.preventDefault()
|
|
835
|
+
suppressSessionClickUntil = Date.now() + 800
|
|
836
|
+
suppressSessionClickRow = lp.row
|
|
837
|
+
}
|
|
838
|
+
}, { capture: true, passive: false })
|
|
839
|
+
document.addEventListener('touchcancel', () => {
|
|
840
|
+
clearLongPress()
|
|
841
|
+
}, true)
|
|
842
|
+
// A long press ends with a synthetic click; swallow that one click so it
|
|
843
|
+
// neither opens the conversation nor collapses the sidebar.
|
|
844
|
+
window.addEventListener('click', (e) => {
|
|
845
|
+
if (!active || Date.now() > suppressSessionClickUntil) return
|
|
846
|
+
const t = e.target
|
|
847
|
+
if (t instanceof Element && suppressSessionClickRow !== null && (t === suppressSessionClickRow || suppressSessionClickRow.contains(t))) {
|
|
848
|
+
e.stopPropagation()
|
|
849
|
+
e.preventDefault()
|
|
850
|
+
suppressSessionClickUntil = 0
|
|
851
|
+
suppressSessionClickRow = null
|
|
852
|
+
}
|
|
853
|
+
}, true)
|
|
854
|
+
// Some Android browsers fire a native context menu after a long press.
|
|
855
|
+
document.addEventListener('contextmenu', (e) => {
|
|
856
|
+
if (!active) return
|
|
857
|
+
const t = e.target
|
|
858
|
+
if (!(t instanceof Element)) return
|
|
859
|
+
if (t.closest('[class$="_sessionRow"]') !== null && ((longPress !== null && longPress.triggered) || Date.now() <= suppressSessionClickUntil)) {
|
|
860
|
+
e.preventDefault()
|
|
861
|
+
}
|
|
862
|
+
}, true)
|
|
863
|
+
// Keep the long-press menu open across the finger-lift: the official
|
|
864
|
+
// Menu closes on pointerleave of its root, which fires on touch end.
|
|
865
|
+
document.addEventListener('pointerleave', (e) => {
|
|
866
|
+
if (!active || Date.now() > longPressMenuGuardUntil) return
|
|
867
|
+
const t = e.target
|
|
868
|
+
if (t instanceof Element && (t.closest('[class$="_rowActions"]') !== null || t.closest('[class$="_sessionRow"]') !== null || t.closest('[class$="_projectRow"]') !== null)) {
|
|
869
|
+
e.stopPropagation()
|
|
870
|
+
}
|
|
871
|
+
}, true)
|
|
872
|
+
|
|
873
|
+
// Intercept programmatic .focus() on the composer input: the official
|
|
874
|
+
// conversation component focuses it on mount and on session changes;
|
|
875
|
+
// user taps focus it through the browser's own pipeline. Only allow
|
|
876
|
+
// composer focus that follows a real tap on the input itself.
|
|
877
|
+
const lanOrigFocus = HTMLElement.prototype.focus
|
|
878
|
+
HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions): void {
|
|
879
|
+
if (active && (this.tagName === 'TEXTAREA' || this.tagName === 'INPUT') && this.closest('[class$="_composerSeat"]') !== null && Date.now() - lastComposerTap >= 800) {
|
|
880
|
+
return
|
|
881
|
+
}
|
|
882
|
+
lanOrigFocus.call(this, options)
|
|
883
|
+
}
|
|
884
|
+
let lastComposerTapRegistered = false
|
|
885
|
+
if (!lastComposerTapRegistered) {
|
|
886
|
+
lastComposerTapRegistered = true
|
|
887
|
+
document.addEventListener('pointerdown', (e) => {
|
|
888
|
+
if (!active) return
|
|
889
|
+
const t = e.target
|
|
890
|
+
if (t instanceof Element && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') && t.closest('[class$="_composerSeat"]') !== null) lastComposerTap = Date.now()
|
|
891
|
+
}, true)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// The plugin apply() wires toggleSidebar/closeDetails to ctx.layout once
|
|
895
|
+
// it is live, and flips the master switch when the settings snapshot
|
|
896
|
+
// settles.
|
|
897
|
+
w.__dshRemoteAdapt = {
|
|
898
|
+
evaluate,
|
|
899
|
+
toggleSidebar: null,
|
|
900
|
+
closeDetails: null,
|
|
901
|
+
setEnabled(on: boolean): void {
|
|
902
|
+
adaptEnabled = on
|
|
903
|
+
if (on) evaluate()
|
|
904
|
+
else revert()
|
|
905
|
+
},
|
|
906
|
+
flushCloseDetails(): void {
|
|
907
|
+
// The first apply() ran before any wiring existed, so its
|
|
908
|
+
// closeDetails call was a no-op; replay it once the layout face is
|
|
909
|
+
// live so a restored details panel cannot sit hidden-uncloseable.
|
|
910
|
+
if (active) w.__dshRemoteAdapt?.closeDetails?.()
|
|
911
|
+
},
|
|
912
|
+
}
|
|
913
|
+
}
|