dsh-zen-remote 1.0.0 → 1.0.2
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.md +20 -0
- package/docs/interface.md +1 -0
- package/lib/client.js +458 -33
- package/lib/types/client/effects/keyboard-guard.d.ts +27 -0
- package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/effects/welcome-notice.d.ts +20 -0
- package/lib/types/client/effects/welcome-notice.d.ts.map +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/styles/base.css.d.ts +1 -1
- package/lib/types/client/styles/base.css.d.ts.map +1 -1
- package/lib/types/client/styles/chips.css.d.ts +1 -1
- package/lib/types/client/styles/chips.css.d.ts.map +1 -1
- package/lib/types/client/styles/compat.css.d.ts +1 -1
- package/lib/types/client/styles/compat.css.d.ts.map +1 -1
- package/lib/types/client/styles/content.css.d.ts +2 -0
- package/lib/types/client/styles/content.css.d.ts.map +1 -0
- package/lib/types/client/styles/header.css.d.ts +1 -1
- package/lib/types/client/styles/header.css.d.ts.map +1 -1
- package/lib/types/client/styles/index.d.ts +3 -2
- package/lib/types/client/styles/index.d.ts.map +1 -1
- package/package.json +7 -9
- package/pwa/app.css +7 -2
- package/src/client/effects/keyboard-guard.ts +121 -0
- package/src/client/effects/turn-fold.ts +14 -0
- package/src/client/effects/welcome-notice.ts +66 -0
- package/src/client/index.tsx +12 -0
- package/src/client/styles/base.css.ts +28 -13
- package/src/client/styles/chips.css.ts +10 -10
- package/src/client/styles/compat.css.ts +51 -0
- package/src/client/styles/content.css.ts +103 -0
- package/src/client/styles/header.css.ts +41 -7
- package/src/client/styles/index.ts +5 -3
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
|
|
3
|
+
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
4
|
+
const PHONE_QUERY = '(max-width: 767px)'
|
|
5
|
+
|
|
6
|
+
/** The official composer slot wrapper (same marker composer.css.ts styles). */
|
|
7
|
+
const COMPOSER = '[data-slot="conversation.composer.bar"]'
|
|
8
|
+
|
|
9
|
+
/** A tap/keystroke older than this no longer explains a focus. */
|
|
10
|
+
const INTENT_WINDOW_MS = 1000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* S9 — keep the phone keyboard down until the user asks for it.
|
|
14
|
+
*
|
|
15
|
+
* dsh-client-ui-conversation focuses the composer textarea on every
|
|
16
|
+
* sessionId change (lib/client.js:3423 — el.focus({preventScroll:true}) in a
|
|
17
|
+
* [locked, sessionId] effect). Sensible on desktop; on a phone it pops the
|
|
18
|
+
* software keyboard over half the screen every time a session opens.
|
|
19
|
+
*
|
|
20
|
+
* Rule: focus on the composer textarea survives only when the user asked for
|
|
21
|
+
* it — a tap on the textarea itself or typing on a hardware keyboard.
|
|
22
|
+
* Anything else (session-open autofocus, push-deep-link opens, the refocus
|
|
23
|
+
* side effects of the other composer buttons — slash-command toggle, attach,
|
|
24
|
+
* send) is blurred.
|
|
25
|
+
*
|
|
26
|
+
* Two triggers, because one is not enough:
|
|
27
|
+
* - focusin catches the autofocus the moment it happens;
|
|
28
|
+
* - a body MutationObserver re-runs the check after transcript swaps
|
|
29
|
+
* (opening a session re-renders the flow but may reuse the same textarea,
|
|
30
|
+
* and a focus that landed before this plugin loaded never fired focusin
|
|
31
|
+
* for us at all).
|
|
32
|
+
* Once focus is user-granted it stays granted until the textarea blurs, so
|
|
33
|
+
* the observer never yanks a keyboard the user opened (e.g. while the agent
|
|
34
|
+
* streams and the user pauses typing).
|
|
35
|
+
*/
|
|
36
|
+
export function installKeyboardGuard(ctx: ClientContext): void {
|
|
37
|
+
ctx.effect(() => {
|
|
38
|
+
const narrow = window.matchMedia(PHONE_QUERY)
|
|
39
|
+
let lastIntent = 0
|
|
40
|
+
let granted = false
|
|
41
|
+
let observer: MutationObserver | null = null
|
|
42
|
+
let frame = 0
|
|
43
|
+
|
|
44
|
+
const composerTextarea = (node: unknown): HTMLElement | null =>
|
|
45
|
+
node instanceof HTMLElement && node.tagName === 'TEXTAREA' && node.closest(COMPOSER) !== null
|
|
46
|
+
? node
|
|
47
|
+
: null
|
|
48
|
+
|
|
49
|
+
const onPointerDown = (event: PointerEvent): void => {
|
|
50
|
+
// Only the textarea itself grants the keyboard. A tap on any other
|
|
51
|
+
// composer control (slash-command toggle, attach, model menu, send)
|
|
52
|
+
// must not: several of them refocus the input as a side effect, which
|
|
53
|
+
// popped the keyboard on every command-button tap.
|
|
54
|
+
if (composerTextarea(event.target) !== null) {
|
|
55
|
+
lastIntent = Date.now()
|
|
56
|
+
granted = true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const onKeyDown = (): void => {
|
|
60
|
+
lastIntent = Date.now()
|
|
61
|
+
if (composerTextarea(document.activeElement) !== null) granted = true
|
|
62
|
+
}
|
|
63
|
+
const sweep = (): void => {
|
|
64
|
+
const el = composerTextarea(document.activeElement)
|
|
65
|
+
if (el === null) return
|
|
66
|
+
if (granted || Date.now() - lastIntent < INTENT_WINDOW_MS) {
|
|
67
|
+
granted = true
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
el.blur()
|
|
71
|
+
}
|
|
72
|
+
const onFocusIn = (event: FocusEvent): void => {
|
|
73
|
+
if (composerTextarea(event.target) === null) return
|
|
74
|
+
sweep()
|
|
75
|
+
}
|
|
76
|
+
const onFocusOut = (event: FocusEvent): void => {
|
|
77
|
+
if (composerTextarea(event.target) === null) return
|
|
78
|
+
granted = false
|
|
79
|
+
}
|
|
80
|
+
const schedule = (): void => {
|
|
81
|
+
if (observer === null || frame !== 0) return
|
|
82
|
+
// setTimeout, not requestAnimationFrame: rAF pauses in hidden/
|
|
83
|
+
// backgrounded pages, where the autofocus still happens — the sweep
|
|
84
|
+
// must run there too or the keyboard pops when the page is foregrounded.
|
|
85
|
+
frame = window.setTimeout(() => {
|
|
86
|
+
frame = 0
|
|
87
|
+
sweep()
|
|
88
|
+
}, 100)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const attach = (): void => {
|
|
92
|
+
if (observer !== null) return
|
|
93
|
+
document.addEventListener('pointerdown', onPointerDown, { capture: true, passive: true })
|
|
94
|
+
document.addEventListener('keydown', onKeyDown, { capture: true, passive: true })
|
|
95
|
+
document.addEventListener('focusin', onFocusIn, true)
|
|
96
|
+
document.addEventListener('focusout', onFocusOut, true)
|
|
97
|
+
observer = new MutationObserver(schedule)
|
|
98
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
99
|
+
sweep()
|
|
100
|
+
}
|
|
101
|
+
const detach = (): void => {
|
|
102
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
103
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
104
|
+
document.removeEventListener('focusin', onFocusIn, true)
|
|
105
|
+
document.removeEventListener('focusout', onFocusOut, true)
|
|
106
|
+
observer?.disconnect()
|
|
107
|
+
observer = null
|
|
108
|
+
if (frame !== 0) window.clearTimeout(frame)
|
|
109
|
+
frame = 0
|
|
110
|
+
granted = false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (narrow.matches) attach()
|
|
114
|
+
const onChange = (event: MediaQueryListEvent): void => (event.matches ? attach() : detach())
|
|
115
|
+
narrow.addEventListener('change', onChange)
|
|
116
|
+
return () => {
|
|
117
|
+
narrow.removeEventListener('change', onChange)
|
|
118
|
+
detach()
|
|
119
|
+
}
|
|
120
|
+
}, 'dsh-mobile-nav: composer keyboard guard')
|
|
121
|
+
}
|
|
@@ -160,11 +160,13 @@ export function installTurnFold(ctx: ClientContext): void {
|
|
|
160
160
|
const running = flow.querySelector(':scope > [role="status"]') !== null
|
|
161
161
|
const groups = groupsOf(flow)
|
|
162
162
|
const live = new Set<string>()
|
|
163
|
+
const liveItems = new Set<Element>()
|
|
163
164
|
|
|
164
165
|
groups.forEach((group, index) => {
|
|
165
166
|
if (group.items.length === 0) return
|
|
166
167
|
const open = expanded.has(group.key)
|
|
167
168
|
for (const item of group.items) {
|
|
169
|
+
liveItems.add(item)
|
|
168
170
|
item.setAttribute(FOLD, '')
|
|
169
171
|
if (open) item.setAttribute(OPEN, '')
|
|
170
172
|
else item.removeAttribute(OPEN)
|
|
@@ -189,6 +191,18 @@ export function installTurnFold(ctx: ClientContext): void {
|
|
|
189
191
|
const key = chip.dataset['foldGroup']
|
|
190
192
|
if (key === undefined || !live.has(key)) chip.remove()
|
|
191
193
|
}
|
|
194
|
+
|
|
195
|
+
// Unmark elements that stopped being process items. Without this, an
|
|
196
|
+
// assistant-step row folded whole while it streamed ONLY reasoning kept
|
|
197
|
+
// its row-level fold after prose started arriving in the same DOM node
|
|
198
|
+
// (React updates the row in place), hiding the streaming reply until a
|
|
199
|
+
// completion re-render rebuilt the row — the "reply only appears when
|
|
200
|
+
// the turn finishes" phone bug (2026-08-18).
|
|
201
|
+
for (const stale of flow.querySelectorAll(`[${FOLD}]`)) {
|
|
202
|
+
if (liveItems.has(stale)) continue
|
|
203
|
+
stale.removeAttribute(FOLD)
|
|
204
|
+
stale.removeAttribute(OPEN)
|
|
205
|
+
}
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
const schedule = (): void => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
|
|
3
|
+
/** Per-browser opt-out flag; localStorage because remote (non-loopback)
|
|
4
|
+
connections never persist the acknowledgement host-side. */
|
|
5
|
+
const OPTOUT_KEY = 'dsh-zen-remote:welcome-notice-optout'
|
|
6
|
+
|
|
7
|
+
/** Localized aria-labels of the DSH first-run notice dialog
|
|
8
|
+
(@deepseek-ai/dsh-client-ui-settings-models WELCOME_NOTICE_COPY). */
|
|
9
|
+
const NOTICE_LABELS = ['内测声明', 'Internal Testing Notice']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* "内测声明" first-run notice, opt-out half (ALL widths — same user-directed
|
|
13
|
+
* exception as the _previewBadge rule in styles/base.css.ts).
|
|
14
|
+
*
|
|
15
|
+
* Why not CSS: the notice dialog keeps document #root inert while MOUNTED and
|
|
16
|
+
* only restores it on unmount (its acknowledge button). display:none hid the
|
|
17
|
+
* dialog without unmounting it, which left the whole app permanently
|
|
18
|
+
* unclickable on remote connections (2026-08-18 incident) — remote browsers
|
|
19
|
+
* re-mount the notice on every page load because their acknowledgement is
|
|
20
|
+
* memory-only (`connection.isLoopback ? "host" : "memory"` upstream).
|
|
21
|
+
*
|
|
22
|
+
* So the dialog now shows normally, plus one injected "不再弹出" button. The
|
|
23
|
+
* user chooses: acknowledging via that button records the opt-out in this
|
|
24
|
+
* browser's localStorage, and later mounts are auto-acknowledged by clicking
|
|
25
|
+
* the dialog's own continue button — the component unmounts through its
|
|
26
|
+
* normal path and #root's inert is properly restored.
|
|
27
|
+
*/
|
|
28
|
+
export function installWelcomeNoticeOptOut(ctx: ClientContext): void {
|
|
29
|
+
ctx.effect(() => {
|
|
30
|
+
const handle = (dialog: HTMLElement): void => {
|
|
31
|
+
// The notice's action row holds exactly one button ("继续"/"Continue").
|
|
32
|
+
const continueBtn = dialog.querySelector('button')
|
|
33
|
+
if (continueBtn === null) return
|
|
34
|
+
if (localStorage.getItem(OPTOUT_KEY) !== null) {
|
|
35
|
+
continueBtn.click()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const row = continueBtn.parentElement
|
|
39
|
+
if (row === null || row.querySelector('.zen-welcome-optout') !== null) return
|
|
40
|
+
const optOut = document.createElement('button')
|
|
41
|
+
optOut.type = 'button'
|
|
42
|
+
optOut.className = 'zen-welcome-optout'
|
|
43
|
+
optOut.textContent =
|
|
44
|
+
dialog.getAttribute('aria-label') === '内测声明' ? '不再弹出' : "Don't show again"
|
|
45
|
+
optOut.addEventListener('click', () => {
|
|
46
|
+
localStorage.setItem(OPTOUT_KEY, '1')
|
|
47
|
+
continueBtn.click()
|
|
48
|
+
})
|
|
49
|
+
row.insertBefore(optOut, continueBtn)
|
|
50
|
+
}
|
|
51
|
+
const scan = (): void => {
|
|
52
|
+
for (const label of NOTICE_LABELS) {
|
|
53
|
+
const dialog = document.querySelector<HTMLElement>(
|
|
54
|
+
`[role="dialog"][aria-label="${label}"]`,
|
|
55
|
+
)
|
|
56
|
+
if (dialog !== null) handle(dialog)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// The dialog portals straight under <body>, so childList on body is
|
|
60
|
+
// enough; scan() once first in case it mounted before this plugin loaded.
|
|
61
|
+
scan()
|
|
62
|
+
const observer = new MutationObserver(scan)
|
|
63
|
+
observer.observe(document.body, { childList: true })
|
|
64
|
+
return () => observer.disconnect()
|
|
65
|
+
}, 'dsh-mobile-nav: welcome-notice opt-out')
|
|
66
|
+
}
|
package/src/client/index.tsx
CHANGED
|
@@ -15,6 +15,8 @@ import { installAionuiCompat } from './effects/aionui-compat.ts'
|
|
|
15
15
|
import { installHeaderStatusDot } from './effects/header-status.ts'
|
|
16
16
|
import { installGestures } from './effects/gestures.ts'
|
|
17
17
|
import { installTurnFold } from './effects/turn-fold.ts'
|
|
18
|
+
import { installWelcomeNoticeOptOut } from './effects/welcome-notice.ts'
|
|
19
|
+
import { installKeyboardGuard } from './effects/keyboard-guard.ts'
|
|
18
20
|
import { NS, en, zh } from './locales.ts'
|
|
19
21
|
import type { MobileNavKey } from './locales.ts'
|
|
20
22
|
|
|
@@ -78,6 +80,16 @@ export function apply(ctx: ClientContext): void {
|
|
|
78
80
|
// not carry this.
|
|
79
81
|
installTurnFold(ctx)
|
|
80
82
|
|
|
83
|
+
// "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
|
|
84
|
+
// dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
|
|
85
|
+
// per-browser "不再弹出" opt-out instead.
|
|
86
|
+
installWelcomeNoticeOptOut(ctx)
|
|
87
|
+
|
|
88
|
+
// S9: opening a session must not pop the phone keyboard — the official
|
|
89
|
+
// composer autofocuses on every sessionId change; keep focus only when the
|
|
90
|
+
// user tapped the composer (or typed) themselves.
|
|
91
|
+
installKeyboardGuard(ctx)
|
|
92
|
+
|
|
81
93
|
// Page-stack store (apply world) — created before any registration so
|
|
82
94
|
// every slot below (the phone home screen, the session header's back
|
|
83
95
|
// button) shares the exact same handle/instance.
|
|
@@ -145,23 +145,38 @@ export const BASE_CSS = `
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/* ---------- beta-notice
|
|
149
|
-
User-directed (2026-08-17):
|
|
150
|
-
including desktop. This is the ONE place this plugin
|
|
151
|
-
its own ">=1024px strict no-op" rule
|
|
152
|
-
Selector notes:
|
|
148
|
+
/* ---------- beta-notice handling (ALL widths — deliberate exception) ----
|
|
149
|
+
User-directed (2026-08-17): tone down DSH's beta/preview notices
|
|
150
|
+
everywhere, including desktop. This is the ONE place this plugin
|
|
151
|
+
intentionally breaks its own ">=1024px strict no-op" rule.
|
|
153
152
|
- The hero "预览版" badge only carries a hashed class; per repo convention
|
|
154
153
|
the stable part is the suffix (_previewBadge), matched with [class$=].
|
|
155
|
-
- The first-run "内测声明" modal
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
block taps; the app never requires acknowledging this dialog to work. */
|
|
154
|
+
- The first-run "内测声明" modal must NOT be hidden with CSS: while it is
|
|
155
|
+
mounted it holds document #root inert, so display:none leaves the whole
|
|
156
|
+
app unclickable (2026-08-18 incident). effects/welcome-notice.ts instead
|
|
157
|
+
injects a "不再弹出" opt-out button; this styles it. */
|
|
160
158
|
[class$="_previewBadge"] {
|
|
161
159
|
display: none !important;
|
|
162
160
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
/* Mirrors the official primitives Button (md capsule, outline variant):
|
|
162
|
+
36px height, 18px radius, --dsw-alias-border-l2 hairline. */
|
|
163
|
+
.zen-welcome-optout {
|
|
164
|
+
display: inline-flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
height: 36px;
|
|
168
|
+
padding: 0 14px;
|
|
169
|
+
margin-right: 8px;
|
|
170
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, .15));
|
|
171
|
+
border-radius: 18px;
|
|
172
|
+
background: transparent;
|
|
173
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
line-height: 22px;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
-webkit-tap-highlight-color: transparent;
|
|
178
|
+
}
|
|
179
|
+
.zen-welcome-optout:hover {
|
|
180
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
166
181
|
}
|
|
167
182
|
`
|
|
@@ -88,10 +88,14 @@ export const CHIPS_CSS = `/* ---------- chips row + settings entry (S5) --------
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/* Plugin-entry chips row: one horizontally-scrolling line of 34px pills
|
|
91
|
-
between the workspace title and the session list (prototype spec).
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
between the workspace title and the session list (prototype spec).
|
|
92
|
+
Scrollbar hidden on BOTH engines (2026-08-18 user decision): iOS
|
|
93
|
+
overlay scrollbars never showed anyway, but desktop browsers painted a
|
|
94
|
+
permanent 2px strip under the pills. This revisits the AGENTS.md
|
|
95
|
+
"silently cut off the last tab" lesson deliberately — with 4+ chips the
|
|
96
|
+
row now always overflows mid-chip (a partially visible pill is the
|
|
97
|
+
scroll affordance), unlike the early incident where the chips fit
|
|
98
|
+
flush and the hidden overflow was undiscoverable. */
|
|
95
99
|
[data-mobile-nav="chip-row"] {
|
|
96
100
|
flex: 0 0 auto;
|
|
97
101
|
display: flex;
|
|
@@ -101,14 +105,10 @@ export const CHIPS_CSS = `/* ---------- chips row + settings entry (S5) --------
|
|
|
101
105
|
overflow-x: auto;
|
|
102
106
|
overscroll-behavior: contain;
|
|
103
107
|
-webkit-overflow-scrolling: touch;
|
|
104
|
-
scrollbar-width:
|
|
108
|
+
scrollbar-width: none;
|
|
105
109
|
}
|
|
106
110
|
[data-mobile-nav="chip-row"]::-webkit-scrollbar {
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
[data-mobile-nav="chip-row"]::-webkit-scrollbar-thumb {
|
|
110
|
-
background: var(--dsw-alias-border-l2, rgba(0, 0, 0, .16));
|
|
111
|
-
border-radius: 2px;
|
|
111
|
+
display: none;
|
|
112
112
|
}
|
|
113
113
|
[data-mobile-nav="chip"],
|
|
114
114
|
[data-mobile-nav="chip-more"] {
|
|
@@ -678,4 +678,55 @@ export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility -----
|
|
|
678
678
|
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
|
|
679
679
|
}
|
|
680
680
|
}
|
|
681
|
+
|
|
682
|
+
/* ---------- dsh-agent-teams: phone floater reposition (2026-08-18) ----------
|
|
683
|
+
THIRD-PARTY COMPAT RULE — @nanmicoder/dsh-agent-teams (the AgentTeams
|
|
684
|
+
activity floater: a collapsed badge + expandable panel, body-portaled
|
|
685
|
+
under its own mount marker div[data-agent-teams-host]). Its own CSS
|
|
686
|
+
fixes both at top:56px (<=640px) viewport coordinates and never reads
|
|
687
|
+
env(safe-area-inset-*), so on a notched iPhone the badge lands INSIDE
|
|
688
|
+
the safe-area + this plugin's 76px session header band — overlapping
|
|
689
|
+
the info and workbench buttons (real-device report, 2026-08-18).
|
|
690
|
+
Move both below the header instead: safe-area + 76px header chrome
|
|
691
|
+
(48px title row + 28px view-switch row, header.css.ts) + 8px gap.
|
|
692
|
+
Anchored on the plugin's own mount marker + class-suffix selectors per
|
|
693
|
+
this repo's hashed-class convention — when dsh-agent-teams is not
|
|
694
|
+
installed the marker never exists and this whole section is inert
|
|
695
|
+
(same presence-gating as the dsh-better-sidebar rules above).
|
|
696
|
+
!important because their positions come from html-level CSS vars
|
|
697
|
+
(--agent-teams-panel-top et al.) whose style tag order vs ours is not
|
|
698
|
+
guaranteed. z-index: their 2147483000 would float the badge over every
|
|
699
|
+
one of this plugin's own overlays (drawer 40, info sheet 55-57, close
|
|
700
|
+
pill 70); 30 keeps it above content but below all of them. */
|
|
701
|
+
@media (max-width: 767px) {
|
|
702
|
+
[data-agent-teams-host] [class$="_badge"],
|
|
703
|
+
[data-agent-teams-host] [class$="_panel"] {
|
|
704
|
+
top: calc(var(--mnav-sat) + 84px) !important;
|
|
705
|
+
right: 10px !important;
|
|
706
|
+
z-index: 30 !important;
|
|
707
|
+
}
|
|
708
|
+
[data-agent-teams-host] [class$="_panel"] {
|
|
709
|
+
left: 10px !important;
|
|
710
|
+
width: auto !important;
|
|
711
|
+
/* Their min-height:min(560px, calc(100dvh - 56 - 56)) resolves against
|
|
712
|
+
the OLD top var — on short phones it exceeds the space left below
|
|
713
|
+
the moved-down top edge and would force overflow; content-sized with
|
|
714
|
+
a hard cap replaces it. 76px bottom clearance keeps the collapsed
|
|
715
|
+
composer's top edge visible under the panel. */
|
|
716
|
+
min-height: 0 !important;
|
|
717
|
+
max-height: calc(100dvh - var(--mnav-sat) - 84px - var(--mnav-sab) - 76px) !important;
|
|
718
|
+
}
|
|
719
|
+
/* The floater belongs to the SESSION page: on the home list it hovered
|
|
720
|
+
over the session rows (user feedback, 2026-08-18). The phone page
|
|
721
|
+
stack is this plugin's own overlay — the official conversation (and
|
|
722
|
+
with it the floater's "current session") stays mounted underneath, so
|
|
723
|
+
agent-teams keeps rendering it; hide it whenever the home level is the
|
|
724
|
+
visible one (data-view flips to "session" inside a session, and the
|
|
725
|
+
badge/panel come right back). :has() is already a documented
|
|
726
|
+
requirement of this plugin (Chromium 105+, docs/interface.md). */
|
|
727
|
+
body:has([data-mobile-nav="home"][data-view="home"]) [data-agent-teams-host] [class$="_badge"],
|
|
728
|
+
body:has([data-mobile-nav="home"][data-view="home"]) [data-agent-teams-host] [class$="_panel"] {
|
|
729
|
+
display: none !important;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
681
732
|
`
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// content — chat markdown readability on phones (2026-08-18). Shares no
|
|
2
|
+
// selector with any other file; appended last in styles/index.ts.
|
|
3
|
+
|
|
4
|
+
export const CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ---------- */
|
|
5
|
+
|
|
6
|
+
@media (max-width: 767px) {
|
|
7
|
+
/* Inline code: upstream renders it display:inline-flex — an atomic inline
|
|
8
|
+
box that can never wrap across lines, so one long code span walks off
|
|
9
|
+
the right edge of the screen. Plain inline keeps the background pill
|
|
10
|
+
per line fragment and lets long tokens break anywhere. Deliberately
|
|
11
|
+
unscoped (any :not(pre) > code, not just the _markdown_ module):
|
|
12
|
+
inline code also renders outside that container (user bubbles,
|
|
13
|
+
reasoning rows), and on a phone there is no width at which an
|
|
14
|
+
unwrappable inline box is right. !important beats the upstream
|
|
15
|
+
display:inline-flex on specificity ties. */
|
|
16
|
+
:not(pre) > code {
|
|
17
|
+
display: inline !important;
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
/* iOS WebKit: overflow-wrap alone still failed to break long tokens in
|
|
20
|
+
inline code on a real iPhone (screenshot-verified 2026-08-18, li >
|
|
21
|
+
code path case) while desktop Chromium broke them fine. break-all is
|
|
22
|
+
the everywhere-supported hammer; acceptable for code pills. */
|
|
23
|
+
word-break: break-all;
|
|
24
|
+
}
|
|
25
|
+
/* Belt-and-suspenders for engines that compute soft-wrap opportunities
|
|
26
|
+
from the block container rather than the inline itself (historical
|
|
27
|
+
WebKit behavior): inherit anywhere through the whole transcript so the
|
|
28
|
+
block side agrees. Prose is untouched until something would overflow;
|
|
29
|
+
the table is already pinned wide by min-width: max-content below. */
|
|
30
|
+
[data-chat-flow],
|
|
31
|
+
[class*="_markdown_"] {
|
|
32
|
+
overflow-wrap: anywhere;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Markdown tables: the official _tableScroll_ wrapper is already a
|
|
36
|
+
horizontal scroll viewport around a width:max-content table, but its
|
|
37
|
+
cells are capped at min(30vw, 320px) — barely 120px on a phone, which
|
|
38
|
+
crushes every column into a vertical word-stack. Let columns size to
|
|
39
|
+
their content instead and read the overflow by scrolling the wrapper.
|
|
40
|
+
480px (not a vw cap — 85vw was ~320px on a 375px phone and still
|
|
41
|
+
wrapped mid-length cells) keeps typical cells on one line; only truly
|
|
42
|
+
prose-heavy cells wrap, instead of dragging the table kilometers
|
|
43
|
+
wide. */
|
|
44
|
+
[class*="_tableScroll_"] th,
|
|
45
|
+
[class*="_tableScroll_"] td {
|
|
46
|
+
max-width: 480px !important;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* iOS WebKit fallback: upstream's own table { width: max-content } is what
|
|
50
|
+
keeps the table at its natural width inside the scroll wrapper, but
|
|
51
|
+
WebKit's max-content support on table boxes is incomplete — on a real
|
|
52
|
+
iPhone the table fell back to shrink-to-fit and every column got
|
|
53
|
+
crushed to its min-width (the "ra/nk" vertical-word-stack screenshot,
|
|
54
|
+
2026-08-18). Redeclare the natural-width intent with both width and
|
|
55
|
+
min-width so whichever keyword the engine honors wins; the wrapper
|
|
56
|
+
already scrolls the overflow. */
|
|
57
|
+
[class*="_tableScroll_"] table {
|
|
58
|
+
width: max-content !important;
|
|
59
|
+
min-width: max-content !important;
|
|
60
|
+
max-width: none !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Turn-tail metadata (2026-08-18): the timing stats ("18:06 · 用时 29秒 ·
|
|
64
|
+
首 token 0.9秒 · 163 tok/s") are desktop-hover-revealed (official
|
|
65
|
+
[data-time-hover-root] gate holds them at opacity 0 until :hover /
|
|
66
|
+
:focus-within) — a phone never hovers, so they were simply invisible;
|
|
67
|
+
and even ignoring that, the row is nowrap + overflow:hidden (plus this
|
|
68
|
+
plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the
|
|
69
|
+
text. On phones: always visible, own full-width line under the action
|
|
70
|
+
icons, wrapping freely, one type size down. !important + the extra
|
|
71
|
+
[class$="_actions"] hop out-specifies both upstream and layout.css.ts. */
|
|
72
|
+
/* Pin the tail row's width chain before allowing wrap: with wrap enabled
|
|
73
|
+
the row's intrinsic (shrink-to-fit) width collapses to its widest
|
|
74
|
+
single icon, taking the whole tail down to ~16px. */
|
|
75
|
+
[data-chat-flow-kind="turn-tail"] [class$="_root"],
|
|
76
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
77
|
+
width: 100%;
|
|
78
|
+
}
|
|
79
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] {
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
row-gap: 2px;
|
|
82
|
+
}
|
|
83
|
+
/* Wide companion to the rule below: mirrors the upstream hover-gate
|
|
84
|
+
selector shape directly, with no assumptions about the tail's DOM
|
|
85
|
+
structure, so the stats can't stay invisible even if the phone's tail
|
|
86
|
+
renders differently than desktop. */
|
|
87
|
+
[data-time-hover-root] [class$="_timeEnd"],
|
|
88
|
+
[data-time-hover-root] [class$="_timeStart"] {
|
|
89
|
+
opacity: 1 !important;
|
|
90
|
+
transition: none !important;
|
|
91
|
+
}
|
|
92
|
+
[data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_timeEnd"] {
|
|
93
|
+
opacity: 1 !important;
|
|
94
|
+
flex-basis: 100% !important;
|
|
95
|
+
white-space: normal !important;
|
|
96
|
+
overflow: visible !important;
|
|
97
|
+
text-overflow: clip !important;
|
|
98
|
+
padding-left: 0 !important;
|
|
99
|
+
font-size: 12px;
|
|
100
|
+
line-height: 18px;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`
|
|
@@ -157,21 +157,55 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
|
|
|
157
157
|
min-width: 0;
|
|
158
158
|
overflow: hidden;
|
|
159
159
|
}
|
|
160
|
-
/*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
/* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header
|
|
161
|
+
lets you tap the parent session's title to switch back — that affordance
|
|
162
|
+
was lost on phone when the whole parent chain was hidden as "多余项".
|
|
163
|
+
Show only the immediate parent (for agent-teams member sessions that IS
|
|
164
|
+
the captain/main session; deeper ancestors stay hidden — a 3-level chain
|
|
165
|
+
cannot fit the ~206px center column at 390px). The parent crumb is the
|
|
166
|
+
official breadcrumb <button> (dsh-client-ui-conversation renders
|
|
167
|
+
open(parentId) as its onClick), so tapping needs no JS from this plugin,
|
|
168
|
+
and main sessions (chain length 1) have no :nth-last-child(2) seg at
|
|
169
|
+
all — the centered single title is pixel-identical to before. */
|
|
170
|
+
[data-phase] header [class$="_crumbSeg"]:not(:nth-last-child(-n+2)) {
|
|
164
171
|
display: none !important;
|
|
165
172
|
}
|
|
173
|
+
[data-phase] header [class$="_crumbSeg"]:nth-last-child(2) {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
flex: none;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
max-width: 38%;
|
|
179
|
+
}
|
|
180
|
+
/* Ancestors BEFORE the parent seg are hidden, which would leave the
|
|
181
|
+
parent seg's own leading "/" dangling at the start of the title. */
|
|
182
|
+
[data-phase] header [class$="_crumbSeg"]:nth-last-child(2) [class$="_crumbSep"] {
|
|
183
|
+
display: none !important;
|
|
184
|
+
}
|
|
185
|
+
/* Parent crumb: small tertiary text so the 19px current title stays the
|
|
186
|
+
visual anchor; min-height keeps a finger-sized target inside the 48px
|
|
187
|
+
row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */
|
|
188
|
+
[data-phase] header [class$="_crumbSeg"]:nth-last-child(2) [class$="_crumb"] {
|
|
189
|
+
font-size: 13px;
|
|
190
|
+
font-weight: 400;
|
|
191
|
+
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));
|
|
192
|
+
min-height: 44px;
|
|
193
|
+
display: inline-flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
padding: 0 2px 0 6px;
|
|
196
|
+
touch-action: manipulation;
|
|
197
|
+
-webkit-tap-highlight-color: transparent;
|
|
198
|
+
}
|
|
166
199
|
[data-phase] header [class$="_crumbSeg"]:last-child {
|
|
167
200
|
display: flex;
|
|
168
201
|
align-items: center;
|
|
169
202
|
min-width: 0;
|
|
170
203
|
max-width: 100%;
|
|
171
204
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
205
|
+
/* The last seg's own "/" is the divider between parent and current title —
|
|
206
|
+
it only exists when there IS an ancestor (index > 0), so it can simply
|
|
207
|
+
stay visible now that the parent seg shows. (It used to be hidden here:
|
|
208
|
+
with the parent hidden it dangled before the title.) */
|
|
175
209
|
/* BOTH selectors are required: [class$=] matches the whole class
|
|
176
210
|
ATTRIBUTE's suffix, and the current-session title button's attribute is
|
|
177
211
|
"wSkVaW_crumb wSkVaW_crumbCurrent" — it ends in _crumbCurrent, so the _crumb
|
|
@@ -8,6 +8,7 @@ import { COMPOSER_CSS } from './composer.css.ts'
|
|
|
8
8
|
import { INFO_CSS } from './info.css.ts'
|
|
9
9
|
import { TURN_FOLD_CSS } from './turn-fold.css.ts'
|
|
10
10
|
import { CHIPS_CSS } from './chips.css.ts'
|
|
11
|
+
import { CONTENT_CSS } from './content.css.ts'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* All mobile styles, concatenated in the exact order of the original
|
|
@@ -19,7 +20,8 @@ import { CHIPS_CSS } from './chips.css.ts'
|
|
|
19
20
|
* win that tie too), the turn-process fold (turn-fold, S8, which shares no
|
|
20
21
|
* selector with any of them), and finally the chips row / settings entry /
|
|
21
22
|
* portal fix (chips, S5) — all appended in this order so their <768px rules
|
|
22
|
-
* win ties against the shared <=1023px block
|
|
23
|
-
*
|
|
23
|
+
* win ties against the shared <=1023px block, then the chat markdown
|
|
24
|
+
* readability overrides (content, which shares no selector with any of
|
|
25
|
+
* them). Injected as ONE <style data-plugin> tag — do not reorder.
|
|
24
26
|
*/
|
|
25
|
-
export const MOBILE_CSS = [BASE_CSS, LAYOUT_CSS, COMPAT_CSS, MISC_CSS, HOME_CSS, HEADER_CSS, COMPOSER_CSS, INFO_CSS, TURN_FOLD_CSS, CHIPS_CSS].join('\n')
|
|
27
|
+
export const MOBILE_CSS = [BASE_CSS, LAYOUT_CSS, COMPAT_CSS, MISC_CSS, HOME_CSS, HEADER_CSS, COMPOSER_CSS, INFO_CSS, TURN_FOLD_CSS, CHIPS_CSS, CONTENT_CSS].join('\n')
|