三件事一起落地(用户 brief),各自独立可验证:
1) 后台任务输出改为常驻浮动窗
- 新增 src/client/FloatingWindow.tsx + .module.css:可复用组件,标题栏(拖拽手柄 +
复制/关闭)、内容区(自滚动,长输出不再跑出视口)、可选底部行、三个拉伸柄
(右/下/右下)。生命周期与 AnchoredPopover 相反:外部点击、失焦、锚点离屏都不关,
只有关闭按钮与 Escape 结束;尺寸只在挂载时量一次(流式输出帧不会挪动读者摆好的窗);
拖拽保持整窗在视口内(放不下才退化到保留 48px 可达);视口变化时夹回。
- 手势复用既有范式:移动走 window 级 pointermove(同 AnchoredPopover),拉伸走
setPointerCapture + createFrameBatcher(同 EditorHost 的面板拉伸)。
- JobOutputPopoverContent → JobOutputWindow;顺带修掉「跟随最新」从不滚动的 bug
(原先滚的是 TerminalBlock 外面那个永不滚动的 div),并清掉随旧弹窗死掉的
.popPre / .jobPopPre 规则。节点/工作流详情与任务窗保持原样。
2) 节点卡统一
- 删除两条左侧竖条(层级条 .cardTop::before、当前会话条 .nodeCurrent::before);
层级改由上段极淡底色承担(7/5/3.5/2.5% → 9/7/5/3.5% 补偿对比度);
当前会话改为 2px accent 描边(「边框加粗高亮」)。
- 圆角 4px → 8px(与浮动窗同一家族);成员/工作流/聚合按用户选择保留类型区分。
- 2px 而非 1.5px:真机 recon 发现 Chromium 在 DPR 1 下把亚像素边框向下取整,
1.5px 的「加粗」完全看不出(实测 borderTopWidth: "1px")。几何不受影响
(border-box + min-height,卡片外框 247×134 与预算逐像素一致)。
3) 手机设置
- 两个新偏好 mobileNoAutoOpen / mobileDefaultTree(默认都开),落到 prefs-shared、
PrefsSchema(volatile 自动)、parsePrefs 校验与设置页新增的「手机」分组。
- 窄屏(沿用 768px 断点)时:新子代理与新后台任务都不再自动激活任务页;
任务页默认树状图(页内切换仍是临时覆盖)。宽屏或开关关掉后行为完全不变。
- 5 条词条 ×20 份词典;外部插件指南的 PrefsSchema 键清单同步。
验证:typecheck / lint ✅;pnpm test 1254 passed / 9 skipped(新增 14 例);
挂载 lane 7/7(真实 DSH 0.1.7-rc.2);3080 真机自测通过(长输出窗口内部滚动
5459px/449px、拖拽 424→304、拉伸 1008×540→918×480、外部点击不关、Escape 关、
卡片 8px/2px/无竖条、窄屏不自动弹出且默认树状图)。
(cherry picked from commit 7d9b428c6c34d28ad2d60c2086496eb533902273)
200 lines
9.0 KiB
TypeScript
200 lines
9.0 KiB
TypeScript
/**
|
||
* Shared "Side card" preference vocabulary (types + constants), consumed by
|
||
* BOTH halves: the host registers the schemastery schema over these values
|
||
* (config.ts) and the client reads/writes them through the settings RPC
|
||
* (client/prefs.ts, client/SideCardSection.tsx). Kept free of schemastery so
|
||
* the browser bundle never pulls the schema runtime in.
|
||
*/
|
||
|
||
/** The user-settings namespace holding the side card preferences. */
|
||
export const SIDEBAR_PREFS_NS = 'dsh-better-sidebar'
|
||
|
||
/** User-facing side card preferences. */
|
||
export interface SidebarPrefs {
|
||
/**
|
||
* Whether the sidebar auto-activates the Tasks page when the current
|
||
* conversation spawns a new subagent.
|
||
*/
|
||
autoOpenSubagent: boolean
|
||
/**
|
||
* Whether the sidebar auto-activates the Tasks page containing the
|
||
* background-jobs section when a NEW job appears for the current
|
||
* conversation (any new job id, not just the first one).
|
||
*/
|
||
autoOpenJobs: boolean
|
||
/**
|
||
* The Tasks page's default presentation: the workflow graph canvas or the
|
||
* classic indentation tree (the in-page toggle still flips it ad hoc).
|
||
*/
|
||
tasksViewMode: 'graph' | 'tree'
|
||
/**
|
||
* MOBILE ADAPTATION (narrow viewports, `isNarrowWidth`): while the viewport
|
||
* is narrow, do not auto-activate the Tasks page for background activity —
|
||
* it suppresses BOTH triggers (`autoOpenSubagent` and `autoOpenJobs`) at
|
||
* once, because on a phone the takeover costs the whole screen. The two
|
||
* individual switches keep their own meaning on wide viewports.
|
||
*/
|
||
mobileNoAutoOpen: boolean
|
||
/**
|
||
* MOBILE ADAPTATION (narrow viewports): open the Tasks page in the classic
|
||
* TREE by default instead of the workflow graph — a narrow screen cannot
|
||
* show a layered graph legibly, while the tree's indentation still reads.
|
||
* It only picks the DEFAULT: the in-page view toggle still flips this
|
||
* session's page ad hoc, and `tasksViewMode` keeps deciding on wide
|
||
* viewports.
|
||
*/
|
||
mobileDefaultTree: boolean
|
||
/**
|
||
* Whether the model-facing `sidebar_open` tool is injected into the
|
||
* model's toolset — one tool that lets the model actively open a local
|
||
* file, a local folder (as a tree rooted there), or an HTTP(S) page in
|
||
* the calling session's sidebar. Off by default: the feature stays
|
||
* dormant until the user explicitly enables it in the side card settings.
|
||
*/
|
||
agentOpenTools: boolean
|
||
/**
|
||
* Whether the editor tab runs in merged mode: a path input replaces the
|
||
* plain header and a toggleable file-tree panel (with a global name
|
||
* search) docks at the tab's right edge. On by default; also makes brand
|
||
* new sessions seed an empty editor tab (tree panel open) instead of the
|
||
* explorer tab. The switch lives under the editor card's gear in the
|
||
* Side card settings; off restores the pre-merge editor exactly.
|
||
*/
|
||
editorExplorer: boolean
|
||
/**
|
||
* Whether the sidebar's filesystem routes enforce the workspace fence:
|
||
* every client-supplied path must resolve (through symlinks) inside the
|
||
* session workspace, else the route answers 403 "outside workspace". On
|
||
* by default; turning it OFF lets the file tree / editor read+write /
|
||
* media / HTML preview / upload routes reach ANY host path (e.g. the
|
||
* global ~/.dsh/AGENTS.md or a linked worktree outside the session cwd)
|
||
* — the trade-off being that any same-origin script (including
|
||
* third-party consumer plugins) can read/write outside the workspace
|
||
* through those routes while it is off. The switch lives under the files
|
||
* tab's gear in the Side card settings; the fence error surfaces offer a
|
||
* one-click global off + retry.
|
||
*/
|
||
workspaceFence: boolean
|
||
/**
|
||
* Title-bar / shell compatibility scheme (the "位置兼容模式" setting):
|
||
* - `auto` (default): CONSERVATIVE — only the standard Window Controls
|
||
* Overlay API (present in frameless Chromium shells that draw the
|
||
* native caption buttons over web content) contributes real geometry;
|
||
* without it nothing is modified, so plain-browser (web) behavior is
|
||
* untouched.
|
||
* - `web`: EXPLICIT "DSH official web" — never adapt, not even WCO
|
||
* geometry (the user declares they run the plain web UI).
|
||
* - `preset`: apply the built-in shell preset named by
|
||
* `titleBarPresetId` (data-driven, opt-in — see shell-presets.ts).
|
||
* - `custom`: apply the free-form `customCss` (and the legacy
|
||
* `titleBarStripPx` strip).
|
||
*/
|
||
titleBarScheme: TitleBarScheme
|
||
/**
|
||
* The built-in shell preset id applied while `titleBarScheme` is
|
||
* `preset` ('' = no preset — nothing extra is applied).
|
||
*/
|
||
titleBarPresetId: string
|
||
/**
|
||
* Free-form CSS injected into the page (last in the cascade, so it can
|
||
* override the plugin's styles; use `!important` to override JS-written
|
||
* inline CSS variables). Applied while `titleBarScheme` is `custom`.
|
||
*/
|
||
customCss: string
|
||
/**
|
||
* LEGACY (kept for read-migration and downgrade mirroring only): position
|
||
* compatibility mode flag. The UI writes `titleBarScheme` instead; a
|
||
* stored `true` without a scheme migrates to the `custom` scheme (with
|
||
* `titleBarStripPx` preserved).
|
||
*/
|
||
titleBarCompat: boolean
|
||
/**
|
||
* LEGACY (kept for read-migration and downgrade mirroring only): the
|
||
* reserved top strip height in px used by the `custom` scheme (0–120,
|
||
* default 40). Drives the `--dsh-title-bar-strip` CSS variable: the
|
||
* toggle cluster drops `strip + 3px` and the right panel's content
|
||
* starts `strip` px below its top edge.
|
||
*/
|
||
titleBarStripPx: number
|
||
/**
|
||
* Whether the HTML previewer drops its sandboxed iframe. Sandbox ON (the
|
||
* default) renders previewed HTML in an opaque-origin iframe that cannot
|
||
* touch the GUI; turning it OFF runs the previewed page with the GUI's
|
||
* own origin — full read/write access to session files and internal
|
||
* APIs. Only for trusted local content; the setting copy warns.
|
||
*/
|
||
htmlViewerNoSandbox: boolean
|
||
/**
|
||
* Whether a newly opened HTML preview starts UNSANDBOXED (the per-surface
|
||
* temporary unlock pre-applied). Off by default: previews open sandboxed
|
||
* and the status row offers the one-tap unlock; when on, previews open
|
||
* in the red unsandboxed state and the status row offers a one-tap
|
||
* restore for the current file.
|
||
*/
|
||
htmlViewerDefaultUnsafe: boolean
|
||
/**
|
||
* Per-tab enable switches, keyed by tab descriptor id (`'explorer'`,
|
||
* `'my-plugin:db'`). An ABSENT key means enabled — only an explicit
|
||
* `false` disables a tab type (hidden from the + menu, `openTab` refuses,
|
||
* and derived flows like subagent auto-open / agent-terminal tabs stop).
|
||
* Already-open tabs of a disabled type keep rendering (closing one
|
||
* prevents reopening), matching the "existing conversations keep their
|
||
* own layouts" rule.
|
||
*/
|
||
tabsEnabled: Record<string, boolean>
|
||
/**
|
||
* Per-viewer enable switches, keyed by file viewer descriptor id
|
||
* (`'image'`, `'my-plugin:csv'`). An ABSENT key means enabled; a disabled
|
||
* viewer is skipped by `matchFileViewer` so files fall through to the
|
||
* next matching viewer (or the download button when none match).
|
||
*/
|
||
viewersEnabled: Record<string, boolean>
|
||
/**
|
||
* Plugin-owned settings blobs (v0.12.0+), keyed by descriptor id: each
|
||
* registered tab/viewer that declares `settings.pluginToggles` (or writes
|
||
* through `settings.render`'s `updatePluginSetting`) persists its values
|
||
* here — an open map, so third-party keys need no host PrefsSchema field.
|
||
* Values are JSON-serializable (the row controls produce strings /
|
||
* numbers / booleans; custom panels are responsible for their own).
|
||
*/
|
||
pluginSettings: Record<string, Record<string, unknown>>
|
||
}
|
||
|
||
/** Range contract of {@link SidebarPrefs.titleBarStripPx}. */
|
||
export const TITLE_BAR_STRIP_MIN = 0
|
||
export const TITLE_BAR_STRIP_MAX = 120
|
||
export const TITLE_BAR_STRIP_DEFAULT = 40
|
||
|
||
/** The title-bar / shell compatibility schemes (see {@link SidebarPrefs.titleBarScheme}). */
|
||
export const TITLE_BAR_SCHEMES = ['auto', 'web', 'preset', 'custom'] as const
|
||
export type TitleBarScheme = typeof TITLE_BAR_SCHEMES[number]
|
||
|
||
/** Fallback prefs used whenever the settings document is unreachable or malformed. */
|
||
export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
||
autoOpenSubagent: true,
|
||
autoOpenJobs: true,
|
||
tasksViewMode: 'graph',
|
||
// Both mobile adaptations are ON by default: a phone is the case they exist
|
||
// for, and each one only ever changes what happens on a NARROW viewport.
|
||
mobileNoAutoOpen: true,
|
||
mobileDefaultTree: true,
|
||
agentOpenTools: false,
|
||
editorExplorer: false,
|
||
workspaceFence: true,
|
||
titleBarScheme: 'auto',
|
||
titleBarPresetId: '',
|
||
customCss: '',
|
||
titleBarCompat: false,
|
||
titleBarStripPx: TITLE_BAR_STRIP_DEFAULT,
|
||
htmlViewerNoSandbox: false,
|
||
htmlViewerDefaultUnsafe: false,
|
||
tabsEnabled: {},
|
||
viewersEnabled: {},
|
||
pluginSettings: {},
|
||
}
|
||
|
||
/** Clamp one title-bar strip height into the contract range (shared by schema and client reads). */
|
||
export function clampTitleBarStrip(value: number): number {
|
||
return Math.min(TITLE_BAR_STRIP_MAX, Math.max(TITLE_BAR_STRIP_MIN, Math.round(value)))
|
||
}
|