Files
apes-sidebar/src/prefs-shared.ts
T
Menghuan1918 d38fefe55d feat(tasks): 任务输出常驻浮动窗 + 节点卡去竖线 + 手机设置
三件事一起落地(用户 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)
2026-09-27 22:18:57 +08:00

200 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)))
}