dsh-zen-remote 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -1
- package/README.md +56 -16
- package/docs/interface.md +69 -6
- package/docs/remote-access.en.md +7 -5
- package/docs/remote-access.md +8 -6
- package/dsh-push.mjs +272 -64
- package/lib/client.js +550 -28
- package/lib/lan-gate-server.cjs +7 -1
- package/lib/types/client/MobileHome.d.ts.map +1 -1
- package/lib/types/client/MobileSessionHeader.d.ts +1 -1
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/effects/modal-back.d.ts +22 -0
- package/lib/types/client/effects/modal-back.d.ts.map +1 -0
- package/lib/types/client/history-nav.d.ts +49 -0
- package/lib/types/client/history-nav.d.ts.map +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +4 -2
- package/lib/types/client/locales.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/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/lib/types/client/styles/content.css.d.ts +1 -1
- package/lib/types/client/styles/content.css.d.ts.map +1 -1
- 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/home.css.d.ts +1 -1
- package/lib/types/client/styles/home.css.d.ts.map +1 -1
- package/llms.txt +1 -1
- package/package.json +23 -21
- package/pwa/inject.js +1 -0
- package/scripts/sync-doc-version.mjs +83 -0
- package/src/client/MobileHome.tsx +17 -1
- package/src/client/MobileSessionHeader.tsx +134 -1
- package/src/client/MobileSessionInfo.tsx +38 -8
- package/src/client/effects/modal-back.ts +98 -0
- package/src/client/history-nav.ts +121 -0
- package/src/client/index.tsx +2 -0
- package/src/client/locales.ts +8 -4
- package/src/client/styles/base.css.ts +6 -0
- package/src/client/styles/chips.css.ts +4 -1
- package/src/client/styles/compat.css.ts +8 -1
- package/src/client/styles/composer.css.ts +25 -0
- package/src/client/styles/content.css.ts +24 -0
- package/src/client/styles/header.css.ts +123 -0
- package/src/client/styles/home.css.ts +19 -10
package/lib/client.js
CHANGED
|
@@ -385,6 +385,122 @@ function createNavStore() {
|
|
|
385
385
|
});
|
|
386
386
|
}
|
|
387
387
|
};
|
|
388
|
+
__modules["history-nav.js"] = function (require, module, exports) {
|
|
389
|
+
"use strict";
|
|
390
|
+
/**
|
|
391
|
+
* Back-gesture plumbing: a stack of dismissible layers mirrored into
|
|
392
|
+
* `history`, so Android's system back gesture closes what is on top instead
|
|
393
|
+
* of leaving the PWA.
|
|
394
|
+
*
|
|
395
|
+
* Why this exists at all: the phone shell's page stack is pure store state
|
|
396
|
+
* (`nav-store.ts`), and nothing ever pushed a history entry. Android's edge
|
|
397
|
+
* swipe IS the browser's back, so with an empty history it exited the app and
|
|
398
|
+
* the next launch was a cold reload. The edge is also where the system
|
|
399
|
+
* gesture lives, which is why the plugin's own edge-swipe never fired on
|
|
400
|
+
* Android — those pixels never reach the page. `systemGestureExclusionRects`
|
|
401
|
+
* is a native-app API with no web equivalent, so the gesture cannot be
|
|
402
|
+
* blocked; it can only be *given something to do*. (An early version called
|
|
403
|
+
* `history.back()` from the custom gesture and was removed as "a no-op
|
|
404
|
+
* against an SPA" — the no-op was the missing history entry, not `back()`.)
|
|
405
|
+
*
|
|
406
|
+
* Contract, and the one rule that keeps this honest: **layers close in one
|
|
407
|
+
* direction only.** A surface never flips its own state to closed; it calls
|
|
408
|
+
* {@link popLayer}, which rewinds history, which fires `popstate`, which runs
|
|
409
|
+
* the layer's `close`. State driven from both ends drifts out of sync with
|
|
410
|
+
* the history stack, and the drift shows up as a back gesture that does
|
|
411
|
+
* nothing.
|
|
412
|
+
*
|
|
413
|
+
* Verified against the harness: DSH's own client never touches the History
|
|
414
|
+
* API (`pushState`/`popstate`/`replaceState` all absent from dsh-client-
|
|
415
|
+
* runtime, -ui-conversation and -ui-layout, 2026-08-20), so this is
|
|
416
|
+
* uncontested ground.
|
|
417
|
+
*/
|
|
418
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
419
|
+
exports.pushLayer = pushLayer;
|
|
420
|
+
exports.popLayer = popLayer;
|
|
421
|
+
exports.hasLayer = hasLayer;
|
|
422
|
+
exports.layerDepth = layerDepth;
|
|
423
|
+
exports.resetLayers = resetLayers;
|
|
424
|
+
let stack = [];
|
|
425
|
+
let wired = false;
|
|
426
|
+
function depthOf(state) {
|
|
427
|
+
if (typeof state !== 'object' || state === null)
|
|
428
|
+
return 0;
|
|
429
|
+
const depth = state.dshNav;
|
|
430
|
+
return typeof depth === 'number' ? depth : 0;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Unwind to the depth the browser landed on, closing every layer above it.
|
|
434
|
+
* A single gesture can cross more than one entry (a long swipe, or a
|
|
435
|
+
* restored session), so this is a loop rather than one pop.
|
|
436
|
+
*/
|
|
437
|
+
function onPopState(event) {
|
|
438
|
+
const target = depthOf(event.state);
|
|
439
|
+
while (stack.length > target) {
|
|
440
|
+
const layer = stack.pop();
|
|
441
|
+
try {
|
|
442
|
+
layer?.close();
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// A layer that throws must not strand the ones underneath it.
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function wire() {
|
|
450
|
+
if (wired)
|
|
451
|
+
return;
|
|
452
|
+
wired = true;
|
|
453
|
+
window.addEventListener('popstate', onPopState);
|
|
454
|
+
}
|
|
455
|
+
/** Open a layer: remember how to close it, and give back one entry to spend. */
|
|
456
|
+
function pushLayer(layer) {
|
|
457
|
+
wire();
|
|
458
|
+
if (stack.some((entry) => entry.id === layer.id))
|
|
459
|
+
return;
|
|
460
|
+
stack.push(layer);
|
|
461
|
+
// Same URL on purpose: these are view states, not addresses. A changed URL
|
|
462
|
+
// would show up in the PWA's address handling and survive a reload as a
|
|
463
|
+
// route this app cannot serve.
|
|
464
|
+
history.pushState({ dshNav: stack.length }, '', location.href);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Close a layer the app-side way (a close button, a nav action). Rewinds
|
|
468
|
+
* history instead of flipping state, so the two never diverge.
|
|
469
|
+
*/
|
|
470
|
+
function popLayer(id) {
|
|
471
|
+
const index = stack.findIndex((entry) => entry.id === id);
|
|
472
|
+
if (index < 0)
|
|
473
|
+
return;
|
|
474
|
+
if (index === stack.length - 1) {
|
|
475
|
+
// The common case: let popstate do the closing so there is exactly one
|
|
476
|
+
// code path that ever calls `close`.
|
|
477
|
+
history.back();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Closed out of order (a deeper layer is still open above it). Drop it from
|
|
481
|
+
// the stack and close it here — this is the one case where `close` runs
|
|
482
|
+
// outside popstate, because rewinding would take the deeper layer with it.
|
|
483
|
+
const [layer] = stack.splice(index, 1);
|
|
484
|
+
try {
|
|
485
|
+
layer?.close();
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Same containment as the popstate path.
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/** Whether a layer is currently open. */
|
|
492
|
+
function hasLayer(id) {
|
|
493
|
+
return stack.some((entry) => entry.id === id);
|
|
494
|
+
}
|
|
495
|
+
/** Test seam: current depth. */
|
|
496
|
+
function layerDepth() {
|
|
497
|
+
return stack.length;
|
|
498
|
+
}
|
|
499
|
+
/** Test seam: drop everything without touching history. */
|
|
500
|
+
function resetLayers() {
|
|
501
|
+
stack = [];
|
|
502
|
+
}
|
|
503
|
+
};
|
|
388
504
|
__modules["session-dot.js"] = function (require, module, exports) {
|
|
389
505
|
"use strict";
|
|
390
506
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -762,6 +878,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
762
878
|
const react_1 = require("react");
|
|
763
879
|
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
764
880
|
const nav_store_ts_1 = require("./nav-store.js");
|
|
881
|
+
const history_nav_ts_1 = require("./history-nav.js");
|
|
882
|
+
/** The phone shell's one page-stack step: session list -> session. */
|
|
883
|
+
const SESSION_LAYER = 'session';
|
|
765
884
|
const session_dot_ts_1 = require("./session-dot.js");
|
|
766
885
|
const MobileHomeChips_tsx_1 = require("./MobileHomeChips.js");
|
|
767
886
|
/** Phone breakpoint: below the tablet range, where the app-shell layout applies. */
|
|
@@ -881,8 +1000,21 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
|
|
|
881
1000
|
// store directly — a handle mounts under exactly one scope, and this one
|
|
882
1001
|
// is already root-scoped here (see nav-store.ts) — so it dispatches
|
|
883
1002
|
// GO_HOME_EVENT instead and this, the store's actual owner, applies it.
|
|
1003
|
+
//
|
|
1004
|
+
// Both directions go through the history layer (history-nav.ts) so
|
|
1005
|
+
// Android's system back gesture lands on the session→list step instead of
|
|
1006
|
+
// exiting the PWA. GO_HOME_EVENT rewinds rather than setting the store: the
|
|
1007
|
+
// store is only ever moved home by the layer's own close callback, so the
|
|
1008
|
+
// history stack and the page stack cannot drift apart. The direct
|
|
1009
|
+
// `show('home')` fallback covers a session view that was somehow entered
|
|
1010
|
+
// without a layer (a restored session, a host that blocks pushState).
|
|
884
1011
|
(0, react_1.useEffect)(() => {
|
|
885
|
-
const onGoHome = () =>
|
|
1012
|
+
const onGoHome = () => {
|
|
1013
|
+
if ((0, history_nav_ts_1.hasLayer)(SESSION_LAYER))
|
|
1014
|
+
(0, history_nav_ts_1.popLayer)(SESSION_LAYER);
|
|
1015
|
+
else
|
|
1016
|
+
actions.show('home');
|
|
1017
|
+
};
|
|
886
1018
|
window.addEventListener(nav_store_ts_1.GO_HOME_EVENT, onGoHome);
|
|
887
1019
|
return () => window.removeEventListener(nav_store_ts_1.GO_HOME_EVENT, onGoHome);
|
|
888
1020
|
}, [actions]);
|
|
@@ -890,6 +1022,7 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
|
|
|
890
1022
|
start();
|
|
891
1023
|
setSheet(null);
|
|
892
1024
|
actions.show('session');
|
|
1025
|
+
(0, history_nav_ts_1.pushLayer)({ id: SESSION_LAYER, close: () => actions.show('home') });
|
|
893
1026
|
};
|
|
894
1027
|
// Opens the OFFICIAL settings modal (dsh-client-ui-settings-general's
|
|
895
1028
|
// SettingsRoot) by clicking its real trigger button — its "open" state is
|
|
@@ -946,6 +1079,47 @@ const nav_store_ts_1 = require("./nav-store.js");
|
|
|
946
1079
|
function IconInfoOutline16({ size = 16 }) {
|
|
947
1080
|
return ((0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "8", r: "6.7", stroke: "currentColor", strokeWidth: "1.3" }), (0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "4.7", r: "0.95", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("rect", { x: "7.25", y: "6.9", width: "1.5", height: "4.7", rx: "0.75", fill: "currentColor" })] }));
|
|
948
1081
|
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Two more hand-built 14px glyphs, same reason as IconInfoOutline16 above:
|
|
1084
|
+
* the primitives family has no subagent or background-task icon. Drawn on
|
|
1085
|
+
* the same 16-box with the same 1.3 stroke so the activity chip reads as
|
|
1086
|
+
* part of the header icon family.
|
|
1087
|
+
*/
|
|
1088
|
+
/** Three linked nodes — a parent delegating to children. */
|
|
1089
|
+
function IconSubagentOutline14({ size = 14 }) {
|
|
1090
|
+
return ((0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "3.2", r: "2", stroke: "currentColor", strokeWidth: "1.3" }), (0, jsx_runtime_1.jsx)("circle", { cx: "3.4", cy: "12.6", r: "2", stroke: "currentColor", strokeWidth: "1.3" }), (0, jsx_runtime_1.jsx)("circle", { cx: "12.6", cy: "12.6", r: "2", stroke: "currentColor", strokeWidth: "1.3" }), (0, jsx_runtime_1.jsx)("path", { d: "M8 5.4v2.2M8 7.6H3.4v3M8 7.6h4.6v3", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round" })] }));
|
|
1091
|
+
}
|
|
1092
|
+
/** A clock face — work still ticking in the background. */
|
|
1093
|
+
function IconTaskOutline14({ size = 14 }) {
|
|
1094
|
+
return ((0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "8", r: "6.1", stroke: "currentColor", strokeWidth: "1.3" }), (0, jsx_runtime_1.jsx)("path", { d: "M8 4.4V8l2.5 1.7", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round" })] }));
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* The official triggers our pills stand in for. Both are hidden and parked
|
|
1098
|
+
* as invisible anchors at the right end of the view-switch band
|
|
1099
|
+
* (styles/header.css.ts) so their own popovers still mount and position from
|
|
1100
|
+
* there; the pill is the visible control and forwards the tap.
|
|
1101
|
+
*
|
|
1102
|
+
* Told apart by `aria-haspopup`, not by class: the subagent entry declares
|
|
1103
|
+
* `tree` (its menu is a session tree) while the jobs entry declares none.
|
|
1104
|
+
* Both roots/triggers only expose build-hashed class names, so the ARIA
|
|
1105
|
+
* contract is the one stable discriminator between two otherwise identical
|
|
1106
|
+
* `[class$="_root"] > button[class$="_trigger"]` shapes.
|
|
1107
|
+
*/
|
|
1108
|
+
const ACTIONS_SLOT = '[data-slot="conversation.session.header.actions"]';
|
|
1109
|
+
const SUBAGENT_TRIGGER = `${ACTIONS_SLOT} button[aria-haspopup="tree"]`;
|
|
1110
|
+
const JOBS_TRIGGER = `${ACTIONS_SLOT} > [class$="_root"] > button[class$="_trigger"]:not([aria-haspopup])`;
|
|
1111
|
+
/** One glanceable group: icon + count + state dot; opens the official popover. */
|
|
1112
|
+
function ActivityPill({ kind, count, state, label, trigger, onFallback, Icon }) {
|
|
1113
|
+
if (count === 0)
|
|
1114
|
+
return null;
|
|
1115
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "activity-pill", "data-activity-kind": kind, "data-activity-state": state, "aria-label": label, title: label, onClick: () => {
|
|
1116
|
+
const el = document.querySelector(trigger);
|
|
1117
|
+
if (el === null)
|
|
1118
|
+
onFallback();
|
|
1119
|
+
else
|
|
1120
|
+
el.click();
|
|
1121
|
+
}, children: [(0, jsx_runtime_1.jsx)(Icon, {}), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "activity-count", children: count }), (0, jsx_runtime_1.jsx)("i", { "data-mobile-nav": "activity-dot", "aria-hidden": "true" })] }));
|
|
1122
|
+
}
|
|
949
1123
|
/**
|
|
950
1124
|
* Reads the official session-header tablist by role/aria only (no hashed
|
|
951
1125
|
* classes) — the plan's one sanctioned official-DOM read: ChatStore's view
|
|
@@ -1002,10 +1176,28 @@ function useViewTabs() {
|
|
|
1002
1176
|
* (styles/header.css.ts) keeps them hidden at >= 768px so the tablet drawer
|
|
1003
1177
|
* and the desktop layout stay exactly as they were.
|
|
1004
1178
|
*/
|
|
1005
|
-
function MobileHeaderActions({ t }) {
|
|
1179
|
+
function MobileHeaderActions({ sessionId, useSessions, t }) {
|
|
1006
1180
|
const tabs = useViewTabs();
|
|
1007
1181
|
const active = tabs.find((tab) => tab.active) ?? tabs[0];
|
|
1008
|
-
|
|
1182
|
+
/* At-a-glance activity, right end of the view-switch row. The native
|
|
1183
|
+
header entries these replace were hidden on phone: both are ~103px wide
|
|
1184
|
+
and the header's left grid column is 92px, so they overflowed straight
|
|
1185
|
+
across the centred session title. Reading the counts from the sessions
|
|
1186
|
+
snapshot instead of re-homing the official DOM keeps React's ownership
|
|
1187
|
+
of its own nodes intact — the detail lives one tap away in the info
|
|
1188
|
+
card, which this chip opens. */
|
|
1189
|
+
const subagents = useSessions((s) => s.subagentsByParent[sessionId]?.entries ?? []);
|
|
1190
|
+
const jobs = useSessions((s) => s.jobsBySession[sessionId] ?? []);
|
|
1191
|
+
const subagentRunning = subagents.some((e) => e.kind === 'child' && e.activity === 'running');
|
|
1192
|
+
const jobRunning = jobs.some((j) => j.status === 'running' || j.status === 'stopping');
|
|
1193
|
+
const jobBad = jobs.some((j) => j.status === 'failed' || j.status === 'killed');
|
|
1194
|
+
const subagentState = subagentRunning ? 'running' : 'done';
|
|
1195
|
+
const jobState = jobRunning ? 'running' : jobBad ? 'warning' : 'done';
|
|
1196
|
+
const hasActivity = subagents.length > 0 || jobs.length > 0;
|
|
1197
|
+
/* Only reached when the official entry is missing entirely — the info
|
|
1198
|
+
card still carries both counts, so the tap explains something. */
|
|
1199
|
+
const openInfoCard = () => { window.dispatchEvent(new CustomEvent(nav_store_ts_1.SESSION_INFO_EVENT)); };
|
|
1200
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "header-back", "aria-label": t('backToList'), title: t('backToList'), onClick: () => window.dispatchEvent(new CustomEvent(nav_store_ts_1.GO_HOME_EVENT)), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconChevronLeftOutline14, { size: 20 }) }), tabs.length > 1 && active !== undefined && ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "header-viewrow", "aria-label": t('switchView'), onClick: () => tabs.find((tab) => !tab.active)?.el.click(), children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "header-viewrow-label", children: active.label }), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "header-viewrow-dots", "aria-hidden": "true", children: tabs.map((tab, index) => ((0, jsx_runtime_1.jsx)("i", { "data-active": tab.active ? '' : undefined }, index))) })] })), hasActivity && ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "header-activity", children: [(0, jsx_runtime_1.jsx)(ActivityPill, { kind: "subagent", count: subagents.length, state: subagentState, label: t('infoSubagents', { count: subagents.length }), trigger: SUBAGENT_TRIGGER, onFallback: openInfoCard, Icon: IconSubagentOutline14 }), (0, jsx_runtime_1.jsx)(ActivityPill, { kind: "job", count: jobs.length, state: jobState, label: t('infoJobs', { count: jobs.length }), trigger: JOBS_TRIGGER, onFallback: openInfoCard, Icon: IconTaskOutline14 })] }))] }));
|
|
1009
1201
|
}
|
|
1010
1202
|
/**
|
|
1011
1203
|
* Session header, right lane: the session-info entry (S4 owns the actual
|
|
@@ -1096,6 +1288,9 @@ const react_1 = require("react");
|
|
|
1096
1288
|
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1097
1289
|
const client_1 = require("@deepseek-ai/dsh-client-runtime/client");
|
|
1098
1290
|
const nav_store_ts_1 = require("./nav-store.js");
|
|
1291
|
+
const history_nav_ts_1 = require("./history-nav.js");
|
|
1292
|
+
/** Layer id for the info sheet, so Android back closes it before leaving the session. */
|
|
1293
|
+
const INFO_LAYER = 'session-info';
|
|
1099
1294
|
const MobileSessionHeader_tsx_1 = require("./MobileSessionHeader.js");
|
|
1100
1295
|
/* ---- StatsLine-identical formatting -------------------------------------
|
|
1101
1296
|
* Ported (not imported — the source functions are module-private to
|
|
@@ -1123,10 +1318,14 @@ function formatDuration(ms) {
|
|
|
1123
1318
|
function billedInputTokens(usage) {
|
|
1124
1319
|
return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
1125
1320
|
}
|
|
1126
|
-
/**
|
|
1321
|
+
/**
|
|
1322
|
+
* Cache-hit share of prompt-side input over the whole durable log; null when
|
|
1323
|
+
* nothing was billed. Returned unrounded — the cell is the headline figure
|
|
1324
|
+
* now and shows one decimal, so rounding here would throw that digit away.
|
|
1325
|
+
*/
|
|
1127
1326
|
function cacheHitPercent(usage) {
|
|
1128
1327
|
const denominator = billedInputTokens(usage);
|
|
1129
|
-
return denominator === 0 ? null :
|
|
1328
|
+
return denominator === 0 ? null : (usage.cacheReadTokens / denominator) * 100;
|
|
1130
1329
|
}
|
|
1131
1330
|
/** Data-missing / not-yet-observed placeholder for a stat cell. */
|
|
1132
1331
|
const NA = '—';
|
|
@@ -1161,6 +1360,9 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
|
|
|
1161
1360
|
const onOpen = () => {
|
|
1162
1361
|
setError(null);
|
|
1163
1362
|
setOpen(true);
|
|
1363
|
+
// Stack a history entry on top of the session's: Android back now
|
|
1364
|
+
// closes this sheet first and only then leaves the session.
|
|
1365
|
+
(0, history_nav_ts_1.pushLayer)({ id: INFO_LAYER, close: () => setOpen(false) });
|
|
1164
1366
|
};
|
|
1165
1367
|
window.addEventListener(nav_store_ts_1.SESSION_INFO_EVENT, onOpen);
|
|
1166
1368
|
return () => window.removeEventListener(nav_store_ts_1.SESSION_INFO_EVENT, onOpen);
|
|
@@ -1168,11 +1370,20 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
|
|
|
1168
1370
|
const tabs = (0, MobileSessionHeader_tsx_1.useViewTabs)();
|
|
1169
1371
|
const row = useSessions((s) => s.byId[sessionId]);
|
|
1170
1372
|
const subagentCount = useSessions((s) => s.subagentsByParent[sessionId]?.entries.length ?? 0);
|
|
1373
|
+
const jobCount = useSessions((s) => s.jobsBySession[sessionId]?.length ?? 0);
|
|
1171
1374
|
const stats = useProjection('sessionStats');
|
|
1172
1375
|
const usage = useProjection('tokenUsage');
|
|
1173
1376
|
if (!open)
|
|
1174
1377
|
return null;
|
|
1175
|
-
|
|
1378
|
+
// Rewind rather than flip the flag — see the one-direction rule in
|
|
1379
|
+
// history-nav.ts. Falls back to a plain close if the layer is gone (the
|
|
1380
|
+
// sheet was opened before this wiring existed, or pushState is unavailable).
|
|
1381
|
+
const close = () => {
|
|
1382
|
+
if ((0, history_nav_ts_1.hasLayer)(INFO_LAYER))
|
|
1383
|
+
(0, history_nav_ts_1.popLayer)(INFO_LAYER);
|
|
1384
|
+
else
|
|
1385
|
+
setOpen(false);
|
|
1386
|
+
};
|
|
1176
1387
|
const run = async (action) => {
|
|
1177
1388
|
setBusy(true);
|
|
1178
1389
|
setError(null);
|
|
@@ -1241,19 +1452,26 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
|
|
|
1241
1452
|
value: stats === undefined || stats.toolMs === 0 ? NA : formatDuration(stats.toolMs),
|
|
1242
1453
|
sub: undefined,
|
|
1243
1454
|
},
|
|
1455
|
+
/* Cache hit is the headline, token flow the sub-line (2026-08-20): on a
|
|
1456
|
+
long session the ratio is the number worth glancing at — it moves, and
|
|
1457
|
+
it is what the bill turns on — while the absolute in→out figure is
|
|
1458
|
+
reference detail. One decimal, because "100%" and "99.6%" are very
|
|
1459
|
+
different answers and integer rounding hid that. */
|
|
1244
1460
|
{
|
|
1245
|
-
label: t('
|
|
1246
|
-
value:
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1461
|
+
label: t('infoStatCacheHit'),
|
|
1462
|
+
value: cacheHit === null ? NA : `${cacheHit.toFixed(1)}%`,
|
|
1463
|
+
sub: tokensEmpty || usage === undefined
|
|
1464
|
+
? undefined
|
|
1465
|
+
: t('infoTokenFlow', {
|
|
1466
|
+
io: `${formatTokens(billedInputTokens(usage))}→${formatTokens(usage.outputTokens)}`,
|
|
1467
|
+
}),
|
|
1250
1468
|
},
|
|
1251
1469
|
];
|
|
1252
1470
|
return ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-layer", children: [(0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-mask", role: "button", tabIndex: -1, "aria-label": t('infoClose'), onClick: close }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-sheet", role: "dialog", "aria-modal": "true", children: [(0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-head", children: [tabs.length > 1 && ((0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-tabs", role: "group", "aria-label": t('switchView'), children: tabs.map((tab) => ((0, jsx_runtime_1.jsx)("button", { type: "button", "aria-pressed": tab.active, "data-mobile-nav": "info-tab", "data-selected": tab.active ? '' : undefined, onClick: () => {
|
|
1253
1471
|
if (!tab.active)
|
|
1254
1472
|
tab.el.click();
|
|
1255
1473
|
close();
|
|
1256
|
-
}, children: tab.label }, tab.label))) })), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "info-close", "aria-label": t('infoClose'), onClick: close, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconCloseOutline16, { size: 16 }) })] }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-badges", children: [row?.agentPreset !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: row.agentPreset }), subagentCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoSubagents', { count: subagentCount }) })), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge-cwd", children: cwd ?? t('infoCwdFallback') })] }), (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-stats", children: cells.map((cell) => ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-stat", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-value", children: cell.value }), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-label", children: cell.label }), cell.sub !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-sub", children: cell.sub })] }, cell.label))) }), error !== null && (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-error", children: t('infoActionError', { message: error }) }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onExport, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconDownloadOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoExport') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onRename, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconEditOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoRename') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onFork, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconBranchOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoFork') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", "data-mobile-nav-danger": "", disabled: busy, onClick: onArchive, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconArchiveOutline20, { size: 20 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoArchive') })] })] })] })] }));
|
|
1474
|
+
}, children: tab.label }, tab.label))) })), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "info-close", "aria-label": t('infoClose'), onClick: close, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconCloseOutline16, { size: 16 }) })] }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-badges", children: [row?.agentPreset !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: row.agentPreset }), subagentCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoSubagents', { count: subagentCount }) })), jobCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoJobs', { count: jobCount }) })), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge-cwd", children: cwd ?? t('infoCwdFallback') })] }), (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-stats", children: cells.map((cell) => ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-stat", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-value", children: cell.value }), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-label", children: cell.label }), cell.sub !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-sub", children: cell.sub })] }, cell.label))) }), error !== null && (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-error", children: t('infoActionError', { message: error }) }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onExport, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconDownloadOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoExport') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onRename, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconEditOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoRename') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onFork, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconBranchOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoFork') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", "data-mobile-nav-danger": "", disabled: busy, onClick: onArchive, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconArchiveOutline20, { size: 20 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoArchive') })] })] })] })] }));
|
|
1257
1475
|
}
|
|
1258
1476
|
};
|
|
1259
1477
|
__modules["attach-upload.js"] = function (require, module, exports) {
|
|
@@ -1712,6 +1930,12 @@ exports.BASE_CSS = `
|
|
|
1712
1930
|
align-items: center;
|
|
1713
1931
|
justify-content: center;
|
|
1714
1932
|
height: 36px;
|
|
1933
|
+
/* The notice's action row is a flex row and the official confirm button
|
|
1934
|
+
takes the slack; without an explicit shrink guard this button collapsed
|
|
1935
|
+
to about two characters wide on a phone. Content width, never squeezed. */
|
|
1936
|
+
flex: 0 0 auto;
|
|
1937
|
+
min-width: max-content;
|
|
1938
|
+
white-space: nowrap;
|
|
1715
1939
|
padding: 0 14px;
|
|
1716
1940
|
margin-right: 8px;
|
|
1717
1941
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, .15));
|
|
@@ -2898,7 +3122,14 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
|
|
|
2898
3122
|
border: none;
|
|
2899
3123
|
border-radius: 999px;
|
|
2900
3124
|
background: var(--dsw-alias-bg-base, #fff);
|
|
2901
|
-
|
|
3125
|
+
/* Drop shadow alone does not draw an edge: the fill is bg-base, which in
|
|
3126
|
+
dark theme is the same colour as the surface behind it, and a black
|
|
3127
|
+
shadow on a dark ground is invisible — the pill read as floating text
|
|
3128
|
+
with no boundary. The inset hairline is the edge; the drop shadow only
|
|
3129
|
+
lifts it. Same token the session rows use for their dark-mode ring. */
|
|
3130
|
+
box-shadow:
|
|
3131
|
+
inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, .14)),
|
|
3132
|
+
0 4px 16px rgba(0, 0, 0, .2);
|
|
2902
3133
|
color: var(--dsw-alias-label-primary, inherit);
|
|
2903
3134
|
font-family: inherit;
|
|
2904
3135
|
font-size: 14px;
|
|
@@ -3207,14 +3438,19 @@ exports.HOME_CSS = `/* ---------- phone app shell (< 768px) ---------- */
|
|
|
3207
3438
|
values, not source-read guesses, are what actually matter here —
|
|
3208
3439
|
--dsw-alias-bg-base/-layer-1/-layer-2 all resolve to the exact same
|
|
3209
3440
|
color in the LIGHT theme (neutral-bluish-00, i.e. plain white); they
|
|
3210
|
-
only diverge in dark mode.
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3441
|
+
only diverge in dark mode.
|
|
3442
|
+
|
|
3443
|
+
2026-08-20: the page and the CARDS swapped tokens. This surface used
|
|
3444
|
+
to be --dsw-specific-sidebar-fill, which made the list page a
|
|
3445
|
+
different shade from the session page — measured on-device in dark
|
|
3446
|
+
theme, rgb(27,27,28) here against rgb(21,21,23) there, a visible step
|
|
3447
|
+
every time you opened a session. The page now takes bg-base, the same
|
|
3448
|
+
token the session page and <body> resolve to, so the two screens are
|
|
3449
|
+
literally the same colour; the cards took sidebar-fill in exchange, so
|
|
3450
|
+
the contrast that separates a row from the page is preserved — it
|
|
3451
|
+
just runs the other way (raised card on a base page, the more
|
|
3452
|
+
conventional reading of the two anyway). */
|
|
3453
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
3218
3454
|
color: var(--dsw-alias-label-primary, inherit);
|
|
3219
3455
|
padding-top: var(--mnav-sat);
|
|
3220
3456
|
transform: translateX(0);
|
|
@@ -3332,7 +3568,10 @@ exports.HOME_CSS = `/* ---------- phone app shell (< 768px) ---------- */
|
|
|
3332
3568
|
padding: 14px 16px;
|
|
3333
3569
|
border: none;
|
|
3334
3570
|
border-radius: 22px;
|
|
3335
|
-
|
|
3571
|
+
/* The page's old colour (see the swap note on [data-mobile-nav="home"]):
|
|
3572
|
+
differs from bg-base in BOTH themes, and is the token
|
|
3573
|
+
dsh-better-sidebar's panel already uses for a secondary surface. */
|
|
3574
|
+
background: var(--dsw-specific-sidebar-fill, #f5f5f5);
|
|
3336
3575
|
color: inherit;
|
|
3337
3576
|
font-family: inherit;
|
|
3338
3577
|
text-align: left;
|
|
@@ -3435,7 +3674,8 @@ exports.HOME_CSS = `/* ---------- phone app shell (< 768px) ---------- */
|
|
|
3435
3674
|
margin: 12px 16px;
|
|
3436
3675
|
padding: 48px 24px;
|
|
3437
3676
|
border-radius: 22px;
|
|
3438
|
-
|
|
3677
|
+
/* Stands in for the rows, so it carries the rows' surface. */
|
|
3678
|
+
background: var(--dsw-specific-sidebar-fill, #f5f5f5);
|
|
3439
3679
|
color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));
|
|
3440
3680
|
font-size: 15px;
|
|
3441
3681
|
text-align: center;
|
|
@@ -3611,6 +3851,7 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
3611
3851
|
list in misc.css.ts. */
|
|
3612
3852
|
[data-mobile-nav="header-back"],
|
|
3613
3853
|
[data-mobile-nav="header-viewrow"],
|
|
3854
|
+
[data-mobile-nav="header-activity"],
|
|
3614
3855
|
[data-mobile-nav="header-info"],
|
|
3615
3856
|
[data-mobile-nav="header-workbench"],
|
|
3616
3857
|
/* The workbench close pill's default-hidden used to live in compat.css.ts —
|
|
@@ -3853,6 +4094,48 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
3853
4094
|
[data-phase] header [data-slot="conversation.session.header.utilities"] > * {
|
|
3854
4095
|
display: none !important;
|
|
3855
4096
|
}
|
|
4097
|
+
/* Native background-task / subagent entries: kept mounted, but parked as
|
|
4098
|
+
ZERO-WIDTH INVISIBLE ANCHORS at the right end of the view-switch band.
|
|
4099
|
+
Our activity pill is the visible control and forwards the tap to the
|
|
4100
|
+
official trigger, so the official popover — a child of this root — still
|
|
4101
|
+
mounts and positions from here.
|
|
4102
|
+
|
|
4103
|
+
Why not just leave them visible in the header: each is ~103px wide while
|
|
4104
|
+
headerActions' grid column is 92px, and flex children that cannot shrink
|
|
4105
|
+
simply overflow — measured at 375px the subagent entry ran x=60..163
|
|
4106
|
+
straight across the centred title at x=100..275.
|
|
4107
|
+
|
|
4108
|
+
Why not display:none them either: the menu lives inside the root, so
|
|
4109
|
+
hiding the root takes the popover with it and the pill would have
|
|
4110
|
+
nothing to open. Hide the TRIGGER (below) and keep the root as a box. */
|
|
4111
|
+
[data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"]:has(> button[class$="_trigger"]) {
|
|
4112
|
+
display: block !important;
|
|
4113
|
+
position: absolute !important;
|
|
4114
|
+
right: 0 !important;
|
|
4115
|
+
top: 100% !important;
|
|
4116
|
+
width: 0 !important;
|
|
4117
|
+
height: 28px !important;
|
|
4118
|
+
overflow: visible !important;
|
|
4119
|
+
order: 0 !important;
|
|
4120
|
+
}
|
|
4121
|
+
/* The official trigger never shows — the pill replaced it. Programmatic
|
|
4122
|
+
.click() still reaches React's handler on a display:none button. */
|
|
4123
|
+
[data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"] > button[class$="_trigger"] {
|
|
4124
|
+
display: none !important;
|
|
4125
|
+
}
|
|
4126
|
+
/* Anchor the popover to the RIGHT edge and clamp it to the viewport. The
|
|
4127
|
+
root now sits at the right edge with zero width, so the official
|
|
4128
|
+
\`left: 0\` would start the 336px panel off-screen; layout.css.ts's
|
|
4129
|
+
\`left: 8px\` re-anchor was written for a viewport-width containing block
|
|
4130
|
+
that this element is not. Right-anchoring is the one that holds for both
|
|
4131
|
+
(2026-08-20: the old pairing put the panel 29px past the right edge). */
|
|
4132
|
+
[data-phase] header [data-slot="conversation.session.header.actions"] [class$="_menu"] {
|
|
4133
|
+
left: auto !important;
|
|
4134
|
+
right: 0 !important;
|
|
4135
|
+
width: min(336px, calc(100vw - 32px)) !important;
|
|
4136
|
+
max-width: none !important;
|
|
4137
|
+
max-height: min(420px, calc(100dvh - 140px)) !important;
|
|
4138
|
+
}
|
|
3856
4139
|
/* The agent-preset mode badge specifically survives the blanket hide
|
|
3857
4140
|
above: layout.css.ts's shared <=1023px block targets it directly
|
|
3858
4141
|
(header [class$="_label"]:has(> svg) { display: block !important }),
|
|
@@ -3870,6 +4153,12 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
3870
4153
|
[data-phase] header [data-slot="conversation.session.header.actions"] > [data-mobile-nav="header-viewrow"] {
|
|
3871
4154
|
display: flex !important;
|
|
3872
4155
|
}
|
|
4156
|
+
/* Our own chip lives in the same slot, so the blanket hide above swallows
|
|
4157
|
+
it unless it is named here too (it was, on the first build: the chip
|
|
4158
|
+
rendered with the right counts and painted nothing). */
|
|
4159
|
+
[data-phase] header [data-slot="conversation.session.header.actions"] > [data-mobile-nav="header-activity"] {
|
|
4160
|
+
display: inline-flex !important;
|
|
4161
|
+
}
|
|
3873
4162
|
[data-phase] header [data-slot="conversation.session.header.utilities"] > [data-mobile-nav="header-info"],
|
|
3874
4163
|
[data-phase] header [data-slot="conversation.session.header.utilities"] > [data-mobile-nav="header-workbench"] {
|
|
3875
4164
|
display: inline-flex !important;
|
|
@@ -3947,6 +4236,80 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
|
|
|
3947
4236
|
display:contents on titleCluster does not break that search — so
|
|
3948
4237
|
top:100% sits directly under the title regardless of the title row's
|
|
3949
4238
|
actual height, with no hardcoded offset to keep in sync. */
|
|
4239
|
+
/* Activity chip (subagents / background tasks) rides the right end of the
|
|
4240
|
+
same band. It is a SIBLING of the switch button, not a child — that
|
|
4241
|
+
button spans the full row, so nesting would make every tap on the chip
|
|
4242
|
+
switch views too — and it shares titleRow as its containing block, so
|
|
4243
|
+
top:100% lands both on the same 28px line. z-index puts it above the
|
|
4244
|
+
row it overlaps. */
|
|
4245
|
+
[data-mobile-nav="header-activity"] {
|
|
4246
|
+
position: absolute;
|
|
4247
|
+
right: 0;
|
|
4248
|
+
top: 100%;
|
|
4249
|
+
z-index: 1;
|
|
4250
|
+
display: inline-flex;
|
|
4251
|
+
align-items: center;
|
|
4252
|
+
gap: 9px;
|
|
4253
|
+
height: 28px;
|
|
4254
|
+
padding: 0 2px 0 10px;
|
|
4255
|
+
border: none;
|
|
4256
|
+
background: transparent;
|
|
4257
|
+
color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));
|
|
4258
|
+
font-family: inherit;
|
|
4259
|
+
cursor: pointer;
|
|
4260
|
+
touch-action: manipulation;
|
|
4261
|
+
-webkit-tap-highlight-color: transparent;
|
|
4262
|
+
}
|
|
4263
|
+
[data-mobile-nav="activity-pill"] {
|
|
4264
|
+
display: inline-flex;
|
|
4265
|
+
align-items: center;
|
|
4266
|
+
gap: 3px;
|
|
4267
|
+
height: 28px;
|
|
4268
|
+
padding: 0;
|
|
4269
|
+
border: none;
|
|
4270
|
+
background: transparent;
|
|
4271
|
+
color: inherit;
|
|
4272
|
+
font-family: inherit;
|
|
4273
|
+
font-size: 12px;
|
|
4274
|
+
line-height: 18px;
|
|
4275
|
+
cursor: pointer;
|
|
4276
|
+
touch-action: manipulation;
|
|
4277
|
+
-webkit-tap-highlight-color: transparent;
|
|
4278
|
+
}
|
|
4279
|
+
[data-mobile-nav="activity-pill"]:active {
|
|
4280
|
+
opacity: .55;
|
|
4281
|
+
}
|
|
4282
|
+
[data-mobile-nav="activity-count"] {
|
|
4283
|
+
font-variant-numeric: tabular-nums;
|
|
4284
|
+
}
|
|
4285
|
+
/* Steady green = everything settled. */
|
|
4286
|
+
[data-mobile-nav="activity-dot"] {
|
|
4287
|
+
display: block;
|
|
4288
|
+
width: 6px;
|
|
4289
|
+
height: 6px;
|
|
4290
|
+
margin-left: 1px;
|
|
4291
|
+
border-radius: 50%;
|
|
4292
|
+
background: var(--dsw-alias-state-success-primary, #16a34a);
|
|
4293
|
+
}
|
|
4294
|
+
/* A job that failed or was killed is not a success — never paint it green. */
|
|
4295
|
+
[data-activity-state="warning"] [data-mobile-nav="activity-dot"] {
|
|
4296
|
+
background: var(--dsw-alias-state-warn-primary, #d97706);
|
|
4297
|
+
}
|
|
4298
|
+
/* Pulsing blue = still running. Same state palette as the session dot. */
|
|
4299
|
+
[data-activity-state="running"] [data-mobile-nav="activity-dot"] {
|
|
4300
|
+
background: var(--dsw-alias-state-business-primary, #4f6ef7);
|
|
4301
|
+
animation: dsh-zen-activity-pulse 1.4s ease-in-out infinite;
|
|
4302
|
+
}
|
|
4303
|
+
@keyframes dsh-zen-activity-pulse {
|
|
4304
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
4305
|
+
50% { opacity: .45; transform: scale(.72); }
|
|
4306
|
+
}
|
|
4307
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4308
|
+
[data-activity-state="running"] [data-mobile-nav="activity-dot"] {
|
|
4309
|
+
animation: none;
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
|
|
3950
4313
|
[data-mobile-nav="header-viewrow"] {
|
|
3951
4314
|
position: absolute;
|
|
3952
4315
|
left: 0;
|
|
@@ -4278,6 +4641,31 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
4278
4641
|
padding: 0 10px !important;
|
|
4279
4642
|
font-size: 11.5px !important;
|
|
4280
4643
|
}
|
|
4644
|
+
/* --- 7a. the native to-do card opts OUT of the 26px pill cage ---
|
|
4645
|
+
conversation.input.dock is not only a chip rail: DSH's own TodoPanel
|
|
4646
|
+
(dsh-client-ui-conversation) registers here, and it is an expandable
|
|
4647
|
+
card, not a pill — tapping its header renders the to-do list INSIDE
|
|
4648
|
+
itself. The cage's max-height:26px + overflow:hidden left that list
|
|
4649
|
+
rendered but clipped to nothing, so on a phone the bar looked dead:
|
|
4650
|
+
aria-expanded flipped, 8 rows mounted at 216px, and the card still
|
|
4651
|
+
painted 26px tall (measured live, 2026-08-20). Give it the official
|
|
4652
|
+
geometry back — full row, natural height, 12px radius. Its list caps
|
|
4653
|
+
itself at 180px with its own scroller, so this cannot run into the
|
|
4654
|
+
composer. Anchored on the official data-testid, a stable contract
|
|
4655
|
+
marker rather than a hashed class name. */
|
|
4656
|
+
[data-slot="conversation.input.dock"] > [data-testid="todo-panel"] {
|
|
4657
|
+
flex: 1 1 auto !important;
|
|
4658
|
+
min-width: 0 !important;
|
|
4659
|
+
max-width: none !important;
|
|
4660
|
+
min-height: 0 !important;
|
|
4661
|
+
max-height: none !important;
|
|
4662
|
+
overflow: visible !important;
|
|
4663
|
+
border-radius: 12px !important;
|
|
4664
|
+
/* The cage shrinks dock text to 11.5px/18px; the card's rows inherit it
|
|
4665
|
+
and turn cramped. Restore the official 13px body scale. */
|
|
4666
|
+
font-size: 13px !important;
|
|
4667
|
+
line-height: normal !important;
|
|
4668
|
+
}
|
|
4281
4669
|
|
|
4282
4670
|
/* --- 7b. attachment chips (S7.1) ---
|
|
4283
4671
|
Our own dock entry opts OUT of the 26px pill cage above: an image chip is
|
|
@@ -4972,7 +5360,10 @@ exports.CHIPS_CSS = `/* ---------- chips row + settings entry (S5) ---------- */
|
|
|
4972
5360
|
padding: 0 14px;
|
|
4973
5361
|
border: none;
|
|
4974
5362
|
border-radius: 17px;
|
|
4975
|
-
|
|
5363
|
+
/* Same surface as the list rows: the home page is bg-base since the
|
|
5364
|
+
2026-08-20 swap (see home.css.ts), so a bg-base chip would disappear
|
|
5365
|
+
into it. */
|
|
5366
|
+
background: var(--dsw-specific-sidebar-fill, #f5f5f5);
|
|
4976
5367
|
color: var(--dsw-alias-label-primary, inherit);
|
|
4977
5368
|
font-family: inherit;
|
|
4978
5369
|
font-size: 13px;
|
|
@@ -5112,6 +5503,30 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
|
|
|
5112
5503
|
overflow-wrap: anywhere;
|
|
5113
5504
|
}
|
|
5114
5505
|
|
|
5506
|
+
/* Transcript density (2026-08-20): the desktop prose rhythm is 28px of
|
|
5507
|
+
leading on 15px text — a 1.87 ratio that reads generously on a wide
|
|
5508
|
+
column but wastes a phone screen, where the same paragraph is three
|
|
5509
|
+
times as many lines. Unitless 1.65 so nested blocks (code, quotes,
|
|
5510
|
+
lists) recompute from their OWN font-size instead of inheriting a fixed
|
|
5511
|
+
28px that is far too loose for 12-13px chrome text. 1.65 is the floor I
|
|
5512
|
+
am willing to go to for mixed CJK/Latin: the glyphs are full-height with
|
|
5513
|
+
no descender relief, so tighter starts to look stacked. Phone only —
|
|
5514
|
+
the desktop column keeps the official rhythm. */
|
|
5515
|
+
[class*="_markdown_"] {
|
|
5516
|
+
line-height: 1.65;
|
|
5517
|
+
}
|
|
5518
|
+
/* Paragraph rhythm. Each paragraph is its own markdown block and the
|
|
5519
|
+
separation comes from the flex container's row-gap, not from margins —
|
|
5520
|
+
at the stock 16px the paragraph baseline step was 40.75px against a
|
|
5521
|
+
24.75px line step, i.e. 1.65x, which is what reads as airy. 12px brings
|
|
5522
|
+
the step to ~36.75px: still clearly more than one line so paragraphs
|
|
5523
|
+
never run together, but noticeably denser. Scoped by :has() to
|
|
5524
|
+
containers that actually hold prose blocks, so tool cards and other
|
|
5525
|
+
flex bodies with the same hashed suffix keep their own spacing. */
|
|
5526
|
+
[data-phase] [class$="_body"]:has(> [class*="_markdown_"]) {
|
|
5527
|
+
row-gap: 12px !important;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5115
5530
|
/* Markdown tables: the official _tableScroll_ wrapper is already a
|
|
5116
5531
|
horizontal scroll viewport around a width:max-content table, but its
|
|
5117
5532
|
cells are capped at min(30vw, 320px) — barely 120px on a phone, which
|
|
@@ -6274,13 +6689,15 @@ exports.zh = {
|
|
|
6274
6689
|
'infoMode': '模式',
|
|
6275
6690
|
'infoCwdFallback': '未知目录',
|
|
6276
6691
|
'infoSubagents': '{count} 个子代理',
|
|
6692
|
+
'infoJobs': '{count} 个后台任务',
|
|
6693
|
+
'activityOpen': '查看子代理与后台任务',
|
|
6277
6694
|
'infoStatTurns': '轮次',
|
|
6278
6695
|
'infoStatSteps': '步骤',
|
|
6279
6696
|
'infoStatTtft': '首字延迟',
|
|
6280
6697
|
'infoStatLlm': '模型耗时',
|
|
6281
6698
|
'infoStatTool': '工具耗时',
|
|
6282
|
-
'
|
|
6283
|
-
'
|
|
6699
|
+
'infoStatCacheHit': '缓存命中',
|
|
6700
|
+
'infoTokenFlow': 'Token {io}',
|
|
6284
6701
|
'infoExport': '导出日志',
|
|
6285
6702
|
'infoRename': '重命名',
|
|
6286
6703
|
'infoRenamePrompt': '会话新名称',
|
|
@@ -6325,13 +6742,15 @@ exports.en = {
|
|
|
6325
6742
|
'infoMode': 'Mode',
|
|
6326
6743
|
'infoCwdFallback': 'Unknown directory',
|
|
6327
6744
|
'infoSubagents': '{count} subagents',
|
|
6745
|
+
'infoJobs': '{count} background tasks',
|
|
6746
|
+
'activityOpen': 'View subagents and background tasks',
|
|
6328
6747
|
'infoStatTurns': 'Turns',
|
|
6329
6748
|
'infoStatSteps': 'Steps',
|
|
6330
6749
|
'infoStatTtft': 'TTFT',
|
|
6331
6750
|
'infoStatLlm': 'LLM time',
|
|
6332
6751
|
'infoStatTool': 'Tool time',
|
|
6333
|
-
'
|
|
6334
|
-
'
|
|
6752
|
+
'infoStatCacheHit': 'Cache hit',
|
|
6753
|
+
'infoTokenFlow': 'Tokens {io}',
|
|
6335
6754
|
'infoExport': 'Export log',
|
|
6336
6755
|
'infoRename': 'Rename',
|
|
6337
6756
|
'infoRenamePrompt': 'New session name',
|
|
@@ -6628,6 +7047,107 @@ function installTurnFold(ctx) {
|
|
|
6628
7047
|
}, 'dsh-mobile-nav: turn process fold');
|
|
6629
7048
|
}
|
|
6630
7049
|
};
|
|
7050
|
+
__modules["effects/modal-back.js"] = function (require, module, exports) {
|
|
7051
|
+
"use strict";
|
|
7052
|
+
/**
|
|
7053
|
+
* Give Android's back gesture something to close for modals this plugin does
|
|
7054
|
+
* NOT own — the official settings/export dialogs, the composer's permission
|
|
7055
|
+
* and model sheets, third-party panels like dsh-better-sidebar's workbench.
|
|
7056
|
+
*
|
|
7057
|
+
* Those all keep their open state in their own React components with no
|
|
7058
|
+
* public setter, so there is nothing to call. What they do share is a
|
|
7059
|
+
* contract the DOM exposes: while open they are `[aria-modal="true"]`, and
|
|
7060
|
+
* they close on Escape (the standard dismissal every one of these primitives
|
|
7061
|
+
* implements). So: watch for one appearing, stack a history layer, and close
|
|
7062
|
+
* it by sending Escape.
|
|
7063
|
+
*
|
|
7064
|
+
* Anchored on the ARIA contract rather than on any hashed class or plugin
|
|
7065
|
+
* name, so a modal from a plugin nobody has written compat for still gets a
|
|
7066
|
+
* working back gesture.
|
|
7067
|
+
*
|
|
7068
|
+
* Phone only. On desktop the back gesture is not a thing and the official
|
|
7069
|
+
* dialogs keep their own behaviour untouched (the repo's desktop no-op rule).
|
|
7070
|
+
*/
|
|
7071
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7072
|
+
exports.installModalBack = installModalBack;
|
|
7073
|
+
const history_nav_ts_1 = require("./history-nav.js");
|
|
7074
|
+
const PHONE = '(max-width: 767px)';
|
|
7075
|
+
/**
|
|
7076
|
+
* Modals this plugin does NOT own. Every surface the plugin builds itself
|
|
7077
|
+
* carries a `data-mobile-nav` marker and registers its own layer with a real
|
|
7078
|
+
* close callback, so excluding them here is what keeps a sheet from being
|
|
7079
|
+
* counted twice.
|
|
7080
|
+
*
|
|
7081
|
+
* It was counted twice once (2026-08-20, caught on-device): the info card
|
|
7082
|
+
* pushed its own layer AND matched here, so one back press unwound two
|
|
7083
|
+
* entries, the history landed at the root while the session view stayed on
|
|
7084
|
+
* screen, and the next press did nothing at all.
|
|
7085
|
+
*/
|
|
7086
|
+
const MODAL = '[aria-modal="true"]:not([data-mobile-nav])';
|
|
7087
|
+
/** One id per open modal, so nesting (a dialog over a sheet) still stacks. */
|
|
7088
|
+
let seq = 0;
|
|
7089
|
+
function escapeTo(el) {
|
|
7090
|
+
const init = { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true };
|
|
7091
|
+
el.dispatchEvent(new KeyboardEvent('keydown', init));
|
|
7092
|
+
el.dispatchEvent(new KeyboardEvent('keyup', init));
|
|
7093
|
+
}
|
|
7094
|
+
function installModalBack(ctx) {
|
|
7095
|
+
ctx.effect(() => {
|
|
7096
|
+
const phone = window.matchMedia(PHONE);
|
|
7097
|
+
/** Modal element -> the layer id standing in for it. */
|
|
7098
|
+
const tracked = new Map();
|
|
7099
|
+
const sync = () => {
|
|
7100
|
+
if (!phone.matches) {
|
|
7101
|
+
// Leaving the phone breakpoint: drop the layers, keep the modals.
|
|
7102
|
+
for (const id of tracked.values())
|
|
7103
|
+
if ((0, history_nav_ts_1.hasLayer)(id))
|
|
7104
|
+
(0, history_nav_ts_1.popLayer)(id);
|
|
7105
|
+
tracked.clear();
|
|
7106
|
+
return;
|
|
7107
|
+
}
|
|
7108
|
+
const open = new Set(document.querySelectorAll(MODAL));
|
|
7109
|
+
for (const el of open) {
|
|
7110
|
+
if (tracked.has(el))
|
|
7111
|
+
continue;
|
|
7112
|
+
seq += 1;
|
|
7113
|
+
const id = `modal-${seq}`;
|
|
7114
|
+
tracked.set(el, id);
|
|
7115
|
+
(0, history_nav_ts_1.pushLayer)({
|
|
7116
|
+
id,
|
|
7117
|
+
close: () => {
|
|
7118
|
+
tracked.delete(el);
|
|
7119
|
+
// Only meaningful while it is still mounted; a modal the user
|
|
7120
|
+
// already dismissed just consumes its entry silently.
|
|
7121
|
+
if (el.isConnected)
|
|
7122
|
+
escapeTo(el);
|
|
7123
|
+
},
|
|
7124
|
+
});
|
|
7125
|
+
}
|
|
7126
|
+
for (const [el, id] of [...tracked]) {
|
|
7127
|
+
if (open.has(el))
|
|
7128
|
+
continue;
|
|
7129
|
+
// Closed by its own affordance (X, backdrop, Escape). Spend the entry
|
|
7130
|
+
// so the next back press moves the page stack instead of no-opping.
|
|
7131
|
+
tracked.delete(el);
|
|
7132
|
+
if ((0, history_nav_ts_1.hasLayer)(id))
|
|
7133
|
+
(0, history_nav_ts_1.popLayer)(id);
|
|
7134
|
+
}
|
|
7135
|
+
};
|
|
7136
|
+
const observer = new MutationObserver(sync);
|
|
7137
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-modal'] });
|
|
7138
|
+
phone.addEventListener('change', sync);
|
|
7139
|
+
sync();
|
|
7140
|
+
return () => {
|
|
7141
|
+
observer.disconnect();
|
|
7142
|
+
phone.removeEventListener('change', sync);
|
|
7143
|
+
for (const id of tracked.values())
|
|
7144
|
+
if ((0, history_nav_ts_1.hasLayer)(id))
|
|
7145
|
+
(0, history_nav_ts_1.popLayer)(id);
|
|
7146
|
+
tracked.clear();
|
|
7147
|
+
};
|
|
7148
|
+
}, 'dsh-zen-remote: modal back gesture');
|
|
7149
|
+
}
|
|
7150
|
+
};
|
|
6631
7151
|
__modules["effects/welcome-notice.js"] = function (require, module, exports) {
|
|
6632
7152
|
"use strict";
|
|
6633
7153
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -6841,6 +7361,7 @@ const aionui_compat_ts_1 = require("./effects/aionui-compat.js");
|
|
|
6841
7361
|
const header_status_ts_1 = require("./effects/header-status.js");
|
|
6842
7362
|
const gestures_ts_1 = require("./effects/gestures.js");
|
|
6843
7363
|
const turn_fold_ts_1 = require("./effects/turn-fold.js");
|
|
7364
|
+
const modal_back_ts_1 = require("./effects/modal-back.js");
|
|
6844
7365
|
const welcome_notice_ts_1 = require("./effects/welcome-notice.js");
|
|
6845
7366
|
const keyboard_guard_ts_1 = require("./effects/keyboard-guard.js");
|
|
6846
7367
|
const locales_ts_1 = require("./locales.js");
|
|
@@ -6886,6 +7407,7 @@ function apply(ctx) {
|
|
|
6886
7407
|
// effects/turn-fold.ts for why the keyed conversation.chat.node seat could
|
|
6887
7408
|
// not carry this.
|
|
6888
7409
|
(0, turn_fold_ts_1.installTurnFold)(ctx);
|
|
7410
|
+
(0, modal_back_ts_1.installModalBack)(ctx);
|
|
6889
7411
|
// "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
|
|
6890
7412
|
// dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
|
|
6891
7413
|
// per-browser "不再弹出" opt-out instead.
|