dsh-any-background 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -49,7 +49,7 @@ window.__ModuleLoader__.load({
49
49
  uiOpacitySide: "侧边栏",
50
50
  uiOpacityCard: "对话框中选项面板",
51
51
  uiOpacityInput: "输入框与控件",
52
- uiSop: "设置界面透明度",
52
+ uiSop: "设置界面",
53
53
  uiChatRegion: "对话文本框",
54
54
  uiTrajectory: "轨迹页",
55
55
  uiPanelRegion: "bettersidebar",
@@ -241,7 +241,7 @@ window.__ModuleLoader__.load({
241
241
  uiOpacitySide: "Sidebar",
242
242
  uiOpacityCard: "Cards & panels",
243
243
  uiOpacityInput: "Input & controls",
244
- uiSop: "Settings interface opacity",
244
+ uiSop: "Settings interface",
245
245
  uiChatRegion: "Conversation text frame",
246
246
  uiTrajectory: "Trajectory view",
247
247
  uiPanelRegion: "bettersidebar",
@@ -413,13 +413,13 @@ window.__ModuleLoader__.load({
413
413
  const DEFAULT_CONFIG = {
414
414
  color: null,
415
415
  opacities: {
416
- bg: .5,
416
+ bg: 0,
417
417
  sidebar: .5,
418
418
  card: .5,
419
419
  input: .5
420
420
  },
421
421
  blurs: {
422
- bg: 30,
422
+ bg: 0,
423
423
  sidebar: 30,
424
424
  card: 30,
425
425
  settings: 30,
@@ -484,7 +484,7 @@ window.__ModuleLoader__.load({
484
484
  },
485
485
  settingsOpacity: .5,
486
486
  wallpaperOpacity: 1,
487
- blur: 30,
487
+ blur: 0,
488
488
  bgState: {
489
489
  zoom: 1,
490
490
  x: 0,
@@ -994,93 +994,552 @@ window.__ModuleLoader__.load({
994
994
  };
995
995
  }
996
996
  //#endregion
997
- //#region src/client/host.ts
997
+ //#region src/host-compat/channel.ts
998
+ /** What this plugin supports, in upgrade order. One row = one build whose facts
999
+ * were checked; several rows may share a `channel` when a newer build was
1000
+ * verified as needing no change to that folder's adapter (as `0.1.7-rc.1` was) —
1001
+ * a row then reports `exact` instead of `line`, and no new folder appears.
1002
+ * Adding an ADAPTER means adding a channel plus a folder in
1003
+ * `client/host-compat/versions/`. */
1004
+ const SUPPORTED_RELEASES = [
1005
+ {
1006
+ channel: "0.1.5-rc",
1007
+ release: "0.1.5-rc.2"
1008
+ },
1009
+ {
1010
+ channel: "0.1.6-alpha",
1011
+ release: "0.1.6-alpha.2"
1012
+ },
1013
+ {
1014
+ channel: "0.1.7-alpha",
1015
+ release: "0.1.7-alpha.2"
1016
+ },
1017
+ {
1018
+ channel: "0.1.7-alpha",
1019
+ release: "0.1.7-rc.1"
1020
+ }
1021
+ ];
1022
+ const UNKNOWN_HOST_INFO = {
1023
+ version: null,
1024
+ channel: "unknown"
1025
+ };
1026
+ /** True when a string looks like a release version. A bare `~/.dsh` directory
1027
+ * basename does not, which is why the disk probe sanity-checks before trusting
1028
+ * a path segment. */
1029
+ const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
1030
+ function looksLikeVersion(value) {
1031
+ return VERSION_RE.test(value);
1032
+ }
1033
+ const PARSE_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
1034
+ function parse(version) {
1035
+ const m = PARSE_RE.exec(version);
1036
+ if (m === null) return null;
1037
+ const parts = m[4] === void 0 ? [] : m[4].split(".");
1038
+ const build = parts.length > 1 ? Number(parts[1]) : NaN;
1039
+ return {
1040
+ core: [
1041
+ Number(m[1]),
1042
+ Number(m[2]),
1043
+ Number(m[3])
1044
+ ],
1045
+ channel: parts.length > 0 ? parts[0] : null,
1046
+ build: Number.isFinite(build) ? build : null
1047
+ };
1048
+ }
1049
+ /** The table, parsed once: every row is a literal this file controls. */
1050
+ const TABLE = SUPPORTED_RELEASES.map((row) => ({
1051
+ ...row,
1052
+ parsed: parse(row.release)
1053
+ }));
1054
+ function cmpCore(a, b) {
1055
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
1056
+ return 0;
1057
+ }
1058
+ /** Total order over releases, following the semver rule that a prerelease sorts
1059
+ * BELOW the release it precedes (`0.1.6-alpha.2 < 0.1.6`). */
1060
+ function cmp(a, b) {
1061
+ const core = cmpCore(a.core, b.core);
1062
+ if (core !== 0) return core;
1063
+ if (a.channel === null || b.channel === null) {
1064
+ if (a.channel === b.channel) return 0;
1065
+ return a.channel === null ? 1 : -1;
1066
+ }
1067
+ if (a.channel !== b.channel) return a.channel < b.channel ? -1 : 1;
1068
+ const ab = a.build ?? 0;
1069
+ const bb = b.build ?? 0;
1070
+ return ab === bb ? 0 : ab < bb ? -1 : 1;
1071
+ }
1072
+ /** Pick the adapter for a release, clamping to the nearest verified line when the
1073
+ * release is outside everything this plugin has seen.
1074
+ *
1075
+ * A release is matched by PATCH LINE before anything else: `0.1.6-alpha.4` and a
1076
+ * future `0.1.6` stable both belong to the line verified at `0.1.6-alpha.2`, and
1077
+ * handing them that adapter beats giving up — the DOM probes only cover panel
1078
+ * mechanics, not the surface decisions the line is known for.
1079
+ *
1080
+ * Outside every verified line the answer is a guess, so it is the closest one and
1081
+ * it says so (`nearest`): newer than the newest line → that newest adapter, older
1082
+ * than the oldest → the oldest, strictly between two → the LOWER one, because an
1083
+ * adapter may only claim what it was verified for. A release that will not parse
1084
+ * at all is `unknown`, and the DOM arbitrates there. */
1085
+ function classifyRelease(version) {
1086
+ const host = version === null ? null : parse(version);
1087
+ if (host === null) return {
1088
+ channel: "unknown",
1089
+ match: "unresolved"
1090
+ };
1091
+ const sameLine = TABLE.filter((row) => cmpCore(row.parsed.core, host.core) === 0);
1092
+ if (sameLine.length > 0) {
1093
+ const exact = sameLine.find((row) => row.release === version);
1094
+ if (exact !== void 0) return {
1095
+ channel: exact.channel,
1096
+ match: "exact"
1097
+ };
1098
+ const atOrBelow = sameLine.filter((row) => cmp(row.parsed, host) <= 0);
1099
+ return {
1100
+ channel: (atOrBelow.length > 0 ? atOrBelow[atOrBelow.length - 1] : sameLine[0]).channel,
1101
+ match: "line"
1102
+ };
1103
+ }
1104
+ const older = TABLE.filter((row) => cmp(row.parsed, host) < 0);
1105
+ if (older.length === 0) return {
1106
+ channel: TABLE[0].channel,
1107
+ match: "nearest"
1108
+ };
1109
+ if (TABLE.filter((row) => cmp(row.parsed, host) > 0).length === 0) return {
1110
+ channel: TABLE[TABLE.length - 1].channel,
1111
+ match: "nearest"
1112
+ };
1113
+ return {
1114
+ channel: older[older.length - 1].channel,
1115
+ match: "nearest"
1116
+ };
1117
+ }
1118
+ /** The channel a release maps onto — the verdict's answer without the confidence. */
1119
+ function channelOf(version) {
1120
+ return classifyRelease(version).channel;
1121
+ }
1122
+ /** Prerelease tag of a release string (`alpha.2` in `0.1.6-alpha.2`), or null
1123
+ * when the version is bare/undetermined. Needed because a channel is coarser
1124
+ * than reality: `conversation.session.header.leading` ships in exactly one
1125
+ * build within the 0.1.6-alpha line. */
1126
+ function prereleaseOf(version) {
1127
+ if (version === null) return null;
1128
+ return /^\d+\.\d+\.\d+-([0-9A-Za-z.-]+)$/.exec(version)?.[1] ?? null;
1129
+ }
1130
+ //#endregion
1131
+ //#region src/client/host-compat/versions/shared.ts
998
1132
  /**
999
- * Host release detection + feature classification.
1133
+ * Selector + declaration vocabulary shared by every per-version adapter.
1000
1134
  *
1001
- * WHY THIS EXISTS
1002
- * The client context exposes no host version: there is no `hostVersion` /
1003
- * `dshVersion` field on `ctx`, and `window.__DSH_BOOT__.version` is the
1004
- * module-table format tag (`'client'`), not a release. Capability probing alone
1005
- * is also not sufficient, because the obvious probes do not separate the
1006
- * generations — `ctx.sidebarRightTabs` and the native `[data-sidebar-right-panel]`
1007
- * marker both exist on 0.1.5-rc.2 already, so using either as a "0.1.6+" test
1008
- * mis-classifies 0.1.5 as newer.
1135
+ * These are the ANCHORS, not the decisions. What varies between releases (which
1136
+ * element slides, whether the panel needs promoting, which header slot keys
1137
+ * exist) is answered in the version folders; what is stable across all of them
1138
+ * lives here so no adapter has to re-type a host selector and get it subtly wrong.
1009
1139
  *
1010
- * So the release is resolved on the Node half (where the launcher's on-disk
1011
- * layout is visible) and handed to this half through the `read` RPC payload.
1012
- * This module stores that verdict and turns it into the feature flags the rest
1013
- * of the plugin gates on. When the release cannot be determined the flags fall
1014
- * back to capability probing, never to a guess.
1140
+ * Verified against the harness release tags (dsh-v0.1.5-rc.2 … dsh-v0.1.7-rc.1),
1141
+ * not inferred:
1142
+ * · `[data-sidebar-right-panel]` — emitted by `ui-sidebar-right/shell/SidebarRight.tsx`
1143
+ * on every release in range, with values `push` | `fullscreen`.
1144
+ * · `[data-dockkit-host]` / `[data-dockkit-empty]` — emitted ONLY by
1145
+ * `ui-dockkit/components/TabLayout.tsx`, a file that first appears at
1146
+ * 0.1.7-alpha.1. Dockkit itself EXISTS earlier (its `TabMenu` carries
1147
+ * `[data-dockkit-tab-menu]` on every release), so only these two attributes
1148
+ * are the 0.1.7 marker — never test "is dockkit installed".
1149
+ * · `[data-dsh-bottom-panel]` — the optional dsh-better-sidebar workbench, not
1150
+ * a host element at all. It is a single element on every release, which is
1151
+ * why it needs no version arm.
1152
+ * · `[data-plugin-panel]` / `[data-plugin-scope] > ul` — emitted by
1153
+ * `ui-plugin-manager/src/client/PluginManagerPage.tsx`. That package is NEW AT
1154
+ * 0.1.6-alpha.2: `git ls-tree` shows it absent from 0.1.5-rc.2, 0.1.5-rc.3 and
1155
+ * 0.1.6-alpha.1, where the plugin list lived in `ui-settings-plugin-inventory`
1156
+ * under `[data-plugin-scope]` alone — no `[data-plugin-panel]`, and no card
1157
+ * `ul` in that group either. From alpha.2 on, the group markup (`section` →
1158
+ * `div.groupHead` + `ul.cards`) is byte-identical through 0.1.7-rc.1, so the
1159
+ * ONE rule below serves all four tags that emit the page.
1015
1160
  *
1016
1161
  * @module
1017
1162
  */
1018
- let current = {
1019
- version: null,
1020
- generation: "unknown"
1163
+ /** The host's own right Sidebar wrapper. */
1164
+ const PANEL_WRAPPER = "[data-sidebar-right-panel]";
1165
+ /** The same wrapper in its fullscreen state; the host raises its own z-index to 40. */
1166
+ const PANEL_FULLSCREEN = "[data-sidebar-right-panel=\"fullscreen\"]";
1167
+ /** The docked children a 0.1.7+ panel slides — the panel itself stays put. */
1168
+ const DOCKKIT_SLIDERS = "[data-sidebar-right-panel] [data-dockkit-host=\"dock\"],[data-sidebar-right-panel] [data-dockkit-empty]";
1169
+ /** The third-party workbench panel, one element on every release. */
1170
+ const BETTER_SIDEBAR_PANEL = "[data-dsh-bottom-panel]";
1171
+ /** Every surface the panel opacity slider owns. Layout-neutral token re-scope. */
1172
+ const PANEL_SURFACES = `${BETTER_SIDEBAR_PANEL},${PANEL_WRAPPER}`;
1173
+ /** DOM-shape test for "this is NOT a 0.1.7-style frame". Used ONLY by the
1174
+ * unresolved adapter, where the DOM is the authority because the release is not. */
1175
+ const WITHOUT_DOCKKIT_FRAME = ":not(:has([data-dockkit-host],[data-dockkit-empty]))";
1176
+ /** The panel blur declaration pair. Both prefixes because Safari needs the
1177
+ * `-webkit-` one and the cascade must not let them disagree. */
1178
+ function panelBlurRule(selectors) {
1179
+ return `${selectors}{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}`;
1180
+ }
1181
+ /**
1182
+ * The plugin-manager page's frosted card block, for the releases that ship it.
1183
+ *
1184
+ * `设置 → 插件` lists each group's plugins in a `ul` that has NO surface of its
1185
+ * own — measured live on 0.1.7-alpha.1: a transparent flex column, 2px gaps, each
1186
+ * `li` transparent with a 12px radius. With the settings surfaces faded, the whole
1187
+ * table floats straight on the wallpaper. This frames each group's list the way the
1188
+ * composer capsule is framed: a rounded block on an `::before` underlay.
1189
+ *
1190
+ * Which releases emit the page decides who gets this string — see `pluginPageRule`
1191
+ * in each version folder.
1192
+ *
1193
+ * BINDING, and why it is not just "put it in the dialog": the block paints from
1194
+ * `--dsh-any-bg-settings-surface` and frosts from `--dsh-any-blur-settings`, i.e.
1195
+ * the settings-opacity / settings-blur pair. On 0.1.7 the page is a child of
1196
+ * `centerCol`, NOT of the settings dialog, so `SETTINGS_STYLE_RULE`'s token
1197
+ * re-scope never reaches it and the list would otherwise follow the homepage card
1198
+ * alpha; reading the plugin-owned variables off `:root` puts them under the slider
1199
+ * that owns this page. The `var(--dsw-alias-bg-layer-2)` fallback covers the window
1200
+ * before the first `applySettingsOverrides` write.
1201
+ *
1202
+ * The frost rides an underlay because backdrop-filter must never sit directly on a
1203
+ * host part (containing block + backdrop root), and the `isolation` is what keeps
1204
+ * the underlay's `z-index:-1` inside the block instead of escaping behind the frame.
1205
+ * The `:has([role="dialog"])` valve matters here more than anywhere else: every row
1206
+ * carries a `plugins.item` slot that third-party plugins fill with their own
1207
+ * controls, so a dialog can legitimately open inside the list.
1208
+ */
1209
+ const PLUGIN_PAGE_FROST_RULE = "[data-plugin-panel] [data-plugin-scope]>ul{position:relative;isolation:isolate;padding:8px;border-radius:14px}[data-plugin-panel] [data-plugin-scope]>ul:has([role=\"dialog\"]){isolation:auto}[data-plugin-panel] [data-plugin-scope]>ul::before{content:\"\";position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));-webkit-backdrop-filter:var(--dsh-any-blur-settings,none);backdrop-filter:var(--dsh-any-blur-settings,none)}";
1210
+ /** Session-header slot anchors present on EVERY release in range. The slot
1211
+ * renderer stamps `data-slot="<key>"` unconditionally (`ui-renderer/scoped-slots.tsx`),
1212
+ * and all five keys are registered from 0.1.5-rc.2 through 0.1.7-rc.1 —
1213
+ * verified per tag. Adapters append the keys their own release adds. */
1214
+ const BASE_HEADER_SLOT_KEYS = [
1215
+ "conversation.session.header",
1216
+ "conversation.session.header.actions",
1217
+ "conversation.session.header.utilities",
1218
+ "conversation.session.header.corner",
1219
+ "conversation.session.header.lineage"
1220
+ ];
1221
+ //#endregion
1222
+ //#region src/client/host-compat/versions/v0-1-5-rc-2-3/adapter.ts
1223
+ /**
1224
+ * Adapter for the DSH 0.1.5-rc channel (`0.1.5-rc.2`, `0.1.5-rc.3`).
1225
+ *
1226
+ * Nothing here is inferred from a neighbouring release — each fact below was
1227
+ * checked against the harness tags `dsh-v0.1.5-rc.2` / `dsh-v0.1.5-rc.3`.
1228
+ *
1229
+ * Host shape this line presents:
1230
+ * `ui-sidebar-right/shell/SidebarRight.module.css`
1231
+ * .panel { position:absolute; z-index:10; transform:translateX(100%); visibility:hidden }
1232
+ * .panel[data-sidebar-right-open] { transform:none }
1233
+ * .panel[data-sidebar-right-panel='fullscreen'] { position:fixed; inset:0; z-index:40 }
1234
+ * `ui-dockkit` ships WITHOUT `TabLayout.tsx`, so `[data-dockkit-host]` and
1235
+ * `[data-dockkit-empty]` are never rendered on this line.
1236
+ * `ui-plugin-manager` (the page behind `PLUGIN_PAGE_FROST_RULE`) is not a package
1237
+ * on this line at all, and neither tag emits `[data-plugin-panel]`.
1238
+ *
1239
+ * Consequence for the panel slider: the WRAPPER is what slides, so the
1240
+ * `backdrop-filter` belongs on the wrapper — and because the wrapper sits inside
1241
+ * an animated track with its own stacking context, it must be promoted out of it
1242
+ * or the filter has no wallpaper to sample.
1243
+ *
1244
+ * @module
1245
+ */
1246
+ /** The wrapper carries the slide transform on this line, so it carries the blur. */
1247
+ function panelFragments$3() {
1248
+ return {
1249
+ promotion: `${PANEL_WRAPPER}{position:fixed!important;z-index:26!important}${PANEL_FULLSCREEN}{z-index:40!important}`,
1250
+ blur: panelBlurRule(PANEL_WRAPPER) + panelBlurRule(BETTER_SIDEBAR_PANEL)
1251
+ };
1252
+ }
1253
+ function createAdapter$3(host) {
1254
+ return {
1255
+ id: "0.1.5-rc.2/rc.3",
1256
+ channel: "0.1.5-rc",
1257
+ host,
1258
+ panelFragments: panelFragments$3,
1259
+ pluginPageRule: "",
1260
+ surface: {
1261
+ ownsSidebarGuideSurface: false,
1262
+ headerSlotKeys: BASE_HEADER_SLOT_KEYS
1263
+ }
1264
+ };
1265
+ }
1266
+ //#endregion
1267
+ //#region src/client/host-compat/versions/v0-1-6-alpha-1-2/adapter.ts
1268
+ /**
1269
+ * Adapter for the DSH 0.1.6-alpha channel (`0.1.6-alpha.1`, `0.1.6-alpha.2`).
1270
+ *
1271
+ * Facts checked per tag against `dsh-v0.1.6-alpha.1` / `dsh-v0.1.6-alpha.2` —
1272
+ * deliberately not copied over from the 0.1.5 folder, even where the two lines
1273
+ * turned out to agree. If a future 0.1.6 patch diverges, only this file moves.
1274
+ *
1275
+ * Host shape this line presents:
1276
+ * `SidebarRight.module.css` is unchanged from the 0.1.5 line for the parts that
1277
+ * matter: `.panel` still carries `position:absolute` + `transform:translateX(100%)`,
1278
+ * `data-sidebar-right-open` still clears it with `transform:none`, and the panel
1279
+ * width is still an inline `width` with no `--dsh-sidebar-width` variable.
1280
+ * alpha.2 appends fullscreen window chrome (`.63` windows / `.77` darwin), which
1281
+ * does not move the blur target, AND the `ui-plugin-manager` package — so the
1282
+ * plugin-page frost below is prerelease-gated rather than channel-wide.
1283
+ * `ui-dockkit` still has no `TabLayout.tsx`, so no `[data-dockkit-host]` /
1284
+ * `[data-dockkit-empty]` on this line either.
1285
+ *
1286
+ * So the panel mechanics match 0.1.5: the wrapper slides, the wrapper takes the
1287
+ * blur, and the wrapper needs promoting.
1288
+ *
1289
+ * @module
1290
+ */
1291
+ /** The extra session-header anchor this line introduces: the macOS titlebar work
1292
+ * added `conversation.session.header.leading`. It was deleted again at
1293
+ * 0.1.7-alpha.1 with no shim, so it is unique to this line — which is why the
1294
+ * channel alone is not enough to answer it and the prerelease is read.
1295
+ * `alpha.1` predates it.
1296
+ *
1297
+ * Erring toward "included": a slot key the host never emits simply matches
1298
+ * nothing, whereas a key we omit hides a header area from the plugin for the
1299
+ * whole session. */
1300
+ function hasLeadingHeaderSlot(version) {
1301
+ const pre = prereleaseOf(version);
1302
+ if (pre === null) return true;
1303
+ const m = /^alpha\.(\d+)$/.exec(pre);
1304
+ return m === null || Number(m[1]) >= 2;
1305
+ }
1306
+ /** The other thing this line splits on: `ui-plugin-manager` — the package that
1307
+ * renders `设置 → 插件` and stamps `[data-plugin-panel]` — was CREATED at
1308
+ * alpha.2. Until then the plugin list was a settings tab in
1309
+ * `ui-settings-plugin-inventory`, which carries `[data-plugin-scope]` but no
1310
+ * card list to frame, so alpha.1 has nothing for the frost rule to apply to.
1311
+ *
1312
+ * Same erring-toward-included convention as `hasLeadingHeaderSlot`: the rule's
1313
+ * own `[data-plugin-panel]` prefix means an over-broad arm cannot paint, while
1314
+ * an over-narrow one leaves a page bare for the whole session. */
1315
+ function hasPluginManagerPage(version) {
1316
+ const pre = prereleaseOf(version);
1317
+ if (pre === null) return true;
1318
+ const m = /^alpha\.(\d+)$/.exec(pre);
1319
+ return m === null || Number(m[1]) >= 2;
1320
+ }
1321
+ function panelFragments$2() {
1322
+ return {
1323
+ promotion: `${PANEL_WRAPPER}{position:fixed!important;z-index:26!important}${PANEL_FULLSCREEN}{z-index:40!important}`,
1324
+ blur: panelBlurRule(PANEL_WRAPPER) + panelBlurRule(BETTER_SIDEBAR_PANEL)
1325
+ };
1326
+ }
1327
+ function createAdapter$2(host) {
1328
+ return {
1329
+ id: "0.1.6-alpha.1/alpha.2",
1330
+ channel: "0.1.6-alpha",
1331
+ host,
1332
+ panelFragments: panelFragments$2,
1333
+ pluginPageRule: hasPluginManagerPage(host.version) ? PLUGIN_PAGE_FROST_RULE : "",
1334
+ surface: {
1335
+ ownsSidebarGuideSurface: true,
1336
+ headerSlotKeys: hasLeadingHeaderSlot(host.version) ? [...BASE_HEADER_SLOT_KEYS, "conversation.session.header.leading"] : BASE_HEADER_SLOT_KEYS
1337
+ }
1338
+ };
1339
+ }
1340
+ //#endregion
1341
+ //#region src/client/host-compat/versions/v0-1-7-alpha-1-2-rc-1/adapter.ts
1342
+ /**
1343
+ * Adapter for the DSH 0.1.7-alpha channel (`0.1.7-alpha.1`, `0.1.7-alpha.2`),
1344
+ * re-verified unchanged at `0.1.7-rc.1`.
1345
+ *
1346
+ * This is the line the panel mechanics actually changed on — verified against
1347
+ * `dsh-v0.1.6-alpha.2` → `dsh-v0.1.7-alpha.1`, where `SidebarRight.module.css`
1348
+ * was rewritten (116 lines changed) and `ui-dockkit/src/components/TabLayout.tsx`
1349
+ * was ADDED. Both facts were checked, not assumed.
1350
+ *
1351
+ * `0.1.7-rc.1` was checked the same way, by diffing every package that emits an
1352
+ * anchor this plugin matches (the panel CSS, dockkit, the plugin-manager page, the
1353
+ * slot renderer, the theme tokens): no anchor line moved, and the two UI changes it
1354
+ * carries do not reach us —
1355
+ * · `ui-plugin-manager`: a new `InstallDialog` failure screen plus compatibility
1356
+ * wording. The card block (`section` → `div.groupHead` + `ul.cards`) is
1357
+ * untouched, and the dialog is a body-level `Modal`, so the `:has()` valve of
1358
+ * `PLUGIN_PAGE_FROST_RULE` never even has to fire for it.
1359
+ * · `ui-conversation`: `.titleRow` gains `container-type:inline-size`, which makes
1360
+ * it a stacking context and a containing block. Header slots sit inside that
1361
+ * row, but this plugin reaches them through background/blur tokens, and the
1362
+ * dropdowns they trigger portal to `document.body` (see `header-tag.ts`), so
1363
+ * nothing of ours lands inside the new containment.
1364
+ *
1365
+ * Host shape this line presents:
1366
+ * .panel { position:absolute; --dsh-dockkit-dock-layer:10; pointer-events:none }
1367
+ * — no transform, no visibility, and no z-index of its own any more.
1368
+ * .panel :global([data-dockkit-host='dock']),
1369
+ * .panel :global([data-dockkit-empty]),
1370
+ * .panel :global([data-dockkit-divider]) { transform:translateX(var(--dsh-sidebar-width)); visibility:hidden }
1371
+ * .panel[data-sidebar-right-open] :global(…) { transform:none }
1372
+ * — the DOCKED CHILDREN are what slide.
1373
+ * .panel[data-sidebar-right-panel='fullscreen'] { --dsh-dockkit-dock-layer:40 }
1374
+ * — fullscreen no longer flips the panel to `position:fixed`; it raises a
1375
+ * layer variable, and the width becomes `100vw` on the absolute panel.
1376
+ *
1377
+ * Consequences encoded below:
1378
+ * 1. The blur belongs on the children. On the wrapper it frosted a stationary
1379
+ * frame — "the blur stays put while the sidebar moves".
1380
+ * 2. The promotion must NOT be emitted. `position:fixed` detaches the panel from
1381
+ * the track the host animates, which is the whole reason 2 the blur stopped
1382
+ * travelling. The host's own sheet documents the content root as owning "no
1383
+ * stacking context or transform", so `backdrop-filter` resolves against the
1384
+ * page and samples the wallpaper unaided.
1385
+ * 3. `[data-dockkit-empty]` must be included alongside `[data-dockkit-host]`: a
1386
+ * pane with no tabs renders the empty host instead, and keying on `host`
1387
+ * alone mis-detects that (transient) shape.
1388
+ *
1389
+ * Also true of this line, recorded so the next reader does not re-derive it:
1390
+ * · `conversation.session.header.leading` was DELETED here with no shim, so this
1391
+ * folder deliberately does not list it (see the 0.1.6 folder).
1392
+ * · `--dsw-specific-menu` was severed from `--dsw-alias-bg-layer-3` and became a
1393
+ * literal translucent rgba, plus a `html[data-platform='darwin']` re-override.
1394
+ * The plugin already samples and writes that token directly, so the severance
1395
+ * needs no adapter of its own — but note the consequence: `toRgba()` REPLACES
1396
+ * alpha, so the header slider now overrides the host's own 0.58 rather than
1397
+ * fading an opaque layer. Intended (the slider means "this opacity"), worth
1398
+ * knowing if the 0.1.7 header ever looks too thin.
1399
+ * · `[data-chat-flow]` is stamped on a nested `ChatGroupSeat` content element
1400
+ * too. Checked against every use in this plugin: the stroke group is inherited
1401
+ * so a second match changes nothing, and the view-card discovery takes the
1402
+ * first match, which is still the real flow root. No arm needed.
1403
+ *
1404
+ * @module
1405
+ */
1406
+ function panelFragments$1() {
1407
+ return {
1408
+ promotion: "",
1409
+ blur: panelBlurRule(DOCKKIT_SLIDERS) + panelBlurRule(BETTER_SIDEBAR_PANEL)
1410
+ };
1411
+ }
1412
+ function createAdapter$1(host) {
1413
+ return {
1414
+ id: "0.1.7-alpha.1/alpha.2/rc.1",
1415
+ channel: "0.1.7-alpha",
1416
+ host,
1417
+ panelFragments: panelFragments$1,
1418
+ pluginPageRule: PLUGIN_PAGE_FROST_RULE,
1419
+ surface: {
1420
+ ownsSidebarGuideSurface: true,
1421
+ headerSlotKeys: BASE_HEADER_SLOT_KEYS
1422
+ }
1423
+ };
1424
+ }
1425
+ //#endregion
1426
+ //#region src/client/host-compat/versions/unknown/adapter.ts
1427
+ /**
1428
+ * Stand-in for "we could not resolve the release".
1429
+ *
1430
+ * This is a REAL case, not a theoretical one: a plain `~/.dsh` install has no
1431
+ * launcher layout, so the front-door probe finds neither a manifest nor a
1432
+ * version-shaped directory name. It must not degrade into a guessed channel —
1433
+ * a wrong guess silently restyles the window.
1434
+ *
1435
+ * Instead, the DOM arbitrates. Both panel arms ship, each gated on the shape that
1436
+ * only one release family can present, which is exactly how this plugin behaved
1437
+ * before release detection existed. It is why an unresolved host still gets a
1438
+ * working panel blur.
1439
+ *
1440
+ * The one concession this costs: an engine without `:has()` cannot parse a gated
1441
+ * selector and drops that block, so such a host loses the promotion. A resolved
1442
+ * host does not have that problem — its arm is unconditional.
1443
+ *
1444
+ * @module
1445
+ */
1446
+ function panelFragments() {
1447
+ const wrapper = `${PANEL_WRAPPER}${WITHOUT_DOCKKIT_FRAME}`;
1448
+ return {
1449
+ promotion: `${wrapper}{position:fixed!important;z-index:26!important}${PANEL_FULLSCREEN}${WITHOUT_DOCKKIT_FRAME}{z-index:40!important}`,
1450
+ blur: panelBlurRule(DOCKKIT_SLIDERS) + panelBlurRule(wrapper) + panelBlurRule(BETTER_SIDEBAR_PANEL)
1451
+ };
1452
+ }
1453
+ function createAdapter(host) {
1454
+ return {
1455
+ id: "unknown",
1456
+ channel: "unknown",
1457
+ host,
1458
+ panelFragments,
1459
+ pluginPageRule: PLUGIN_PAGE_FROST_RULE,
1460
+ surface: {
1461
+ ownsSidebarGuideSurface: false,
1462
+ headerSlotKeys: BASE_HEADER_SLOT_KEYS
1463
+ }
1464
+ };
1465
+ }
1466
+ //#endregion
1467
+ //#region src/client/host-compat/versions/registry.ts
1468
+ const ADAPTERS = {
1469
+ "0.1.5-rc": createAdapter$3,
1470
+ "0.1.6-alpha": createAdapter$2,
1471
+ "0.1.7-alpha": createAdapter$1,
1472
+ unknown: createAdapter
1021
1473
  };
1022
- /** False until the server verdict lands; feature gates must not treat "not
1023
- * asked yet" as "old host". */
1474
+ /** Pick the adapter for a resolved host. `host.channel` is already narrowed to
1475
+ * the union by `adoptHostInfo`, so every channel here has a row by construction —
1476
+ * a new channel in `HostChannel` fails to compile until it is added. */
1477
+ function buildAdapter(host) {
1478
+ return ADAPTERS[host.channel](host);
1479
+ }
1480
+ //#endregion
1481
+ //#region src/client/host-compat/release.ts
1482
+ /**
1483
+ * The client-side half of the front door: where the release verdict lands.
1484
+ *
1485
+ * The browser cannot detect the host release — the client context exposes no
1486
+ * version field, and the app manifest that states it is only readable on the node
1487
+ * half. So `src/host-compat/detect.ts` resolves it there and the verdict travels
1488
+ * in the `read` RPC payload (see `client/rpc.ts`, which calls `adoptHostInfo`).
1489
+ *
1490
+ * This module only HOLDS the verdict and tells subscribers when it changes. It
1491
+ * deliberately answers no feature questions — that is `capabilities.ts`, so the
1492
+ * version → behaviour mapping stays in one place.
1493
+ *
1494
+ * @module
1495
+ */
1496
+ let current = UNKNOWN_HOST_INFO;
1497
+ /** False until the server verdict lands. Feature gates must not read "not asked
1498
+ * yet" as "an old host" — see `adapterResolved()`. */
1024
1499
  let resolved = false;
1025
1500
  const subs$1 = /* @__PURE__ */ new Set();
1026
- /** Adopt the verdict delivered by the server's `read` payload. */
1501
+ /** Cached adapter, keyed by the verdict it was built from. Held because React's
1502
+ * `useSyncExternalStore` requires a stable snapshot between notifications. */
1503
+ let adapterCache = null;
1504
+ /** Adopt the verdict delivered by the server's `read` payload.
1505
+ *
1506
+ * The payload's `channel` is IGNORED and re-derived from `version` here: the
1507
+ * two halves are built together, so a disagreeing pair can only mean one of them
1508
+ * is stale, and re-deriving keeps `channelOf` the single definition of what a
1509
+ * channel means. */
1027
1510
  function adoptHostInfo(raw) {
1028
1511
  if (raw === null || typeof raw !== "object") return;
1029
1512
  const h = raw;
1030
- const version = typeof h.version === "string" && h.version.trim() !== "" ? h.version.trim() : null;
1031
- const generation = h.generation === "0.1.5" || h.generation === "0.1.6" || h.generation === "0.1.7" ? h.generation : "unknown";
1032
- const changed = !resolved || current.version !== version || current.generation !== generation;
1033
- current = {
1513
+ const version = typeof h.version === "string" && looksLikeVersion(h.version.trim()) ? h.version.trim() : null;
1514
+ const next = {
1034
1515
  version,
1035
- generation
1516
+ channel: channelOf(version)
1036
1517
  };
1518
+ const changed = !resolved || current.version !== next.version || current.channel !== next.channel;
1519
+ current = next;
1037
1520
  resolved = true;
1038
- if (changed) subs$1.forEach((cb) => cb());
1521
+ if (changed) {
1522
+ adapterCache = null;
1523
+ subs$1.forEach((cb) => cb());
1524
+ }
1039
1525
  }
1040
- /** Subscribe to a verdict landing (or changing). */
1526
+ /** Subscribe to the verdict landing (or changing). */
1041
1527
  function subscribeHostInfo(cb) {
1042
1528
  subs$1.add(cb);
1043
1529
  return () => {
1044
1530
  subs$1.delete(cb);
1045
1531
  };
1046
1532
  }
1047
- /** Ordered generation rank, for "at least this generation" tests. `unknown`
1048
- * has no rank and must be handled by the caller's fallback. */
1049
- const RANK = {
1050
- "0.1.5": 5,
1051
- "0.1.6": 6,
1052
- "0.1.7": 7
1053
- };
1054
- /**
1055
- * Whether the host is at least `gen`. An unknown generation answers `false`:
1056
- * callers treat that as "do not assume the newer behaviour" and use whatever
1057
- * capability probe they own, which is the safe direction for every gate here.
1058
- */
1059
- function hostAtLeast(gen) {
1060
- const rank = RANK[current.generation];
1061
- return rank !== void 0 && rank >= RANK[gen];
1533
+ /** Whether the server verdict has landed at all. */
1534
+ function adapterResolved() {
1535
+ return resolved;
1062
1536
  }
1063
- /**
1064
- * Whether this host's own right Sidebar already carries the appearance page,
1065
- * which makes the plugin's dsh-better-sidebar page a duplicate entry on the same
1066
- * guide surface.
1067
- *
1068
- * `true` — 0.1.6 or newer: the better-sidebar page stands down.
1069
- * `false` — 0.1.5 or an undetermined-but-answered generation: keep it.
1070
- * `null` — the verdict has not landed yet (the probe is a Node round-trip).
1071
- *
1072
- * Callers must treat `null` as "not yet known": register nothing eagerly on a
1073
- * `true`, and arm a withdrawal for a later `true` rather than blocking on it.
1074
- *
1075
- * NOTE: 0.1.5-rc.2 ALSO ships the native right Sidebar (verified: the
1076
- * `ui-sidebar-right` entry is in that release's `dsh-web-app/cordis.patch.yml`),
1077
- * so this gate deliberately keys off the release rather than off the presence of
1078
- * the native sidebar service — the two are not the same fact.
1079
- */
1080
- function suppressesBetterSidebar() {
1081
- if (!resolved) return null;
1082
- if (current.generation === "unknown") return false;
1083
- return hostAtLeast("0.1.6");
1537
+ /** The adapter chosen for the current verdict — `unknown`'s stand-in until the
1538
+ * verdict lands, which is the safe answer: it asserts nothing and lets the DOM
1539
+ * arbitrate. */
1540
+ function rHostAdapter() {
1541
+ adapterCache ??= buildAdapter(current);
1542
+ return adapterCache;
1084
1543
  }
1085
1544
  //#endregion
1086
1545
  //#region src/client/rpc.ts
@@ -1406,6 +1865,149 @@ window.__ModuleLoader__.load({
1406
1865
  }
1407
1866
  }
1408
1867
  //#endregion
1868
+ //#region src/client/host-compat/capabilities.ts
1869
+ /**
1870
+ * The plugin's ONE vocabulary for host-dependent decisions.
1871
+ *
1872
+ * Everything downstream asks a QUESTION ("who owns the guide surface?", "which CSS
1873
+ * arm applies?") and gets an answer from the current adapter. No consumer names a
1874
+ * release. That is what keeps the per-version folders honest: when 0.1.8 needs a
1875
+ * new behaviour, a consumer changes by reading a new adapter field, not by growing
1876
+ * another version comparison.
1877
+ *
1878
+ * @module
1879
+ */
1880
+ function sidebarGuideOwner() {
1881
+ if (!adapterResolved()) return "pending";
1882
+ return rHostAdapter().surface.ownsSidebarGuideSurface ? "host" : "plugin";
1883
+ }
1884
+ /** The current adapter, for callers that need more than one field from it (and to
1885
+ * keep the snapshot identity stable across a single render pass). */
1886
+ function hostAdapter() {
1887
+ return rHostAdapter();
1888
+ }
1889
+ //#endregion
1890
+ //#region src/client/header-tag.ts
1891
+ /**
1892
+ * Header-surface tagging.
1893
+ *
1894
+ * WHY A RUNTIME TAG IS NEEDED
1895
+ * The session-header dropdowns (open-in-app, session-log, Agent Team, the job
1896
+ * list, the subagent lineage tree) render their popover through the Menu/tree
1897
+ * primitives, which PORTAL the list to `document.body`. A portaled list is not a
1898
+ * DOM descendant of the header — no ancestor selector can reach it — and the
1899
+ * primitive emits no `id`/`aria-controls` back-link to its anchor either. Pure
1900
+ * CSS class matching was the remaining option, but 0.1.7 broke the shapes this
1901
+ * plugin relied on: open-in-app moved from in-place to `portal` (so the old
1902
+ * `:not(_portal)` discriminator no longer selected it), and the session row menu
1903
+ * became a dynamic `sidebar.workspaces.session.menu.item` slot whose rows are no
1904
+ * longer guaranteed to include a destructive one.
1905
+ *
1906
+ * THE SIGNAL
1907
+ * The slot renderer stamps every registered slot with `data-slot="<key>"`
1908
+ * (ui-renderer `scoped-slots.tsx`). Those anchors live in the header subtree, so
1909
+ * the plugin can detect when a header trigger is expanded — which keys identify
1910
+ * the header is a per-release fact and comes from the host adapter, not from a
1911
+ * hardcoded list here.
1912
+ *
1913
+ * WHY NOT GEOMETRY
1914
+ * A tempting link is the inline position the primitive writes on a portaled list
1915
+ * (`style="left: …; top: …"`). It was tried and REJECTED: an `align="end"` list
1916
+ * is flush to the trigger's right edge while the trigger itself sits at the far
1917
+ * right of the header, so the two boxes often do not overlap horizontally (in
1918
+ * the reported DOM the list is at `left:1012` with the trigger near x=1400). Any
1919
+ * purely geometric proximity test therefore misses real header menus.
1920
+ *
1921
+ * THE ACTUAL RULE (structural, no geometry, no timing)
1922
+ * A header dropdown can only be open while its trigger reports
1923
+ * `aria-expanded="true"`, and opening one is what mounts the popover. So a
1924
+ * popover tag is maintained as: "there is at least one expanded header trigger,
1925
+ * AND this popover is not owned by another group". Popovers are only ever
1926
+ * tagged/released on a change of that combined state, which makes the tag
1927
+ * self-consistent and idempotent.
1928
+ *
1929
+ * The one hazard is a popover that is open for an unrelated reason (a composer
1930
+ * menu, a hover card) *while* a header trigger stays expanded. Such a popover
1931
+ * is not a candidate: it is excluded by `ownsForeignSurface`, which skips any
1932
+ * popover carrying markers of a different slider group. In practice header
1933
+ * dropdowns are transient (opening one closes the others), so this is a guard
1934
+ * rather than the common path.
1935
+ *
1936
+ * @module
1937
+ */
1938
+ /** Marker attribute the stylesheet keys on. */
1939
+ const HEADER_POPOVER_ATTR = "data-dsh-any-header-popover";
1940
+ /** Portaled popovers a header trigger can own. */
1941
+ const POPOVER_SELECTOR = "[role=\"menu\"], [role=\"tree\"], [role=\"dialog\"]";
1942
+ /** Which session-header slot anchors to watch — supplied by the host adapter,
1943
+ * because the set is not the same on every release: 0.1.6-alpha.2 registers a
1944
+ * sixth (`.leading`) that neither the line before nor the line after has.
1945
+ * Memoised on the array identity, which the adapter keeps stable until the
1946
+ * release verdict lands. */
1947
+ let slotAnchorCache = null;
1948
+ function headerSlotAnchors() {
1949
+ const keys = hostAdapter().surface.headerSlotKeys;
1950
+ if (slotAnchorCache === null || slotAnchorCache.keys !== keys) slotAnchorCache = {
1951
+ keys,
1952
+ selector: keys.map((key) => `[data-slot="${key}"]`).join(",")
1953
+ };
1954
+ return slotAnchorCache.selector;
1955
+ }
1956
+ /** Whether any header slot anchor has an expanded trigger. */
1957
+ function headerTriggerOpen() {
1958
+ for (const anchor of document.querySelectorAll(headerSlotAnchors())) if (anchor.querySelector("[aria-expanded=\"true\"]") !== null) return true;
1959
+ return false;
1960
+ }
1961
+ /** Surfaces that belong to a different slider group and must never be claimed.
1962
+ * `_denseList` without `_portal` is the in-place card-side dense menu, an
1963
+ * `_ioCard`/`_menu`-classed body-level list belongs elsewhere, and a modal is
1964
+ * its own surface. */
1965
+ function ownsForeignSurface(el) {
1966
+ if (el.getAttribute("aria-modal") === "true") return true;
1967
+ return el.parentElement !== document.body;
1968
+ }
1969
+ let observer = null;
1970
+ let raf = null;
1971
+ /** Recompute tags from the current open state. */
1972
+ function refreshHeaderPopovers() {
1973
+ const open = headerTriggerOpen();
1974
+ const candidates = [...document.querySelectorAll(POPOVER_SELECTOR)].filter((el) => !ownsForeignSurface(el));
1975
+ if (!open) {
1976
+ for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
1977
+ return;
1978
+ }
1979
+ for (const el of candidates) el.setAttribute(HEADER_POPOVER_ATTR, "");
1980
+ for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) if (!candidates.includes(el)) el.removeAttribute(HEADER_POPOVER_ATTR);
1981
+ }
1982
+ function schedule() {
1983
+ if (raf !== null) return;
1984
+ raf = window.requestAnimationFrame(() => {
1985
+ raf = null;
1986
+ refreshHeaderPopovers();
1987
+ });
1988
+ }
1989
+ /** Start tagging. Returns a teardown that stops observing and clears markings. */
1990
+ function startHeaderPopoverTagging() {
1991
+ if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
1992
+ refreshHeaderPopovers();
1993
+ observer = new MutationObserver(schedule);
1994
+ observer.observe(document.body, {
1995
+ childList: true,
1996
+ subtree: true,
1997
+ attributes: true,
1998
+ attributeFilter: ["aria-expanded"]
1999
+ });
2000
+ return () => {
2001
+ observer?.disconnect();
2002
+ observer = null;
2003
+ if (raf !== null) {
2004
+ window.cancelAnimationFrame(raf);
2005
+ raf = null;
2006
+ }
2007
+ for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
2008
+ };
2009
+ }
2010
+ //#endregion
1409
2011
  //#region src/client/utils/image.ts
1410
2012
  /**
1411
2013
  * Read a chosen image file as a data URL WITHOUT re-encoding: the original
@@ -2829,13 +3431,10 @@ window.__ModuleLoader__.load({
2829
3431
  }
2830
3432
  const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
2831
3433
  const SETTINGS_STYLE_RULE = `${SETTINGS_PANEL_SEL}{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none);--dsw-alias-bg-layer-1:var(--dsh-any-bg-settings-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-bg-settings-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-bg-settings-layer-3)}${SETTINGS_PANEL_SEL} .dab-card{backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}`;
2832
- const POPOVER_BLUR_RULE = "[role=\"menu\"]:not([data-dockkit-tab-menu]),[role=\"listbox\"],body > [role=\"tree\"],body > [role=\"dialog\"]:not([aria-modal=\"true\"]){backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}";
2833
- const INPUT_BLUR_RULE = "[data-composer-card]{position:relative;isolation:isolate}[data-composer-card]::before{content:\"\";position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{--dsw-specific-menu:var(--dsh-any-op-menu-cordis)!important}";
2834
3434
  function applyInputBlur(px) {
2835
3435
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
2836
3436
  else document.documentElement.style.removeProperty("--dsh-any-input-blur");
2837
3437
  }
2838
- const MOBILE_HEADER_RULE = ".dsh-mobile-app-header{background:var(--dsh-any-op-bg,transparent)!important;backdrop-filter:var(--dsh-any-part-blur-global,none);-webkit-backdrop-filter:var(--dsh-any-part-blur-global,none)}";
2839
3438
  /** Mirror of the bg-part blur on :root. --dsh-any-part-blur is element-scoped
2840
3439
  * to the columns, so surfaces outside their subtree (the mobile header, or
2841
3440
  * third-party styles) can never inherit it. */
@@ -2843,16 +3442,32 @@ window.__ModuleLoader__.load({
2843
3442
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-part-blur-global", `blur(${px}px)`);
2844
3443
  else document.documentElement.style.removeProperty("--dsh-any-part-blur-global");
2845
3444
  }
2846
- const PANEL_TOKEN_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{--dsw-alias-bg-base:var(--dsh-any-panel-bg-base);--dsw-alias-bg-layer-1:var(--dsh-any-panel-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-panel-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-panel-layer-3)}";
2847
- const NO_DOCKKIT = ":not(:has([data-dockkit-host],[data-dockkit-empty]))";
2848
- const PANEL_BLUR_RULE = `[data-sidebar-right-panel] [data-dockkit-host="dock"],[data-sidebar-right-panel] [data-dockkit-empty]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}[data-sidebar-right-panel]${NO_DOCKKIT}{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}[data-dsh-bottom-panel]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}`;
2849
- const PANEL_PROMOTION_RULE = `[data-sidebar-right-panel]${NO_DOCKKIT}{position:fixed!important;z-index:26!important}[data-sidebar-right-panel="fullscreen"]${NO_DOCKKIT}{z-index:40!important}`;
2850
- const HEADER_POPOVER_RULE = "[data-dsh-any-header-popover][role],ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"]),[role=\"menu\"][class*=\"_denseList\"]:not([class*=\"_portal\"]),[role=\"tree\"][class*=\"_menu\"]{-webkit-backdrop-filter:var(--dsh-any-blur-header,none);backdrop-filter:var(--dsh-any-blur-header,none)}";
2851
- const EXEMPT_DEFAULT_RULE = "[role=\"dialog\"][aria-modal=\"true\"]:not([aria-labelledby]){backdrop-filter:none;-webkit-backdrop-filter:none}[role=\"menu\"]:has([class*=\"_danger\"]){backdrop-filter:none;-webkit-backdrop-filter:none;-webkit-text-stroke-width:0}";
2852
- /** Selector the dynamic header token rule targets — kept in sync with the
2853
- * static HEADER_POPOVER_RULE selector list (specificity equal or higher than
2854
- * the card group's `[role="menu"]:not(...)` so it wins). */
2855
- const HEADER_TOKEN_SELECTOR = "[data-dsh-any-header-popover][role],ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"]),[role=\"menu\"][class*=\"_denseList\"]:not([class*=\"_portal\"]),[role=\"tree\"][class*=\"_menu\"]";
3445
+ const PANEL_TOKEN_RULE = `${PANEL_SURFACES}{--dsw-alias-bg-base:var(--dsh-any-panel-bg-base);--dsw-alias-bg-layer-1:var(--dsh-any-panel-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-panel-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-panel-layer-3)}`;
3446
+ /** The header dropdown surfaces, spelled ONCE. Three rules consume this set (the
3447
+ * static blur rule, the dynamic token rule and the stroke group), and every one
3448
+ * of them drifting by a single attribute re-introduces the bug where a header
3449
+ * menu keeps the card slider's look instead of its own.
3450
+ *
3451
+ * Strategy 1 is the runtime tag (`header-tag.ts`); strategies 2 are class-shape
3452
+ * fallbacks for the first paint before the tag observer runs. */
3453
+ const HEADER_TAG_SELECTOR = `[${HEADER_POPOVER_ATTR}]`;
3454
+ const HEADER_SURFACES = [
3455
+ HEADER_TAG_SELECTOR,
3456
+ "ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"])",
3457
+ `[role="menu"][class*="_denseList"]:not([class*="_portal"])`,
3458
+ "[role=\"tree\"][class*=\"_menu\"]"
3459
+ ];
3460
+ /** SPECIFICITY IS LOAD-BEARING HERE. The card group's rule is
3461
+ * `[role="menu"]:not([data-dockkit-tab-menu])` — specificity (0,2,0) — so a bare
3462
+ * `[data-dsh-any-header-popover]` (0,1,0) LOSES to it no matter how late it
3463
+ * appears in the sheet. That is exactly how the tagged open-in-app menu kept
3464
+ * taking the card blur. `[role]` is a no-op presence test that raises only the
3465
+ * tagged arm to (0,2,0); the other three already qualify at (0,2,0)+. */
3466
+ const HEADER_SURFACES_PADDED = [`${HEADER_TAG_SELECTOR}[role]`, ...HEADER_SURFACES.slice(1)];
3467
+ const HEADER_POPOVER_RULE = `${HEADER_SURFACES_PADDED.join(",")}{-webkit-backdrop-filter:var(--dsh-any-blur-header,none);backdrop-filter:var(--dsh-any-blur-header,none)}`;
3468
+ /** Selector the dynamic header token rule targets — the same set as the static
3469
+ * HEADER_POPOVER_RULE above, so the two can never drift apart. */
3470
+ const HEADER_TOKEN_SELECTOR = HEADER_SURFACES_PADDED.join(",");
2856
3471
  /** Header-popover surfaces: retint the menu token and (re)write the blur.
2857
3472
  * Mirrors applyProduced: the static rule above never changes, so a drag only
2858
3473
  * rewrites the blur variable and this one dynamic rule. */
@@ -2932,8 +3547,8 @@ window.__ModuleLoader__.load({
2932
3547
  `[data-chat-flow]{-webkit-text-stroke:var(--dsh-any-stroke-chat-w,0px) var(--dsh-any-stroke-chat-c,transparent);paint-order:stroke fill}`,
2933
3548
  `[data-conversation-composer-overlay]{-webkit-text-stroke:var(--dsh-any-stroke-trajectory-w,0px) var(--dsh-any-stroke-trajectory-c,transparent);paint-order:stroke fill}`,
2934
3549
  "[data-code-block-content] pre,[data-code-block-content],[data-code-block-banner],[data-composer-chip],:not(pre)>code,[data-changed-files],[data-terminal],[data-read],[data-context-injection-body],[data-search],[data-web],[class*=\"_ioCard\"]{-webkit-text-stroke:var(--dsh-any-stroke-produced-w,0px) var(--dsh-any-stroke-produced-c,transparent);paint-order:stroke fill}",
2935
- `[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-text-stroke:var(--dsh-any-stroke-panel-w,0px) var(--dsh-any-stroke-panel-c,transparent);paint-order:stroke fill}`,
2936
- "[data-dsh-any-header-popover],ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"]),[role=\"menu\"][class*=\"_denseList\"]:not([class*=\"_portal\"]),[role=\"tree\"][class*=\"_menu\"]{-webkit-text-stroke:var(--dsh-any-stroke-header-w,0px) var(--dsh-any-stroke-header-c,transparent);paint-order:stroke fill}",
3550
+ `${PANEL_SURFACES}{-webkit-text-stroke:var(--dsh-any-stroke-panel-w,0px) var(--dsh-any-stroke-panel-c,transparent);paint-order:stroke fill}`,
3551
+ `${HEADER_SURFACES.join(",")}{-webkit-text-stroke:var(--dsh-any-stroke-header-w,0px) var(--dsh-any-stroke-header-c,transparent);paint-order:stroke fill}`,
2937
3552
  "[data-chat-flow] [role=\"status\"],[data-chat-flow] [data-turn-process],[data-chat-flow] [data-text-shimmer],[data-chat-flow] [style*=\"--dsh-text-shimmer-spread\"]{-webkit-text-stroke-width:0!important;paint-order:normal!important}",
2938
3553
  `svg{-webkit-text-stroke-width:0!important;paint-order:normal!important}`,
2939
3554
  `::placeholder{-webkit-text-stroke-width:0!important}`
@@ -3151,7 +3766,6 @@ window.__ModuleLoader__.load({
3151
3766
  function applySettingsOverrides(op) {
3152
3767
  applyLayerOverrides("--dsh-any-bg-settings", op, true);
3153
3768
  }
3154
- const TRAJECTORY_STYLE_RULE = "[data-conversation-composer-overlay]{--dsw-alias-bg-layer-1:var(--dsh-any-traj-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-traj-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-traj-layer-3)}";
3155
3769
  function applyTrajectoryOverrides(op) {
3156
3770
  applyLayerOverrides("--dsh-any-traj", op, false);
3157
3771
  }
@@ -3257,7 +3871,21 @@ window.__ModuleLoader__.load({
3257
3871
  let rightEl = null;
3258
3872
  const PART_BLUR_CLASS = "dab-part-blur";
3259
3873
  const PART_UNDERLAY_CLASS = "dab-part-underlay";
3260
- const PART_BLUR_RULE = `${PART_BLUR_CLASS}{isolation:isolate}.${PART_UNDERLAY_CLASS}{position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;backdrop-filter:var(--dsh-any-part-blur,none);-webkit-backdrop-filter:var(--dsh-any-part-blur,none)}`;
3874
+ /** The AppFrame's main-bg clear-out, as a class rather than an inline write.
3875
+ *
3876
+ * WHY NOT INLINE (measured on 0.1.7): the frame's own translucent
3877
+ * `--dsw-alias-bg-base` is what hides the wallpaper, so `applyPartOpacities` has
3878
+ * to clear it. Written as `frameEl.style.background = 'transparent'` the clear
3879
+ * survived a settings change but not a sidebar open/close — the host owns that
3880
+ * element's `style` too (it animates `grid-template-columns`), and while it
3881
+ * re-asserts its own background the frame snaps back to an opaque
3882
+ * `rgb(200,207,218)`: wallpaper gone, and every frost on top of it flattened,
3883
+ * because a backdrop-filter with nothing behind it has nothing to blur. A class
3884
+ * rule with `!important` sits outside the host's `style` writes entirely, so the
3885
+ * clear cannot be re-clobbered and there is nothing to re-apply. */
3886
+ const PART_FRAME_CLASS = "dab-frame-clear";
3887
+ const FRAME_CLEAR_RULE = `.${PART_FRAME_CLASS}{background:transparent!important}`;
3888
+ const PART_BLUR_RULE = `.${PART_BLUR_CLASS}{isolation:isolate}.${PART_BLUR_CLASS}:has([role="dialog"]){isolation:auto}.${PART_UNDERLAY_CLASS}{position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;backdrop-filter:var(--dsh-any-part-blur,none);-webkit-backdrop-filter:var(--dsh-any-part-blur,none)}`;
3261
3889
  let partBlurStyleEl = null;
3262
3890
  function ensurePartBlurStyle() {
3263
3891
  if (partBlurStyleEl?.isConnected) return;
@@ -3338,7 +3966,8 @@ window.__ModuleLoader__.load({
3338
3966
  discoverParts();
3339
3967
  if (frameEl === null) return;
3340
3968
  const base = tokens["--dsw-alias-bg-base"];
3341
- frameEl.style.background = "transparent";
3969
+ frameEl.classList.add(PART_FRAME_CLASS);
3970
+ if (frameEl.style.background !== "") frameEl.style.removeProperty("background");
3342
3971
  if (centerEl !== null) centerEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
3343
3972
  if (rightEl !== null) rightEl.style.background = "";
3344
3973
  }
@@ -3517,6 +4146,7 @@ window.__ModuleLoader__.load({
3517
4146
  if (cached !== null && centerEl.contains(cached)) return cached;
3518
4147
  viewTargets[idx] = null;
3519
4148
  if (spec.fallback !== true) return null;
4149
+ if (centerEl.querySelector("[data-chat-flow],[data-conversation-scroll],[data-composer-seat],[data-conversation-composer-overlay]") === null) return null;
3520
4150
  if (centerEl.querySelector("[data-conversation-scroll]") !== null) return null;
3521
4151
  if (spec.opacity() <= 0 && spec.blur() <= 0) return null;
3522
4152
  let best = null;
@@ -3535,6 +4165,7 @@ window.__ModuleLoader__.load({
3535
4165
  if (el.clientHeight < centerEl.clientHeight * .5) continue;
3536
4166
  if (el.clientHeight > (best?.clientHeight ?? 0)) best = el;
3537
4167
  }
4168
+ if (best !== null && best.closest(SETTINGS_PANEL_SEL) !== null) best = null;
3538
4169
  const refined = best !== null ? refineMessageColumn(best) : null;
3539
4170
  viewTargets[idx] = refined;
3540
4171
  return refined;
@@ -4075,7 +4706,7 @@ window.__ModuleLoader__.load({
4075
4706
  setBlur(sidebarEl, 0);
4076
4707
  setBlur(centerEl, 0);
4077
4708
  setBlur(rightEl, 0);
4078
- if (frameEl !== null) frameEl.style.removeProperty("background");
4709
+ if (frameEl !== null) frameEl.classList.remove(PART_FRAME_CLASS);
4079
4710
  if (centerEl !== null) centerEl.style.removeProperty("background");
4080
4711
  if (rightEl !== null) rightEl.style.removeProperty("background");
4081
4712
  stopWatchingParts();
@@ -4089,6 +4720,70 @@ window.__ModuleLoader__.load({
4089
4720
  if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
4090
4721
  }
4091
4722
  //#endregion
4723
+ //#region src/client/host-compat/styles.ts
4724
+ /**
4725
+ * Assembly of the plugin's one static stylesheet.
4726
+ *
4727
+ * Until now the sheet was a single template literal in the client entry with
4728
+ * thirteen constants bolted together in whatever order they were added. That
4729
+ * order is load-bearing and was documented in three separate comment blocks in
4730
+ * `wallpaper.ts` — so it is encoded here instead, in one list, with the reason
4731
+ * next to each entry.
4732
+ *
4733
+ * What the release can change lives in one contiguous slot: the panel group
4734
+ * (which element carries the frost) and the plugin-manager page (whether this
4735
+ * release ships that page at all). Everything outside the slot is host-agnostic by
4736
+ * construction — it targets attributes every release in range emits.
4737
+ *
4738
+ * The sheet is rebuilt when the release verdict lands. It has to be: `apply` runs
4739
+ * synchronously while the verdict is a Node round-trip, so the FIRST sheet is
4740
+ * always the unresolved, DOM-arbitrated one. That is the correct interim state,
4741
+ * and swapping in the resolved arm a moment later is what removes the `:has()`
4742
+ * dependency from hosts we do know the answer for.
4743
+ *
4744
+ * @module
4745
+ */
4746
+ /** The plugin's own dark-mode value on the body attribute the host sets. Scoping
4747
+ * the gradient to it avoids matching the host's own theme attribute. */
4748
+ const DARK_GRADIENT = "body[data-ds-dark-theme=\"dsh-any-background\"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}";
4749
+ /**
4750
+ * The order contract. Later entries override earlier ones at equal specificity,
4751
+ * which several groups rely on:
4752
+ * · HEADER_POPOVER_RULE after POPOVER_BLUR_RULE — a surface moving from the card
4753
+ * group to the header group must win, not merely be listed twice.
4754
+ * · PRODUCED_RULE / STROKE_RULE after the blur groups — they shield their
4755
+ * surfaces from an inherited slider.
4756
+ * · EXEMPT_DEFAULT_RULE last of the token groups — an exemption has to land
4757
+ * after whatever it exempts.
4758
+ * · PLACEHOLDER_RULE genuinely last — `::placeholder` resets must survive every
4759
+ * group above.
4760
+ */
4761
+ function buildStaticStyles() {
4762
+ const adapter = rHostAdapter();
4763
+ const panel = adapter.panelFragments();
4764
+ return DARK_GRADIENT + FRAME_CLEAR_RULE + SETTINGS_STYLE_RULE + "[role=\"menu\"]:not([data-dockkit-tab-menu]),[role=\"listbox\"],body > [role=\"tree\"],body > [role=\"dialog\"]:not([aria-modal=\"true\"]){backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}[data-conversation-composer-overlay]{--dsw-alias-bg-layer-1:var(--dsh-any-traj-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-traj-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-traj-layer-3)}[data-composer-card]{position:relative;isolation:isolate}[data-composer-card]::before{content:\"\";position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{--dsw-specific-menu:var(--dsh-any-op-menu-cordis)!important}.dsh-mobile-app-header{background:var(--dsh-any-op-bg,transparent)!important;backdrop-filter:var(--dsh-any-part-blur-global,none);-webkit-backdrop-filter:var(--dsh-any-part-blur-global,none)}" + PANEL_TOKEN_RULE + panel.promotion + panel.blur + adapter.pluginPageRule + PRODUCED_RULE + STROKE_RULE + HEADER_POPOVER_RULE + "[role=\"dialog\"][aria-modal=\"true\"]:not([aria-labelledby]){backdrop-filter:none;-webkit-backdrop-filter:none}[role=\"menu\"]:has([class*=\"_danger\"]){backdrop-filter:none;-webkit-backdrop-filter:none;-webkit-text-stroke-width:0}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}";
4765
+ }
4766
+ /** Append the plugin's stylesheet and keep it matched to the host.
4767
+ * Returns a teardown for the plugin's effect scope. */
4768
+ function mountStaticStyles() {
4769
+ const el = document.createElement("style");
4770
+ el.dataset.plugin = "dsh-any-background";
4771
+ el.textContent = buildStaticStyles();
4772
+ document.head.appendChild(el);
4773
+ let last = el.textContent;
4774
+ const unsubscribe = subscribeHostInfo(() => {
4775
+ const next = buildStaticStyles();
4776
+ if (next !== last) {
4777
+ last = next;
4778
+ el.textContent = next;
4779
+ }
4780
+ });
4781
+ return () => {
4782
+ unsubscribe();
4783
+ el.parentNode?.removeChild(el);
4784
+ };
4785
+ }
4786
+ //#endregion
4092
4787
  //#region src/client/utils/video.ts
4093
4788
  /**
4094
4789
  * Video-background helpers: capturing a single frame as a JPEG snapshot.
@@ -4156,7 +4851,11 @@ window.__ModuleLoader__.load({
4156
4851
  * column: the host provides the modal chrome (backdrop, centering, closing).
4157
4852
  * These classes style only the embedded shell; transient fixed layers (toast,
4158
4853
  * color picker, background editor) escape through Portals on <html>. */
4159
- .dab-root{position:relative;color:var(--dsw-alias-label-primary);animation:dab-fade-in .35s ease both;container-type:inline-size;display:flex;flex-direction:column;align-items:center;width:100%;min-width:0;--dab-mono:ui-monospace,"Cascadia Mono","SF Mono",Consolas,"Courier New",monospace}
4854
+ .dab-root{position:relative;box-sizing:border-box;color:var(--dsw-alias-label-primary);animation:dab-fade-in .35s ease both;container-type:inline-size;display:flex;flex-direction:column;align-items:center;width:100%;min-width:0;--dab-mono:ui-monospace,"Cascadia Mono","SF Mono",Consolas,"Courier New",monospace}
4855
+ /* The root needs its OWN border-box, not just the descendants' below: the
4856
+ * sidebar surface puts 12px of padding on it, and with the default content-box
4857
+ * that padding landed OUTSIDE the width:100%, so the page came out 24px wider
4858
+ * than the pane and the host clipped both edges off. */
4160
4859
  .dab-root *,.dab-root *::before,.dab-root *::after{box-sizing:border-box}
4161
4860
  .dab-root button{font-family:inherit}
4162
4861
 
@@ -8317,11 +9016,11 @@ window.__ModuleLoader__.load({
8317
9016
  ctx.inject?.(["betterSidebar"], (scope) => {
8318
9017
  const service = scope.betterSidebar;
8319
9018
  if (service === void 0) return;
8320
- const suppress = suppressesBetterSidebar();
8321
- if (suppress === true) return;
9019
+ const owner = sidebarGuideOwner();
9020
+ if (owner === "host") return;
8322
9021
  let disposeTab = null;
8323
- const unsubscribeHost = suppress === null ? subscribeHostInfo(() => {
8324
- if (suppressesBetterSidebar() === true) {
9022
+ const unsubscribeHost = owner === "pending" ? subscribeHostInfo(() => {
9023
+ if (sidebarGuideOwner() === "host") {
8325
9024
  disposeTab?.();
8326
9025
  disposeTab = null;
8327
9026
  unsubscribeHost();
@@ -8352,10 +9051,10 @@ window.__ModuleLoader__.load({
8352
9051
  //#endregion
8353
9052
  //#region src/client/sidebar/native-tab.tsx
8354
9053
  /**
8355
- * dsh-any-background — native right-Sidebar integration (DSH 0.1.6+).
9054
+ * dsh-any-background — native right-Sidebar integration.
8356
9055
  *
8357
- * DSH 0.1.6 ships an official right Sidebar (`dsh-client-ui-sidebar-right`)
8358
- * with a public two-stage extension path: a static tab type into
9056
+ * DSH ships an official right Sidebar (`dsh-client-ui-sidebar-right`) with a
9057
+ * public two-stage extension path: a static tab type into
8359
9058
  * `ctx.sidebarRightTabs` — whose `guide` entry draws a card on the Sidebar's
8360
9059
  * guide page — and a body under the keyed `sidebar.right.pane.tab` seat. This
8361
9060
  * module registers the plugin's appearance pages through that path, so the
@@ -8363,6 +9062,11 @@ window.__ModuleLoader__.load({
8363
9062
  * settings panel shows (one implementation, one state store; editing in either
8364
9063
  * place shows up in the other).
8365
9064
  *
9065
+ * NOTE: that registry is NOT a new-host feature. It is provided on 0.1.5-rc.2
9066
+ * already (verified against the release tag), so nothing here may treat "the
9067
+ * service answered" as "this is a recent DSH" — whether this tab or the
9068
+ * better-sidebar page owns the guide surface is the host adapter's call.
9069
+ *
8366
9070
  * The dependency is optional at every level, mirroring `sidebar/tab.tsx`:
8367
9071
  * · the registry is waited on with `ctx.inject`, so a host without the
8368
9072
  * right Sidebar (or an older DSH) silently registers nothing, and a
@@ -8416,7 +9120,7 @@ window.__ModuleLoader__.load({
8416
9120
  locale
8417
9121
  });
8418
9122
  scope.effect(() => {
8419
- const yieldTo = () => suppressesBetterSidebar() === false && rBetterSidebar();
9123
+ const yieldTo = () => sidebarGuideOwner() === "plugin" && rBetterSidebar();
8420
9124
  if (yieldTo()) return;
8421
9125
  let disposeTab = null;
8422
9126
  const withdraw = () => {
@@ -8454,121 +9158,6 @@ window.__ModuleLoader__.load({
8454
9158
  });
8455
9159
  }
8456
9160
  //#endregion
8457
- //#region src/client/header-tag.ts
8458
- /**
8459
- * Header-surface tagging.
8460
- *
8461
- * WHY A RUNTIME TAG IS NEEDED
8462
- * The session-header dropdowns (open-in-app, session-log, Agent Team, the job
8463
- * list, the subagent lineage tree) render their popover through the Menu/tree
8464
- * primitives, which PORTAL the list to `document.body`. A portaled list is not a
8465
- * DOM descendant of the header — no ancestor selector can reach it — and the
8466
- * primitive emits no `id`/`aria-controls` back-link to its anchor either. Pure
8467
- * CSS class matching was the remaining option, but 0.1.7 broke the shapes this
8468
- * plugin relied on: open-in-app moved from in-place to `portal` (so the old
8469
- * `:not(_portal)` discriminator no longer selected it), and the session row menu
8470
- * became a dynamic `sidebar.workspaces.session.menu.item` slot whose rows are no
8471
- * longer guaranteed to include a destructive one.
8472
- *
8473
- * THE SIGNAL
8474
- * The slot renderer stamps every registered slot with `data-slot="<key>"`
8475
- * (ui-renderer `scoped-slots.tsx`), and the session header's own slots are
8476
- * `conversation.session.header{,.actions,.utilities,.corner,.lineage}` — present
8477
- * on 0.1.5 through 0.1.7 alike. Those anchors live in the header subtree, so the
8478
- * plugin can detect when a header trigger is expanded.
8479
- *
8480
- * WHY NOT GEOMETRY
8481
- * A tempting link is the inline position the primitive writes on a portaled list
8482
- * (`style="left: …; top: …"`). It was tried and REJECTED: an `align="end"` list
8483
- * is flush to the trigger's right edge while the trigger itself sits at the far
8484
- * right of the header, so the two boxes often do not overlap horizontally (in
8485
- * the reported DOM the list is at `left:1012` with the trigger near x=1400). Any
8486
- * purely geometric proximity test therefore misses real header menus.
8487
- *
8488
- * THE ACTUAL RULE (structural, no geometry, no timing)
8489
- * A header dropdown can only be open while its trigger reports
8490
- * `aria-expanded="true"`, and opening one is what mounts the popover. So a
8491
- * popover tag is maintained as: "there is at least one expanded header trigger,
8492
- * AND this popover is not owned by another group". Popovers are only ever
8493
- * tagged/released on a change of that combined state, which makes the tag
8494
- * self-consistent and idempotent.
8495
- *
8496
- * The one hazard is a popover that is open for an unrelated reason (a composer
8497
- * menu, a hover card) *while* a header trigger stays expanded. Such a popover
8498
- * is not a candidate: it is excluded by `ownsForeignSurface`, which skips any
8499
- * popover carrying markers of a different slider group. In practice header
8500
- * dropdowns are transient (opening one closes the others), so this is a guard
8501
- * rather than the common path.
8502
- *
8503
- * @module
8504
- */
8505
- /** Marker attribute the stylesheet keys on. */
8506
- const HEADER_POPOVER_ATTR = "data-dsh-any-header-popover";
8507
- /** Slot anchors that identify the session header's own surfaces. */
8508
- const HEADER_SLOT_SELECTOR = [
8509
- "[data-slot=\"conversation.session.header\"]",
8510
- "[data-slot=\"conversation.session.header.actions\"]",
8511
- "[data-slot=\"conversation.session.header.utilities\"]",
8512
- "[data-slot=\"conversation.session.header.corner\"]",
8513
- "[data-slot=\"conversation.session.header.lineage\"]"
8514
- ].join(",");
8515
- /** Portaled popovers a header trigger can own. */
8516
- const POPOVER_SELECTOR = "[role=\"menu\"], [role=\"tree\"], [role=\"dialog\"]";
8517
- /** Whether any header slot anchor has an expanded trigger. */
8518
- function headerTriggerOpen() {
8519
- for (const anchor of document.querySelectorAll(HEADER_SLOT_SELECTOR)) if (anchor.querySelector("[aria-expanded=\"true\"]") !== null) return true;
8520
- return false;
8521
- }
8522
- /** Surfaces that belong to a different slider group and must never be claimed.
8523
- * `_denseList` without `_portal` is the in-place card-side dense menu, an
8524
- * `_ioCard`/`_menu`-classed body-level list belongs elsewhere, and a modal is
8525
- * its own surface. */
8526
- function ownsForeignSurface(el) {
8527
- if (el.getAttribute("aria-modal") === "true") return true;
8528
- return el.parentElement !== document.body;
8529
- }
8530
- let observer = null;
8531
- let raf = null;
8532
- /** Recompute tags from the current open state. */
8533
- function refreshHeaderPopovers() {
8534
- const open = headerTriggerOpen();
8535
- const candidates = [...document.querySelectorAll(POPOVER_SELECTOR)].filter((el) => !ownsForeignSurface(el));
8536
- if (!open) {
8537
- for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
8538
- return;
8539
- }
8540
- for (const el of candidates) el.setAttribute(HEADER_POPOVER_ATTR, "");
8541
- for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) if (!candidates.includes(el)) el.removeAttribute(HEADER_POPOVER_ATTR);
8542
- }
8543
- function schedule() {
8544
- if (raf !== null) return;
8545
- raf = window.requestAnimationFrame(() => {
8546
- raf = null;
8547
- refreshHeaderPopovers();
8548
- });
8549
- }
8550
- /** Start tagging. Returns a teardown that stops observing and clears markings. */
8551
- function startHeaderPopoverTagging() {
8552
- if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
8553
- refreshHeaderPopovers();
8554
- observer = new MutationObserver(schedule);
8555
- observer.observe(document.body, {
8556
- childList: true,
8557
- subtree: true,
8558
- attributes: true,
8559
- attributeFilter: ["aria-expanded"]
8560
- });
8561
- return () => {
8562
- observer?.disconnect();
8563
- observer = null;
8564
- if (raf !== null) {
8565
- window.cancelAnimationFrame(raf);
8566
- raf = null;
8567
- }
8568
- for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
8569
- };
8570
- }
8571
- //#endregion
8572
9161
  //#region src/client/index.tsx
8573
9162
  /**
8574
9163
  * dsh-any-background — browser half entry.
@@ -8667,13 +9256,7 @@ window.__ModuleLoader__.load({
8667
9256
  customDispose?.();
8668
9257
  if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
8669
9258
  }, "dsh-any-background: skin dispose");
8670
- const styleEl = document.createElement("style");
8671
- styleEl.dataset.plugin = "dsh-any-background";
8672
- styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_PROMOTION_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}${STROKE_RULE}${HEADER_POPOVER_RULE}${EXEMPT_DEFAULT_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
8673
- document.head.appendChild(styleEl);
8674
- ctx.effect(() => () => {
8675
- styleEl?.parentNode?.removeChild(styleEl);
8676
- }, "dsh-any-background: gradient");
9259
+ ctx.effect(() => mountStaticStyles(), "dsh-any-background: stylesheet");
8677
9260
  const disposeDragQuality = watchWallpaperDragQuality();
8678
9261
  ctx.effect(() => () => disposeDragQuality(), "dsh-any-background: drag quality");
8679
9262
  let rev = 0;