@xinxing1708/dsh-codex-style 0.0.0-stage → 0.1.3
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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/THIRD_PARTY_NOTICES.md +21 -0
- package/cordis.patch.yml +3 -0
- package/install.cmd +17 -0
- package/lib/client.js +1453 -0
- package/lib/host.js +3 -0
- package/licenses/DeepSeek-Harness-LICENSE +21 -0
- package/package.json +63 -4
- package/scripts/install.mjs +110 -0
- package/scripts/rollback.mjs +20 -0
- package/skin.json +11 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@xinxing1708/dsh-codex-style",
|
|
3
|
+
factory: function(require) {
|
|
4
|
+
'use strict';
|
|
5
|
+
const CSS_TEXT = "/* 自研界面令牌;尺寸与色值参考界面测量。来源说明见 THIRD_PARTY_NOTICES.md。 */\n\nbody[data-dsh-codex-style] {\n /* ---- 间距 ---- */\n --cx-spacing: 4px;\n --cx-space-1: 4px;\n --cx-space-2: 8px;\n --cx-space-3: 12px;\n --cx-space-4: 16px;\n --cx-space-5: 20px;\n --cx-space-6: 24px;\n --cx-space-8: 32px;\n\n /* ---- 圆角阶梯 ---- */\n --cx-radius-2xs: 2px;\n --cx-radius-xs: 4px;\n --cx-radius-sm: 6px;\n --cx-radius-md: 8px;\n --cx-radius-lg: 10px;\n --cx-radius-xl: 12px;\n --cx-radius-2xl: 16px;\n --cx-radius-3xl: 20px;\n --cx-radius-4xl: 24px;\n --cx-radius-composer: 25px;\n --cx-radius-full: 9999px;\n\n /* ---- 暗色面(深色界面:导航/顶栏 #202020、侧栏 #1b1b1b、对话区 #181818) ---- */\n --cx-window: #202020;\n --cx-sidebar: #1b1b1b;\n --cx-surface: #181818;\n --cx-thread: #171717;\n --cx-nested: #212121;\n --cx-elevated: #282828;\n --cx-summary: #2d2d2d;\n --cx-hover: rgba(255, 255, 255, 0.045);\n --cx-active: rgba(255, 255, 255, 0.08);\n\n /* ---- 边框 ---- */\n --cx-border-l1: rgba(255, 255, 255, 0.08);\n --cx-border-l2: rgba(255, 255, 255, 0.04);\n --cx-border-l3: rgba(255, 255, 255, 0.16);\n --cx-border-strong: rgba(255, 255, 255, 0.16);\n\n /* ---- 文本 ---- */\n --cx-text: #ffffff;\n --cx-text-2: rgba(255, 255, 255, 0.7);\n --cx-text-3: rgba(255, 255, 255, 0.5);\n --cx-text-4: rgba(255, 255, 255, 0.32);\n --cx-text-inverse: #0d0d0d;\n\n /* ---- 强调 / 状态 ---- */\n --cx-accent: #0285ff;\n --cx-accent-soft: #339cff;\n --cx-focus: #339cff;\n --cx-success: #40c977;\n --cx-danger: #ff6764;\n --cx-warning: #ff8549;\n --cx-attention: #3a83f7;\n\n /* ---- diff ---- */\n --cx-diff-add-bg: rgba(64, 201, 119, 0.23);\n --cx-diff-del-bg: rgba(255, 103, 100, 0.23);\n --cx-diff-add-mark: #40c977;\n --cx-diff-del-mark: #ff6764;\n\n /* ---- 字体 ---- */\n --cx-font-sans: -apple-system, system-ui, \"Segoe UI\", sans-serif;\n --cx-font-mono: ui-monospace, \"SFMono-Regular\", \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n\n /* ---- 字号 / 行高 ---- */\n --cx-text-xs: 12px;\n --cx-text-sm: 14px;\n --cx-text-md: 16px;\n --cx-text-lg: 18px;\n --cx-leading-xs: 16px;\n --cx-leading-sm: 22px;\n --cx-leading-md: 24px;\n --cx-leading-lg: 28px;\n\n /* ---- 字重 ---- */\n --cx-weight-regular: 400;\n --cx-weight-medium: 500;\n --cx-weight-semibold: 600;\n --cx-weight-bold: 700;\n --cx-weight-shell: 445; /* 外壳专用非标准字重 */\n\n /* ---- 动效 ---- */\n --cx-dur-fast: 120ms;\n --cx-dur: 150ms;\n --cx-dur-relaxed: 300ms;\n --cx-ease: ease;\n --cx-ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);\n --cx-ease-scroll: cubic-bezier(0.22, 1, 0.36, 1);\n\n /* ---- 布局几何 ----\n 以真实 Codex 窗口截图(1988×1292 @1.5x)实测回填:\n 侧栏 288、标题栏 44、输入栈 710,详见 02-开发日志 第 10 轮。 */\n --cx-titlebar-h: 44px;\n --cx-rail-w: 52px;\n --cx-sidebar-w: 288px;\n --cx-thread-max: 768px;\n --cx-right-w: 667px;\n --cx-header-h: 48px;\n --cx-row-h: 30px;\n --cx-control-h: 28px;\n --cx-send-h: 32px;\n --cx-composer-stack-w: 742px; /* 含左右 16px 内边距 → 内容 710 */\n --cx-composer-h: 97px;\n --cx-picker-h: 37px;\n\n /* ---- 输入区实测色(真实截图取色) ---- */\n --cx-picker-fill: #282828;\n --cx-composer-fill: rgba(55, 55, 55, 0.94);\n\n /* ---- 应用视口 ----\n 真实 DSH 窗口截图 1988×1292(Windows 150% 缩放)换算为 1325×861 CSS。\n 原型按此尺寸渲染,各栏占比才与真实界面一致(旧 1600 视口下主区偏宽)。 */\n --cx-app-w: 1325px;\n --cx-app-h: 861px;\n\n /* ---- 推理强度选择器 ----\n 尺寸/颜色按用户真实 Codex 录屏帧实测(1992×1296 @1.503x)。 */\n --cx-reasoning-w: 255px;\n --cx-reasoning-snap-dur: 140ms; /* 第 20 轮节点切换过渡,待用户验收 */\n --cx-reasoning-fx-dur: 180ms;\n --cx-reasoning-text-dur: 220ms;\n /* 第 23 轮按用户新设计口径增强流速,两个亮带错速运行。 */\n --cx-reasoning-flow-dur: 2.8s;\n --cx-reasoning-bloom-dur: 4.2s;\n --cx-reasoning-blue: #4c9bfe;\n --cx-reasoning-track: #3e3e3e;\n /* 第 22 轮:新录屏 100/660/900/1120 帧取色,深底与两种柔亮色分层。 */\n --cx-reasoning-violet: #5134ad;\n --cx-reasoning-purple: #844fcc;\n --cx-reasoning-max-blue: #3a4ebd;\n --cx-reasoning-max-glow: #bd8dff;\n --cx-reasoning-max-ice: #adaaff;\n --cx-reasoning-max-text: #b98cff;\n --cx-reasoning-title: #4c9bfe;\n /* 浮层/菜单底色:真实录屏帧取色 rgb(45,45,45) */\n --cx-popover-fill: #2d2d2d;\n\n /* ---- 阴影 ---- */\n --cx-shadow-sm: 0 1px 2px -1px rgb(0 0 0 / 0.08);\n --cx-shadow-md: 0 2px 4px -1px rgb(0 0 0 / 0.08);\n --cx-shadow-lg: 0 4px 8px -2px rgb(0 0 0 / 0.1);\n --cx-shadow-xl: 0 8px 16px -4px rgb(0 0 0 / 0.12);\n --cx-shadow-2xl: 0 16px 32px -8px rgb(0 0 0 / 0.19);\n --cx-composer-shadow:\n 0 0 0 0.5px var(--cx-border-strong),\n 0 3px 7.5px rgb(0 0 0 / 0.04),\n 0 0 20px rgb(0 0 0 / 0.05);\n\n /* ---- z 轴 ---- */\n --cx-z-ground: 0;\n --cx-z-panel: 10;\n --cx-z-floating: 20;\n --cx-z-drawer: 40;\n --cx-z-popover: 50;\n --cx-z-overlay: 70;\n}\n\n/* ============ 导航栏 ============ */\n.dsc-rail {\n grid-area: rail;\n position: relative;\n z-index: 30;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 0;\n background: var(--cx-window);\n border-right: 1px solid var(--cx-border-l2);\n}\n.dsc-rail__spacer { flex: 1; }\n.dsc-rail__item {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border-radius: var(--cx-radius-md);\n color: var(--cx-text-3);\n transition: background-color var(--cx-dur-fast) var(--cx-ease), color var(--cx-dur-fast) var(--cx-ease);\n}\n.dsc-rail__item:hover { background: var(--cx-hover); color: var(--cx-text); }\n.dsc-rail__item--active { background: var(--cx-active); color: var(--cx-text); }\n/* 插头先以不透明白色统一描边,再整体降低透明度,避免相交线段叠色发亮。 */\n.dsc-rail__plugin-icon { color: #fff; opacity: .5; stroke-width: 1.5; transition: opacity var(--cx-dur-fast) var(--cx-ease); }\n.dsc-rail__item:hover .dsc-rail__plugin-icon,\n.dsc-rail__item--active .dsc-rail__plugin-icon { opacity: 1; }\n.dsc-rail__item::after {\n content: attr(data-tip);\n position: absolute;\n left: 44px;\n top: 50%;\n z-index: var(--cx-z-popover);\n padding: 4px 8px;\n border-radius: var(--cx-radius-sm);\n border: 1px solid var(--cx-border-l1);\n background: #2b2b2b;\n color: var(--cx-text);\n font-size: 12px;\n line-height: 16px;\n white-space: nowrap;\n box-shadow: var(--cx-shadow-lg);\n opacity: 0;\n transform: translateY(-50%) translateX(-2px);\n pointer-events: none;\n transition: opacity var(--cx-dur-fast) var(--cx-ease), transform var(--cx-dur-fast) var(--cx-ease);\n}\n.dsc-rail__item:hover::after { opacity: 1; transform: translateY(-50%) translateX(0); }\n\n/* ============ 侧栏 ============ */\n.dsc-sidebar {\n grid-area: sidebar;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px;\n background: var(--cx-sidebar);\n border-right: 1px solid var(--cx-border-l2);\n overflow: hidden;\n}\n.dsc-sidebar__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 44px;\n margin: -8px -8px 6px;\n padding: 0 6px 0 10px;\n}\n.dsc-brand { display: flex; align-items: center; gap: 7px; }\n.dsc-brand__mark { display: block; height: 19px; width: auto; }\n.dsc-brand__wordmark {\n font-size: 17px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: var(--cx-text);\n line-height: 1;\n}\n.dsc-brand__badge {\n font-size: 9px;\n font-weight: 700;\n letter-spacing: 0.14em;\n line-height: 1;\n padding: 3px 5px 2px;\n border-radius: 3px;\n background: var(--cx-text);\n color: var(--cx-window);\n}\n.dsc-sidebar__header-actions { display: flex; align-items: center; gap: 2px; }\n.dsc-new-chat {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 32px;\n padding: 0 8px;\n border-radius: var(--cx-radius-md);\n font-size: 13px;\n font-weight: var(--cx-weight-medium);\n color: var(--cx-text);\n background: var(--cx-active);\n transition: background-color var(--cx-dur-fast) var(--cx-ease);\n}\n.dsc-new-chat:hover { background: rgba(255, 255, 255, 0.12); }\n.dsc-new-chat kbd { margin-left: auto; font-size: 11px; color: var(--cx-text-4); }\n\n/* 自有组件与公开 slot/region 锚点;外壳几何适配通过自有 data-dsc-* 标记集中处理。 */\nbody[data-dsh-codex-style] { background: var(--cx-window); color-scheme: dark; --shiki-token-constant: #4dabf7; --shiki-token-string: #69db7c; --shiki-token-comment: #adb5bd; --shiki-token-keyword: #faa2c1; --shiki-token-function: #b197fc; }\n/* 整个三列外壳实际缩窄 52px,让宿主 ResizeObserver、右栏与拖拽使用同一可用宽度。\n 不能用 padding-left 代替:宿主测量的是 getBoundingClientRect().width。 */\nbody[data-dsh-codex-chrome] [data-dsc-frame] { margin-left: var(--cx-rail-w); width: calc(100% - var(--cx-rail-w)); }\n/* 全屏右栏原本按 100vw 展开;扣掉独立导航后,继续尊重宿主二级侧栏的真实宽度。 */\nhtml[data-windows-titlebar] body[data-dsh-codex-chrome] [data-dsc-frame] [data-sidebar-right-panel=\"fullscreen\"] { max-width: max(0px, calc(100vw - var(--cx-rail-w) - var(--dsh-windows-sidebar-width, 0px))); }\n/* data-slot 是 renderer 声明的可寻址面;这里只传递变量,不选择/搬移宿主内部节点。 */\nbody[data-dsh-codex-style] [data-slot=\"conversation.composer.bar\"] { --dsw-specific-input-major: var(--cx-composer-fill); --dsw-radius-panel: var(--cx-radius-composer); --dsw-alias-border-l2: var(--cx-border-strong); --dsw-elevation-soft: var(--cx-composer-shadow); --dsh-composer-card-max-width: 710px; }\n/* DSH 0.2.1-rc.2 中英文发送标签;只覆盖发送色,保留停止按钮与宿主提交状态。 */\nbody[data-dsh-codex-style] [data-slot=\"conversation.composer.bar\"] [data-composer-card] button:is([aria-label=\"发送消息\"], [aria-label=\"排队发送\"], [aria-label=\"插话发送\"], [aria-label=\"Send message\"], [aria-label=\"Queue message\"], [aria-label=\"Steer message\"]) { --dsw-alias-button-info-fill: #dfdfdf; --dsw-alias-button-info-hover: #dfdfdf; color: #363636; opacity: 1; }\nbody[data-dsh-codex-style] [data-slot=\"conversation.composer.bar\"] [data-composer-card] button:is([aria-label=\"发送消息\"], [aria-label=\"排队发送\"], [aria-label=\"插话发送\"], [aria-label=\"Send message\"], [aria-label=\"Queue message\"], [aria-label=\"Steer message\"]):disabled { --dsw-alias-button-info-fill: #8b8b8b; }\n/* 正文字体颜色:按 Codex 截图采样 #dfdfdf(原 #ffffff 偏亮)。仅覆盖对话内容区,不影响按钮、菜单等其它主文本。 */\nbody[data-dsh-codex-style] [data-conversation-content] { --dsw-alias-label-primary: #dfdfdf; }\n/* 原 sidebar/settings 父级继续挂载以保留原生页面与子槽位所有权;只隐藏其旧外观。 */\nbody[data-dsh-codex-chrome] [data-slot=\"sidebar\"] { display: block !important; position: relative; width: 100%; height: 100%; visibility: hidden; }\nbody[data-dsh-codex-chrome] [data-slot=\"conversation.composer.bar\"] { --dsh-composer-side-clearance: 0px; display: flex !important; flex-direction: column; align-items: stretch; align-self: center; width: min(calc(100% - 32px),710px); max-width: 710px; position: relative; }\n/* 空态保留官方编辑器、草稿、附件、“+”与 chain;没有重建输入状态或移动节点。 */\nbody[data-dsh-codex-chrome] [data-conversation-content][data-content-phase=\"hero\"] { visibility: hidden; }\nbody[data-dsh-codex-chrome] [data-conversation-content][data-content-phase=\"hero\"] [data-conversation-region=\"composer\"] { position: absolute; inset: auto 0 0; z-index: 7; }\nbody[data-dsh-codex-chrome] [data-conversation-content][data-content-phase=\"hero\"] [data-slot=\"conversation.composer.bar\"],\nbody[data-dsh-codex-chrome] [data-conversation-content][data-content-phase=\"hero\"] [data-slot=\"conversation.input.dock\"],\nbody[data-dsh-codex-chrome] [data-conversation-content][data-content-phase=\"hero\"] [data-conversation-composer-overlay] { visibility: visible; }\nbody[data-dsh-codex-chrome] [data-conversation-content][data-content-phase=\"hero\"][data-dsc-hero-layout] [data-slot=\"conversation.hero.agentPreset\"] { visibility: visible; display: inline-flex !important; position: fixed; top: calc(var(--dsc-hero-card-top) - 33px); right: calc(100vw - var(--dsc-hero-card-right) + 18px); z-index: 8; height: 28px; max-width: 180px; }\n.dsc-ui { font: 14px/22px var(--cx-font-sans); color: var(--cx-text); box-sizing: border-box; }\n.dsc-ui *, .dsc-ui *::before, .dsc-ui *::after { box-sizing: border-box; }\n.dsc-ui button, .dsc-ui input { font: inherit; color: inherit; }\n.dsc-ui button { border: 0; cursor: pointer; background: none; }\n.dsc-ui button:disabled { cursor: default; opacity: .45; }\n.dsc-ui button:focus-visible, .dsc-ui [role=slider]:focus-visible { outline: 2px solid var(--cx-focus); outline-offset: 2px; }\n.dsc-ui svg { flex: none; }\n.dsc-update-control { display: inline-flex; align-items: center; -webkit-app-region: no-drag; }\n.dsc-update-notice { position: fixed; right: 24px; bottom: 24px; width: 320px; max-width: calc(100vw - 32px); padding: 14px; z-index: 10000; background: var(--cx-popover-fill); border: 1px solid var(--cx-border-l1); border-radius: 14px; box-shadow: 0 12px 36px #0007; animation: dsc-pop-in 130ms ease both; -webkit-app-region: no-drag; }\n.dsc-ui.dsc-update-panel { padding: 14px; }\n.dsc-update-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }\n.dsc-ui .dsc-update-close { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border-radius: 6px; color: var(--cx-text-2); }\n.dsc-update-close:hover { background: var(--cx-hover); color: var(--cx-text); }\n.dsc-update-message { margin: 14px 0 10px; font-weight: 500; }\n.dsc-update-message[data-state=update] { color: #72b4ff; }\n.dsc-update-message[data-state=error] { color: #ff8585; }\n.dsc-update-versions { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin: 0; font-size: 13px; }\n.dsc-update-versions dt { color: var(--cx-text-2); }\n.dsc-update-versions dd { margin: 0; text-align: right; overflow-wrap: anywhere; }\n.dsc-update-note { margin: 10px 0 0; color: var(--cx-text-2); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }\n.dsc-update-note > span { display: block; color: var(--cx-text-3); }\n.dsc-ui .dsc-update-retry { margin-top: 14px; padding: 5px 10px; border: 1px solid var(--cx-border-l3); border-radius: 7px; }\n.dsc-update-retry:hover { background: var(--cx-hover); }\n.dsc-update-spin { animation: dsc-update-spin .9s linear infinite; }\n@keyframes dsc-update-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: reduce) { .dsc-update-spin { animation: none; } }\n.dsc-proto-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; line-height: 1; }\n.dsc-proto-icon svg { display: block; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }\n/* 同一内容 portal 到 body:常驻时跟随宿主列,退场与浮卡不受 0px 列裁剪。只动画透明度和位移。 */\n.dsc-sidebar-shell { visibility: hidden; position: fixed; z-index: 20; min-width: 0; min-height: 0; display: flex; opacity: 0; transform: translateX(-24px); pointer-events: none; transition: opacity 180ms ease, transform 240ms var(--cx-ease-out), visibility 0s 240ms; -webkit-app-region: no-drag; }\n.dsc-sidebar-shell[data-open=true] { visibility: visible; opacity: 1; transform: translateX(0); pointer-events: auto; transition: opacity 180ms ease, transform 260ms var(--cx-ease-out), visibility 0s; }\n.dsc-sidebar-shell[data-mode=docked] > .dsc-sidebar { border-top-left-radius: 16px; }\n.dsc-sidebar-shell[data-mode=floating] { z-index: 40; transform: translateX(-12px); transition: opacity 150ms ease, transform 180ms var(--cx-ease-out), visibility 0s 180ms; }\n.dsc-sidebar-shell[data-mode=floating][data-open=true] { transform: translateX(0); transition-delay: 0s; }\n.dsc-sidebar-shell[data-mode=floating]::before { content: ''; position: absolute; top: 0; bottom: 0; left: -12px; width: 12px; }\n.dsc-sidebar-shell[data-mode=floating] > .dsc-sidebar { --cx-sidebar: #2c2c2c; border: 1px solid var(--cx-border-l1); border-radius: 12px; box-shadow: 0 0 0 3px #18181880, 0 8px 28px #0005; }\n.dsc-sidebar-shell[data-mode=floating][data-open=true] > .dsc-sidebar { animation: dsc-sidebar-content-in 150ms var(--cx-ease-out) both; }\n@keyframes dsc-sidebar-content-in { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: translateX(0); } }\n.dsc-independent-rail { visibility: visible; position: fixed; top: var(--dsh-frame-chrome-top, 0px); bottom: 0; left: 0; width: var(--cx-rail-w); z-index: 25; min-height: 0; padding-bottom: 56px; -webkit-app-region: no-drag; }\n.dsc-extension-navigation-anchor { flex: 1; align-self: stretch; min-height: 0; }\n/* 原生 panellist 继续负责图标注入、选中态、点击和卸载;只重排其可见区域。\n 位置来自自有轨道的剩余空间,超高时独立滚动,不覆盖底部齿轮。\n Windows 原样式在 collapsed 时 display:none,并在开合时淡出,故明确覆盖这两项。 */\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] { visibility: visible; display: flex !important; position: fixed; left: var(--dsc-extension-left); top: var(--dsc-extension-top); width: var(--dsc-extension-width); max-height: var(--dsc-extension-height); min-height: 0; z-index: 26; flex-direction: column; align-items: center; gap: 4px !important; padding: 0; margin: 0 !important; box-sizing: border-box; overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ffffff26 transparent; opacity: 1 !important; transform: none !important; animation: none !important; transition: none !important; -webkit-app-region: no-drag; }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-hidden] { display: none !important; }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-row] { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; min-height: 36px; padding: 0; margin: 0; border: 0; border-radius: var(--cx-radius-md); background: transparent; color: var(--cx-text-3); cursor: pointer; }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-row]:hover { color: var(--cx-text); background: var(--cx-hover); }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-row][aria-current=page] { color: var(--cx-text); background: var(--cx-active); }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-row]:focus-visible { outline: 2px solid var(--cx-focus); outline-offset: -2px; }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-label] { display: none; }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-glyph] { display: inline-flex; align-items: center; justify-content: center; line-height: 1; }\nbody[data-dsh-codex-chrome] [data-dsc-extension-nav] [data-dsc-extension-glyph] svg { display: block; max-width: 20px; max-height: 20px; }\n.dsc-navigation-error { position: fixed; left: calc(var(--cx-rail-w) + 8px); top: var(--dsh-frame-overlay-top, 20px); z-index: 50; max-width: 320px; }\n.dsc-rail .dsc-rail__item { color: var(--cx-text-3); background: transparent; flex: none; padding: 0; }\n.dsc-rail .dsc-rail__item:hover { color: var(--cx-text); background: var(--cx-hover); }\n.dsc-rail .dsc-rail__item--active { color: var(--cx-text); background: var(--cx-active); }\n.dsc-rail__item[data-preview-enabled=true]::after { display: none; }\n.dsc-sidebar { flex: 1; min-width: 0; min-height: 0; overflow: hidden; }\n.dsc-sidebar .dsc-sidebar__header { flex: none; }\n.dsc-sidebar .dsc-brand { min-width: 0; gap: 6px; }\n.dsc-sidebar .dsc-brand__mark { height: 19px; max-width: 25px; }\n.dsc-sidebar .dsc-brand__badge { font-size: 8px; padding: 3px 4px 2px; }\n.dsc-sidebar .dsc-sidebar__header-actions { margin-left: auto; }\n/* Codex:新聊天默认无底色、不显示快捷键,仅悬停时出现灰色背景。 */\n.dsc-sidebar .dsc-new-chat { flex: none; justify-content: flex-start; width: 100%; font-size: 13px; font-weight: var(--cx-weight-medium); background: transparent; color: #c2c2c2; margin-bottom: 8px; }\n.dsc-sidebar .dsc-new-chat:hover { background: rgba(255,255,255,.08); }\n.dsc-sidebar .dsc-workspaces { flex: 1; min-height: 0; height: auto; }\n/* 同时限定两个自有类,避免后面的通用 .dsc-search 把它重新变成伸展项。 */\n.dsc-search.dsc-sidebar-search { flex: none; height: 32px; margin: 2px 0 8px; min-height: 32px; }\n.dsc-sidebar-search button { padding: 0 3px; }\n.dsc-sidebar .dsc-tree-tools { justify-content: space-between; padding: 12px 0 4px 8px; }\n.dsc-tree-tools-label { font-size: 12px; color: var(--cx-text-3); }\n.dsc-projects-toggle { display: flex; align-items: center; gap: 4px; height: 28px; padding: 0; }\n.dsc-projects-toggle svg { color: var(--cx-text-3); transform: rotate(-90deg); transition: transform 180ms ease; }\n.dsc-projects-toggle[aria-expanded=true] svg { transform: rotate(0); }\n.dsc-sidebar .dsc-tree-scroll { padding: 0; scrollbar-color: #ffffff26 transparent; scrollbar-width: thin; }\n.dsc-section-page-title { font-size: 17px; font-weight: 600; }\n.dsc-secondary-nav { display: flex; flex-direction: column; gap: 4px; min-height: 0; overflow: auto; padding-top: 6px; }\n.dsc-secondary-nav .dsc-secondary-item { display: flex; align-items: center; gap: 8px; min-height: 32px; width: 100%; padding: 0 8px; border-radius: var(--cx-radius-md); text-align: left; color: var(--cx-text-2); font-size: 13px; }\n.dsc-secondary-nav .dsc-secondary-item.is-active, .dsc-secondary-nav .dsc-secondary-item:hover { background: var(--cx-active); color: var(--cx-text); }\n.dsc-secondary-item .dsc-proto-icon svg { width: 16px; height: 16px; }\n.dsc-window-sidebar-toggle { visibility: visible; position: fixed; top: 6px; left: 12px; z-index: 35; -webkit-app-region: no-drag; }\n.dsc-collapsed-new-chat { display: none; align-items: center; margin-right: 8px; -webkit-app-region: no-drag; }\nbody[data-dsh-codex-chrome] [data-dsc-frame][data-sidebar-collapsed] .dsc-collapsed-new-chat { display: flex; }\n.dsc-collapsed-new-chat .dsc-icon-button { width: 28px; height: 28px; }\n.dsc-gear-launcher { visibility: visible; position: fixed; left: 8px; bottom: 12px; z-index: 35; }\n.dsc-gear-launcher .dsc-gear-button { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; border-radius: var(--cx-radius-md); color: var(--cx-text-3); }\n.dsc-gear-launcher .dsc-gear-button:hover, .dsc-gear-launcher .dsc-gear-button[aria-expanded=true] { background: var(--cx-hover); color: var(--cx-text); }\n/* 自绘悬停提示,避免系统 title 突然出现;菜单打开时不叠加提示。 */\n.dsc-gear-launcher .dsc-gear-button { position: relative; isolation: isolate; transition: color 150ms ease; }\n.dsc-gear-launcher .dsc-gear-button:hover, .dsc-gear-launcher .dsc-gear-button[aria-expanded=true] { background: none; }\n.dsc-gear-button::before { content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--cx-hover); opacity: 0; transition: opacity 150ms ease; pointer-events: none; }\n.dsc-gear-button:hover::before, .dsc-gear-button[aria-expanded=true]::before, .dsc-gear-button:focus-visible::before { opacity: 1; }\n.dsc-gear-button::after { content: attr(aria-label); position: absolute; left: calc(100% + 8px); top: 50%; padding: 5px 9px; border: 1px solid var(--cx-border-l1); border-radius: 7px; background: var(--cx-popover-fill); color: var(--cx-text); font-size: 12px; line-height: 18px; white-space: nowrap; box-shadow: 0 4px 12px #0004; pointer-events: none; opacity: 0; transform: translate(-3px, -50%); transition: opacity 150ms ease, transform 150ms ease; }\n.dsc-gear-button[aria-expanded=false]:hover::after, .dsc-gear-button[aria-expanded=false]:focus-visible::after { opacity: 1; transform: translate(0, -50%); }\n.dsc-app-menu { padding: 5px; }\n.dsc-menu-separator { height: 1px; margin: 4px 6px; background: var(--cx-border-l1); }\n.dsc-app-menu .dsc-account-row { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px; text-align: left; border-radius: var(--cx-radius-md); }\n.dsc-account-row:hover { background: var(--cx-active); }\n.dsc-account-row > img, .dsc-account-avatar { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 28px; height: 28px; border-radius: 50%; background: var(--cx-active); object-fit: cover; }\n.dsc-account-row > span:last-child { display: flex; flex-direction: column; min-width: 0; }\n.dsc-account-row strong { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsc-account-row small { font-size: 11px; color: var(--cx-text-3); }\n.dsc-empty-hero { visibility: visible; position: fixed; z-index: 2; display: flex; flex-direction: column; align-items: center; gap: 24px; transform: translate(-50%,-50%); pointer-events: none; }\n.dsc-empty-hero svg { color: #525252; }\n.dsc-empty-hero h1 { font-size: 26px; line-height: 34px; font-weight: 400; color: var(--cx-text-2); margin: 0; white-space: nowrap; }\n.dsc-hero-projectbar { visibility: visible; position: fixed; z-index: 6; height: calc(var(--cx-picker-h) + var(--cx-radius-composer)); padding: 4px 4px var(--cx-radius-composer); background: var(--cx-picker-fill); border-radius: 20px; }\n.dsc-hero-projectbar .dsc-project-picker { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; max-width: calc(100% - 180px); color: var(--cx-text-2); font-size: 13px; border-radius: var(--cx-radius-full); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.dsc-hero-projectbar .dsc-project-picker:hover, .dsc-hero-projectbar .dsc-project-picker[aria-expanded=true] { background: var(--cx-active); }\n.dsc-ui button:active:not(:disabled) { transform: translateY(1px); }\n.dsc-brand-mark { width: 25px; height: 20px; object-fit: contain; }\n.dsc-brand { display: inline-flex; align-items: center; gap: 7px; }\n.dsc-brand strong { font-size: 17px; letter-spacing: -.02em; }\n.dsc-brand small { font: 700 8px/16px var(--cx-font-sans); letter-spacing: .12em; padding: 0 4px; color: #131313; background: #eee; border-radius: 3px; }\n.dsc-settings-trigger { display: inline-flex; gap: 8px; align-items: center; }\n.dsc-workspaces { height: 100%; min-height: 0; width: 100%; display: flex; flex-direction: column; background: var(--cx-sidebar); }\n.dsc-tree-tools { display: flex; gap: 4px; align-items: center; padding: 6px 8px 12px; }\n.dsc-search { flex: 1; display: flex; align-items: center; gap: 6px; min-width: 0; border-radius: 8px; background: #ffffff08; padding: 3px 8px; color: var(--cx-text-3); }\n.dsc-search input { min-width: 0; width: 100%; border: 0; outline: none; background: none; font-size: 12px; }\n.dsc-search:focus-within { box-shadow: 0 0 0 1px var(--cx-border-l3); }\n.dsc-search input::placeholder { color: var(--cx-text-3); }\n.dsc-icon-button { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 7px; color: var(--cx-text-2); }\n.dsc-icon-button:hover { background: var(--cx-active); }\n.dsc-tree-scroll { flex: 1; min-height: 0; overflow: auto; scrollbar-width: thin; padding: 0 8px; }\n.dsc-section-title { padding: 12px 8px 6px; font-size: 12px; font-weight: 500; color: var(--cx-text-3); }\n.dsc-thread, .dsc-project { display: flex; align-items: center; height: 30px; border-radius: 7px; position: relative; }\n.dsc-thread:hover, .dsc-project:hover, .dsc-thread.is-active { background: #ffffff0d; }\n.dsc-ui .dsc-thread-main, .dsc-ui .dsc-project-main { display: flex; flex: 1; align-items: center; gap: 7px; min-width: 0; height: 100%; padding: 0 8px; text-align: left; color: #c2c2c2; }\n.dsc-thread-title, .dsc-project-main > span { min-width: 0; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.dsc-ui .dsc-thread-main { padding-right: 62px; }\n.dsc-thread-title-text { display: inline-block; width: max-content; max-width: none; vertical-align: top; }\n.dsc-thread-title[data-overflow=true] { text-overflow: clip; mask-image: linear-gradient(to right, #000 calc(100% - 16px), transparent); }\n.dsc-thread:hover .dsc-thread-title[data-overflow=true] .dsc-thread-title-text,\n.dsc-thread-main:focus-visible .dsc-thread-title[data-overflow=true] .dsc-thread-title-text { animation: dsc-title-scroll var(--dsc-title-duration) linear 250ms infinite alternate; }\n@keyframes dsc-title-scroll { from { transform: translateX(0); } to { transform: translateX(var(--dsc-title-shift)); } }\n.dsc-thread-time { position: absolute; right: 9px; font-size: 10px; color: var(--cx-text-4); transition: opacity 120ms ease; }\n.dsc-thread:hover .dsc-thread-time, .dsc-thread:has(:focus-visible) .dsc-thread-time { display: none; }\n.dsc-thread .dsc-state[data-state=idle] { display: none; }\n.dsc-thread-actions { position: absolute; top: 0; right: 4px; height: 100%; display: flex; align-items: center; gap: 1px; opacity: 0; pointer-events: none; transition: opacity 150ms ease; }\n.dsc-thread:hover .dsc-thread-actions, .dsc-thread:has(:focus-visible) .dsc-thread-actions { opacity: 1; pointer-events: auto; }\n.dsc-ui .dsc-thread-action { display: grid; place-items: center; flex: none; width: 25px; height: 26px; padding: 0; border-radius: 5px; color: #969696; transition: color 120ms ease; }\n.dsc-ui .dsc-thread-action:hover, .dsc-ui .dsc-thread-action:focus-visible { color: #d2d2d2; }\n.dsc-thread-action svg { width: 14px; height: 14px; }\n.dsc-row-tooltip { position: fixed; z-index: 10001; pointer-events: none; padding: 5px 11px; border: 1px solid #ffffff08; border-radius: 13px; color: #dfdfdf; background: #2d2d2d; font-size: 13px; line-height: 18px; white-space: nowrap; box-shadow: 0 2px 5px #0002; opacity: 0; visibility: hidden; transform: translateY(3px); transition: opacity 80ms ease, transform 80ms ease, visibility 0s 80ms; }\n.dsc-row-tooltip[data-open=true] { opacity: 1; visibility: visible; transform: translateY(0); transition: opacity 120ms ease 350ms, transform 120ms ease 350ms, visibility 0s 350ms; }\n.dsc-state { color: var(--cx-text-3); flex: none; display: flex; align-items: center; }\n.dsc-state[data-state=pending] { color: var(--cx-warning); }\n.dsc-state[data-state=running] { color: var(--cx-accent-soft); animation: dsc-status-pulse 1.2s ease-in-out infinite; }\n.dsc-state[data-state=done] { color: var(--cx-success); }\n.dsc-row-action { display: flex; opacity: 0; pointer-events: none; transition: opacity 150ms ease; width: 25px; height: 25px; align-items: center; justify-content: center; border-radius: 5px; padding: 0; margin-right: 3px; color: var(--cx-text-3); }\n.dsc-thread:hover .dsc-row-action, .dsc-project:hover .dsc-row-action, .dsc-thread:has(:focus-visible) .dsc-row-action, .dsc-project:has(:focus-visible) .dsc-row-action { opacity: 1; pointer-events: auto; }\n.dsc-row-action:hover { color: #fff; background: var(--cx-active); }\n/* 本轮按用户录像为项目内容局部开放布局动画;外壳与其他面板的尺寸规则保持原约定。 */\n.dsc-project-fold { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 180ms cubic-bezier(.2,.8,.2,1), opacity 150ms ease; }\n.dsc-project-fold[data-open=true] { grid-template-rows: 1fr; opacity: 1; }\n.dsc-project-clip { min-height: 0; overflow: hidden; }\n.dsc-project-children { padding-left: 14px; }\n.dsc-project-children, .dsc-project-list { transform: translateY(-4px); transition: transform 180ms cubic-bezier(.2,.8,.2,1); }\n.dsc-project-fold[data-open=true] > .dsc-project-clip > :is(.dsc-project-children, .dsc-project-list) { transform: translateY(0); }\n.dsc-project-children .dsc-state { display: none; }\n.dsc-empty { display: block; color: var(--cx-text-3); padding: 10px 12px; font-size: 12px; }\n.dsc-archive-toggle { padding: 8px 12px; display: flex; gap: 6px; align-items: center; font-size: 11px; color: var(--cx-text-3); }\n.dsc-archive-toggle input { accent-color: var(--cx-accent); }\n.dsc-thread.is-archived { opacity: .55; }\n.dsc-error { padding: 4px 8px; color: var(--cx-danger); font: 12px/17px var(--cx-font-sans); white-space: normal; max-width: 310px; }\n.dsc-menu { z-index: 10000; padding: 6px; background: var(--cx-popover-fill); border: 1px solid var(--cx-border-l1); border-radius: 14px; box-shadow: 0 12px 36px #0007; overflow: auto; animation: dsc-pop-in 130ms ease both; }\n.dsc-menu-heading { font-size: 12px; color: var(--cx-text-3); padding: 5px 8px 8px; }\n.dsc-option { width: 100%; display: flex; gap: 9px; align-items: center; text-align: left; padding: 7px 8px; border-radius: 7px; min-height: 32px; }\n.dsc-option:hover, .dsc-option:focus-visible { background: #ffffff12; }\n.dsc-option-copy { display: flex; flex: 1; min-width: 0; flex-direction: column; line-height: 20px; }\n.dsc-option-copy small { font-size: 12px; color: var(--cx-text-3); line-height: 17px; }\n/* 行悬停详情卡:几何按原型实测(项目卡 321×130、右侧 5px;会话卡 470 宽、右侧 12px、上移 6px)。 */\n.dsc-row-pop { position: fixed; z-index: 10000; max-width: calc(100vw - 32px); padding: 6px; border-radius: var(--cx-radius-xl); border: 1px solid var(--cx-border-l1); background: var(--cx-popover-fill); box-shadow: var(--cx-shadow-2xl); animation: dsc-row-pop-in 120ms var(--cx-ease-out) both; }\n.dsc-row-pop--project { width: 321px; padding: 8px; }\n.dsc-row-pop--thread { width: 470px; }\n.dsc-row-pop__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 8px 8px 10px; border-bottom: 1px solid var(--cx-border-l2); }\n.dsc-row-pop--project .dsc-row-pop__head { align-items: center; height: 28px; padding: 0 8px; border-bottom: 0; }\n.dsc-row-pop__name { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 14px; font-weight: var(--cx-weight-regular); color: var(--cx-text); }\n.dsc-row-pop__name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsc-row-pop__pin { display: inline-flex; align-items: center; flex: none; color: var(--cx-text-3); }\n.dsc-row-pop__excerpt { margin: 0; font-size: 13px; line-height: 20px; color: var(--cx-text); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }\n.dsc-row-pop__meta { display: flex; align-items: center; gap: 6px; flex: none; font-size: 12px; color: var(--cx-text-3); white-space: nowrap; }\n.dsc-row-pop__line { display: flex; align-items: center; gap: 8px; width: 100%; height: 32px; padding: 0 8px; border-radius: var(--cx-radius-md); font-size: 13px; color: var(--cx-text-2); text-align: left; }\n.dsc-row-pop__line svg { flex: none; color: var(--cx-text-3); }\n.dsc-row-pop--project .dsc-row-pop__line { height: 28px; font-size: 14px; color: var(--cx-text); }\n.dsc-row-pop--project .dsc-row-pop__line svg { color: var(--cx-text-2); }\n.dsc-row-pop__line--path, .dsc-row-pop__line--action { cursor: pointer; }\n.dsc-row-pop__line--path:hover, .dsc-row-pop__line--action:hover { background: var(--cx-hover); color: var(--cx-text); }\n.dsc-row-pop__line--path:focus-visible, .dsc-row-pop__line--action:focus-visible { outline: 2px solid var(--cx-focus); outline-offset: -2px; }\n.dsc-row-pop__path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsc-row-pop--project .dsc-row-pop__path { font-family: var(--cx-font-sans); font-size: 14px; color: var(--cx-text); }\n.dsc-row-pop--thread .dsc-row-pop__path { font-family: var(--cx-font-mono); font-size: 12px; }\n.dsc-row-pop__arrow { margin-left: auto; opacity: 0; transition: opacity 120ms var(--cx-ease); }\n.dsc-row-pop__line--path:hover .dsc-row-pop__arrow, .dsc-row-pop__line--path:focus-visible .dsc-row-pop__arrow { opacity: 1; }\n.dsc-row-pop__sep { height: 1px; background: var(--cx-border-l1); }\n@keyframes dsc-row-pop-in { from { opacity: 0; } to { opacity: 1; } }\n/* 「编辑项目」对话框:按参考图(面板 #2d2d2d、16 圆角、保存白底、移除暗红底);源文件夹只读展示。 */\n.dsc-modal { position: fixed; inset: 0; z-index: 11000; display: flex; align-items: center; justify-content: center; padding: 24px; }\n.dsc-modal__mask { position: absolute; inset: 0; background: rgba(0,0,0,.12); }\n.dsc-modal__panel { position: relative; display: flex; flex-direction: column; width: min(520px, calc(100vw - 32px)); padding: 24px; border-radius: 16px; background: var(--cx-popover-fill); box-shadow: var(--cx-shadow-2xl); color: var(--cx-text); }\n.dsc-modal__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }\n.dsc-modal__head h2 { margin: 0; font-size: 20px; line-height: 28px; font-weight: 600; }\n.dsc-modal__close { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border-radius: var(--cx-radius-sm); color: var(--cx-text-2); }\n.dsc-modal__close:hover { background: var(--cx-hover); color: var(--cx-text); }\n.dsc-modal__field { display: flex; align-items: center; gap: 10px; height: 44px; margin-top: 20px; padding: 0 12px; border: 1px solid var(--cx-border-strong); border-radius: var(--cx-radius-md); color: var(--cx-text-2); }\n.dsc-modal__field:focus-within { border-color: var(--cx-focus); }\n.dsc-modal__input { flex: 1; min-width: 0; height: 100%; border: 0; outline: none; background: none; font: 15px/22px var(--cx-font-sans); color: var(--cx-text); }\n.dsc-modal__input::placeholder { color: var(--cx-text-3); }\n.dsc-modal__label { margin: 20px 0 8px; font-size: 13px; line-height: 18px; color: var(--cx-text-3); }\n.dsc-modal__source { display: flex; align-items: center; gap: 10px; height: 48px; padding: 0 12px; border: 1px solid var(--cx-border-strong); border-radius: var(--cx-radius-md); color: var(--cx-text-2); font-size: 14px; }\n.dsc-modal__path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsc-modal .dsc-error { margin-top: 12px; }\n.dsc-modal__actions { display: flex; align-items: center; gap: 8px; margin-top: 24px; }\n.dsc-modal__spacer { flex: 1; }\n.dsc-modal__remove { height: 36px; padding: 0 16px; border-radius: 999px; background: rgba(250,102,101,.14); color: #fa6665; font-size: 14px; }\n.dsc-modal__remove:hover:not(:disabled) { background: rgba(250,102,101,.22); }\n.dsc-modal__cancel { height: 36px; padding: 0 14px; border-radius: 999px; color: var(--cx-text-2); font-size: 14px; }\n.dsc-modal__cancel:hover:not(:disabled) { background: var(--cx-hover); color: var(--cx-text); }\n.dsc-modal__save { height: 36px; padding: 0 20px; border-radius: 999px; background: #dfdfdf; color: #131313; font-size: 14px; font-weight: 500; }\n.dsc-modal__save:hover:not(:disabled) { background: #fff; }\n.dsc-modal__save:disabled, .dsc-modal__remove:disabled, .dsc-modal__cancel:disabled { opacity: .6; cursor: default; }\n.dsc-permission, .dsc-model { position: relative; display: inline-flex; align-items: center; min-width: 0; }\n.dsc-model { gap: 2px; }\n.dsc-control-status { display: inline-flex; align-items: center; gap: 5px; padding: 0 7px; color: var(--cx-text-3); font-size: 11px; }\n.dsc-control-status button { padding: 2px 4px; text-decoration: underline; }\n.dsc-control-status button:hover { color: var(--cx-text); }\n.dsc-pill { height: 28px; display: inline-flex; align-items: center; gap: 6px; padding: 0 10px; border-radius: 999px; white-space: nowrap; color: var(--cx-text-2); font-size: 14px !important; }\n.dsc-pill:hover, .dsc-pill[aria-expanded=true] { background: #ffffff12; color: white; }\n.dsc-pill[data-warning=true] { color: var(--cx-warning); }\n.dsc-model-trigger { max-width: 260px; gap: 5px; }\n/* 右下角触发器:模型名偏白、思维强度灰色(参考图 4)。 */\n.dsc-model-trigger .dsc-model-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; color: #dfdfdf; }\n.dsc-model-trigger .dsc-effort-name { flex: none; color: #989898; }\n.dsc-model-trigger svg { flex: none; color: #989898; }\n/* 「修改模型」面板:标题与列表样式明确区别于宿主原生菜单。 */\n.dsc-menu.dsc-model-panel { padding: 8px; }\n.dsc-menu.dsc-model-panel .dsc-menu-heading { font-size: 14px; line-height: 20px; color: var(--cx-text); font-weight: 500; padding: 6px 8px 10px; }\n.dsc-model-group { color: var(--cx-text-3); font-size: 11px; padding: 7px 8px 2px; }\n.dsc-model-search { border: 0; border-bottom: 1px solid var(--cx-border-l1); background: none; outline: none; padding: 6px 8px; width: 100%; margin-bottom: 3px; }\n.dsc-back { display: flex; align-items: center; gap: 4px; color: var(--cx-text-3); font-size: 12px !important; border-radius: 5px; }\n.dsc-back:hover { color: #fff; }\n/* 滑条弹层裁掉横向溢出:Max/Low/High 档的撒点动画会向右超出边界,触发多余的水平滚动条。 */\n.dsc-reasoning-pop { padding: 12px; border-radius: var(--cx-radius-2xl); box-shadow: var(--cx-shadow-2xl); overflow: hidden; animation: dsc-reasoning-in 130ms var(--cx-ease) both; }\n.dsc-reasoning { position: relative; }\n.dsc-reasoning-heading { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0 20px 10px; text-align: center; }\n.dsc-reasoning-heading strong { font-size: 14px; font-weight: var(--cx-weight-semibold); line-height: 18px; color: var(--cx-reasoning-title); transition: color 150ms ease; }\n.dsc-reasoning-model { display: inline-flex; align-items: center; justify-content: center; gap: 2px; max-width: 100%; padding: 4px 10px; border-radius: var(--cx-radius-md); color: #ffffff9e !important; font-size: 12px !important; line-height: 16px !important; white-space: nowrap; transition: background-color 120ms var(--cx-ease), color 120ms var(--cx-ease); }\n.dsc-reasoning-model:hover { background: #3c3b3b; color: var(--cx-text) !important; }\n.dsc-reasoning-reset { position: absolute; top: -4px; right: -4px; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: var(--cx-radius-sm); color: var(--cx-text-3) !important; }\n.dsc-reasoning-reset:hover { background: var(--cx-hover); color: var(--cx-text) !important; }\n.dsc-reasoning[data-level=max] .dsc-reasoning-heading strong { color: var(--cx-reasoning-max-text); }\n.dsc-slider { height: 32px; position: relative; margin: 0 2px; border-radius: 999px; cursor: pointer; touch-action: none; outline: none; }\n/* 提交期间保持原有视觉:只禁用拖拽,不切换光标/透明度,避免切换档位时闪变。 */\n.dsc-slider[aria-disabled=true] { cursor: default; opacity: 1; }\n.dsc-slider-track { position: absolute; inset: 4px 0; height: 24px; border-radius: 999px; background: var(--cx-reasoning-track); overflow: hidden; }\n.dsc-slider-fill { position: absolute; inset: 0; border-radius: inherit; background: var(--cx-reasoning-blue); transform-origin: left center; transition: transform 140ms var(--cx-ease-scroll); }\n.dsc-slider-max { position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(90deg,var(--cx-reasoning-max-blue),var(--cx-reasoning-purple) 52%,var(--cx-reasoning-violet)); opacity: 0; transition: opacity 180ms ease; }\n.dsc-slider[data-level=max] .dsc-slider-max { opacity: 1; }\n.dsc-slider-max::before, .dsc-slider-max::after { content: ''; position: absolute; animation-play-state: paused; }\n.dsc-slider-max::before { inset: -50% -25%; background: radial-gradient(ellipse,var(--cx-reasoning-max-ice),#a095ffd9 22%,transparent 58%); animation: dsc-max-flow 2.8s cubic-bezier(.4,0,.6,1) -.45s infinite alternate; animation-play-state: paused; }\n.dsc-slider-max::after { inset: -40% -30%; background: radial-gradient(ellipse,#d6a2ff,var(--cx-reasoning-max-glow) 20%,transparent 55%); animation: dsc-max-bloom 4.2s cubic-bezier(.4,0,.6,1) -1.65s infinite alternate; animation-play-state: paused; }\n.dsc-slider[data-level=max] .dsc-slider-max::before, .dsc-slider[data-level=max] .dsc-slider-max::after { animation-play-state: running; }\n.dsc-stars { position: absolute; inset: 0; opacity: 0; pointer-events: none; transition: opacity 180ms ease; }\n.dsc-slider[data-level=max] .dsc-stars { opacity: 1; }\n.dsc-stars i { position: absolute; border-radius: 50%; background: #fff; box-shadow: 0 0 3px #e0d6ff8c; animation: dsc-star-drift var(--pdur) cubic-bezier(.4,0,.6,1) var(--pdelay) infinite alternate; animation-play-state: paused; }\n.dsc-slider[data-level=max] .dsc-stars i { animation-play-state: running; }\n.dsc-tick { position: absolute; top: 50%; width: 5px; height: 5px; border-radius: 50%; background: #ffffff38; transform: translate(-50%,-50%); transition: opacity 150ms ease,transform 120ms ease; }\n.dsc-tick.is-on { background: #ffffff4d; }\n.dsc-slider[data-level=max] .dsc-tick { opacity: 0; }\n.dsc-thumb-position { position: absolute; inset: 0; pointer-events: none; transition: transform 140ms var(--cx-ease-scroll); }\n.dsc-thumb { position: absolute; top: 50%; left: 14px; width: 28px; height: 28px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px #0007,0 0 0 1px #0000000f; transform: translate(-50%,-50%); pointer-events: auto; transition: transform 140ms var(--cx-ease-out),box-shadow 140ms var(--cx-ease); }\n/* 固定白色滑块为正圆,避免宿主全局圆角规则覆盖插件外形。 */\n.dsc-ui .dsc-slider .dsc-thumb { border-radius: 50% !important; }\n.dsc-thumb:hover { transform: translate(-50%,-50%) scale(1.1); box-shadow: 0 1px 6px #0008,0 0 0 1px #0000000f; }\n.dsc-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }\n.dsc-permission-menu { padding: 4px; border-radius: var(--cx-radius-2xl); box-shadow: var(--cx-shadow-2xl); }\n.dsc-permission-heading { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 4px 8px; color: var(--cx-text-3); font-size: 13px; line-height: 18px; }\n.dsc-permission-heading button { padding: 0; white-space: nowrap; text-decoration: underline; text-underline-offset: 2px; }\n.dsc-permission-help { margin: 4px 8px 8px; color: var(--cx-text-2); font-size: 12px; line-height: 18px; }\n.dsc-permission-menu .dsc-option { align-items: flex-start; gap: 8px; padding: 6px 8px; border-radius: var(--cx-radius-xl); color: var(--cx-text-2); font-size: 14px; line-height: 19px; }\n.dsc-permission-menu .dsc-option > svg { margin-top: 3px; }\n.dsc-permission-menu .dsc-option-copy { line-height: 19px; }\n.dsc-permission-menu .dsc-option[data-warning=true], .dsc-permission-menu .dsc-option[data-warning=true] small { color: var(--cx-warning); }\n.dsc-burst { position: absolute; left: 14px; top: 50%; }\n.dsc-burst i { position: absolute; width: 3px; height: 3px; border-radius: 50%; background: #b98cff; animation: dsc-burst 280ms cubic-bezier(.22,1,.36,1) both; }\n.dsc-slider-labels { display: flex; justify-content: space-between; padding-top: 3px; }\n.dsc-slider-labels button { padding: 0 3px; font-size: 10px; color: var(--cx-text-3); }\n.dsc-slider-labels button:hover { color: white; }\n.dsc-reasoning-description { display: block; text-align: center; color: var(--cx-text-3); font-size: 11px; line-height: 16px; margin-top: 7px; }\n@keyframes dsc-pop-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }\n/* 退场淡出:关闭弹层时先播放 160ms,再卸载 DOM。 */\n@keyframes dsc-pop-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(3px); } }\n.dsc-menu.dsc-menu-closing { animation: dsc-pop-out 160ms ease both; pointer-events: none; }\n@keyframes dsc-reasoning-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes dsc-status-pulse { 50% { opacity: .4; } }\n@keyframes dsc-max-flow { from { transform: translateX(-32%) scale(.82,1); opacity: .95; } to { transform: translateX(32%) scale(1.02,1); opacity: .8; } }\n@keyframes dsc-max-bloom { from { transform: translateX(28%) scale(.7,1); opacity: .9; } to { transform: translateX(-28%) scale(1,1); opacity: 1; } }\n@keyframes dsc-star-drift { from { transform: translate(-50%,-50%) translate(calc(var(--dx)*-.5),calc(var(--dy)*-.5)); opacity: .45; } to { transform: translate(-50%,-50%) translate(calc(var(--dx)*.5),calc(var(--dy)*.5)); opacity: .8; } }\n@keyframes dsc-burst { 0% { transform: translate(-50%,-50%) scale(.45); opacity: 0; } 22% { opacity: .65; } 100% { transform: translate(-50%,-50%) translate(var(--bx),var(--by)) scale(.45); opacity: 0; } }\n@media (prefers-reduced-motion: reduce) {\n .dsc-sidebar-shell, .dsc-row-tooltip, .dsc-ui *, .dsc-ui *::before, .dsc-ui *::after { animation: none !important; transition: none !important; }\n .dsc-burst { display: none; }\n}\n\n/* Codex 几何(第 48 轮):\n 1) 侧边栏列左上角圆角、对话列左上角直角(与 Codex 蓝框/红框一致);\n 2) 对话正文列与输入框同宽:由插件测量输入框卡片的实际视觉宽度写入 --dsc-composer-visual-width,\n 正文列优先取该值,回退到宿主 --dsh-composer-card-max-width(正文 + 32px)。 */\nbody[data-dsh-codex-style] [data-conversation-scroll] [class*=\"_column\"] { max-width: var(--dsc-composer-visual-width, var(--dsh-composer-card-max-width)); }\nhtml[data-windows-titlebar] body[data-dsh-codex-chrome] [data-dsc-sidebar-column] { border-top-left-radius: var(--dsh-windows-content-radius, 16px); }\nhtml[data-windows-titlebar] body[data-dsh-codex-chrome] [data-dsc-center-column] { border-radius: 0; }\nhtml[data-windows-titlebar] body[data-dsh-codex-chrome] [data-dsc-frame][data-sidebar-collapsed] [data-dsc-center-column] { border-top-left-radius: var(--dsh-windows-content-radius, 16px); }\n";
|
|
6
|
+
const PROTOTYPE_ICONS = {"home":"<svg viewBox=\"0 0 16 16\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M3 7.2 8 3.2l5 4v5.3a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z\"/></svg>","tasks":"<svg viewBox=\"0 0 16 16\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"8\" cy=\"8\" r=\"5.5\"/><path d=\"M8 5v3l2 1.5\"/></svg>","plugins":"<svg class=\"dsc-rail__plugin-icon\" viewBox=\"0 0 20 20\" width=\"20\" height=\"20\" stroke-width=\"1.5\" aria-hidden=\"true\"><path d=\"M7.5 3v4m5-4v4M5.5 7h9v2.8a4.5 4.5 0 0 1-9 0V7ZM10 14.3v3.2\"/></svg>","settings":"<svg viewBox=\"0 0 20 20\" width=\"18\" height=\"18\" stroke-width=\"1.4\" aria-hidden=\"true\">\n <path d=\"M8.80 3.20 L9.07 1.85 A8.20 8.20 0 0 1 10.93 1.85 L11.20 3.20 A6.90 6.90 0 0 1 13.96 4.35 L15.10 3.58 A8.20 8.20 0 0 1 16.42 4.90 L15.65 6.04 A6.90 6.90 0 0 1 16.80 8.80 L18.15 9.07 A8.20 8.20 0 0 1 18.15 10.93 L16.80 11.20 A6.90 6.90 0 0 1 15.65 13.96 L16.42 15.10 A8.20 8.20 0 0 1 15.10 16.42 L13.96 15.65 A6.90 6.90 0 0 1 11.20 16.80 L10.93 18.15 A8.20 8.20 0 0 1 9.07 18.15 L8.80 16.80 A6.90 6.90 0 0 1 6.04 15.65 L4.90 16.42 A8.20 8.20 0 0 1 3.58 15.10 L4.35 13.96 A6.90 6.90 0 0 1 3.20 11.20 L1.85 10.93 A8.20 8.20 0 0 1 1.85 9.07 L3.20 8.80 A6.90 6.90 0 0 1 4.35 6.04 L3.58 4.90 A8.20 8.20 0 0 1 4.90 3.58 L6.04 4.35 A6.90 6.90 0 0 1 8.80 3.20\"/>\n <circle cx=\"10\" cy=\"10\" r=\"2.3\"/>\n </svg>","newChat":"<svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8.1 2.8H4.6a2 2 0 0 0-2 2v6.6a2 2 0 0 0 2 2h6.6a2 2 0 0 0 2-2V7.9\"/><path d=\"m10.9 2.7-5.3 5.3L5 10.4l2.4-.6 5.3-5.3a1.3 1.3 0 0 0-1.8-1.8ZM9.8 3.8l1.8 1.8\"/></svg>"};
|
|
7
|
+
const BRAND_MARK = "data:image/png;base64,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";
|
|
8
|
+
const PLUGIN_VERSION = "0.1.3";
|
|
9
|
+
const PLUGIN_PACKAGE = "@xinxing1708/dsh-codex-style";
|
|
10
|
+
const module = { exports: {} };
|
|
11
|
+
const exports = module.exports;
|
|
12
|
+
// 保留官方 sidebar/settings/composer 的所有者;导航在 shell.overlay 独立挂载。
|
|
13
|
+
// 二级侧栏复用同一内容与真实动作;收起态只改变呈现方式,不导航或展开宿主列。
|
|
14
|
+
const DEFAULT_SIDEBAR_WIDTH = 288;
|
|
15
|
+
|
|
16
|
+
// 0.2.1-rc.2 的 root store handle.create() 返回 AppFrame 已持有的同一布局实例。
|
|
17
|
+
// 仅将启动/重新展开时的默认值 280 改为 288;真实 grid、分隔条和宽度变量仍由宿主计算。
|
|
18
|
+
function initializeSidebarWidth(ctx) {
|
|
19
|
+
const entry = ctx.slots.entriesOfSlot('root')[0];
|
|
20
|
+
const store = entry?.store?.create();
|
|
21
|
+
if (!store?.actions?.setSidebar || !store.getSnapshot().layoutInfo) {
|
|
22
|
+
console.warn('codex-style: 当前 root 未提供已核实的侧栏宽度动作');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let previous = store.getSnapshot().layoutInfo.sidebar;
|
|
26
|
+
let owned = false;
|
|
27
|
+
const sync = initial => {
|
|
28
|
+
const width = store.getSnapshot().layoutInfo.sidebar;
|
|
29
|
+
const reopening = previous === 0 && width === 280;
|
|
30
|
+
if (width !== previous) owned = false;
|
|
31
|
+
previous = width;
|
|
32
|
+
if (width !== 280 || (!initial && !reopening)) return;
|
|
33
|
+
// 先更新本地记录,避免 setSidebar 的同步通知被误认为用户拖动。
|
|
34
|
+
previous = DEFAULT_SIDEBAR_WIDTH;
|
|
35
|
+
owned = true;
|
|
36
|
+
store.actions.setSidebar(DEFAULT_SIDEBAR_WIDTH);
|
|
37
|
+
};
|
|
38
|
+
const unsubscribe = store.subscribe(() => sync(false));
|
|
39
|
+
sync(true);
|
|
40
|
+
return () => {
|
|
41
|
+
// 卸载时只恢复本次仍在生效的默认值;保留用户后续拖动值和 0px 收起态。
|
|
42
|
+
unsubscribe();
|
|
43
|
+
if (owned && store.getSnapshot().layoutInfo.sidebar === DEFAULT_SIDEBAR_WIDTH) store.actions.setSidebar(280);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function PrototypeIcon({ name, className = '' }) {
|
|
48
|
+
return h('span', { className: 'dsc-proto-icon ' + className, 'aria-hidden': true, dangerouslySetInnerHTML: { __html: PROTOTYPE_ICONS[name] } });
|
|
49
|
+
}
|
|
50
|
+
function useSlotVersion(ctx, names) {
|
|
51
|
+
const axis = names.join('|');
|
|
52
|
+
const subscribe = useCallback(listener => {
|
|
53
|
+
const disposers = names.map(name => ctx.slots.subscribe(name, listener));
|
|
54
|
+
return () => disposers.forEach(dispose => dispose());
|
|
55
|
+
}, [ctx, axis]);
|
|
56
|
+
const snapshot = useCallback(() => names.map(name => ctx.slots.getVersion(name)).join(':'), [ctx, axis]);
|
|
57
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
58
|
+
}
|
|
59
|
+
function usePanelDirectory(ctx) {
|
|
60
|
+
const version = useSlotVersion(ctx, ['main', 'sidebar.panellist']);
|
|
61
|
+
const locale = useSource(ctx.locale);
|
|
62
|
+
return useMemo(() => {
|
|
63
|
+
const mains = new Set(ctx.slots.entriesOfSlot('main').map(entry => entry.options.key));
|
|
64
|
+
return ctx.slots.entriesOfSlot('sidebar.panellist').map(({ options }) => ({
|
|
65
|
+
id: options.id, order: options.order ?? 0,
|
|
66
|
+
label: (typeof options.label === 'function' ? options.label() : options.label) ?? options.id,
|
|
67
|
+
available: mains.has(options.id)
|
|
68
|
+
})).sort((a, b) => a.order - b.order);
|
|
69
|
+
}, [ctx, version, locale]);
|
|
70
|
+
}
|
|
71
|
+
function useNavigationDirectory(ctx) {
|
|
72
|
+
const panel = useSource(ctx.layout.panelInfo);
|
|
73
|
+
const panels = usePanelDirectory(ctx);
|
|
74
|
+
// 官方分组只按已核实的 main id 识别;社区入口的名称不能决定它属于自定义。
|
|
75
|
+
const pluginRows = panels.filter(row => row.available && row.id === 'plugins');
|
|
76
|
+
const taskRows = panels.filter(row => row.available && row.id === 'schedules');
|
|
77
|
+
const category = panel.activePanelId === null ? 'home' : panel.activePanelId === 'plugins' ? 'plugins' : panel.activePanelId === 'schedules' ? 'tasks' : 'extension';
|
|
78
|
+
return { panel, panels, pluginRows, taskRows, category };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 原 sidebar entry 拥有 panellist 的 renderSlot 授权。保留原按钮、图标及注入,
|
|
82
|
+
// 仅以公开 slot 锚点定位原 nav,给第三方行添加可释放标记并用 CSS 呈现在轨道中。
|
|
83
|
+
// 不复制 component、不借用私有 renderer、不搬移 React 所拥有的 DOM。
|
|
84
|
+
function useNativeExtensionNavigation(sidebar, target, panels) {
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
const host = document.querySelector('[data-slot="sidebar"]');
|
|
87
|
+
if (!host || !target.current) return;
|
|
88
|
+
const attributes = new Map();
|
|
89
|
+
const properties = new Map();
|
|
90
|
+
let nav;
|
|
91
|
+
let frame;
|
|
92
|
+
const mark = (node, name, value) => {
|
|
93
|
+
let saved = attributes.get(node);
|
|
94
|
+
if (!saved) attributes.set(node, saved = new Map());
|
|
95
|
+
if (!saved.has(name)) saved.set(name, node.getAttribute(name));
|
|
96
|
+
if (value === null) node.removeAttribute(name);
|
|
97
|
+
else if (node.getAttribute(name) !== value) node.setAttribute(name, value);
|
|
98
|
+
};
|
|
99
|
+
const property = (node, name, value) => {
|
|
100
|
+
let saved = properties.get(node);
|
|
101
|
+
if (!saved) properties.set(node, saved = new Map());
|
|
102
|
+
if (!saved.has(name)) saved.set(name, [node.style.getPropertyValue(name), node.style.getPropertyPriority(name)]);
|
|
103
|
+
node.style.setProperty(name, value);
|
|
104
|
+
};
|
|
105
|
+
const hidePreview = () => sidebar.hide();
|
|
106
|
+
const detach = () => {
|
|
107
|
+
nav?.removeEventListener('pointerenter', hidePreview);
|
|
108
|
+
nav?.removeEventListener('focusin', hidePreview);
|
|
109
|
+
nav?.removeEventListener('click', hidePreview, true);
|
|
110
|
+
};
|
|
111
|
+
const sync = () => {
|
|
112
|
+
frame = undefined;
|
|
113
|
+
const glyphs = [...host.querySelectorAll('[data-slot="sidebar.panellist"]')];
|
|
114
|
+
const buttons = glyphs.map(glyph => glyph.closest('button'));
|
|
115
|
+
// 注册表通知可以先于原 SidebarRoot 的 React commit;等真实行与目录对齐再标记。
|
|
116
|
+
if (!buttons.length || buttons.length !== panels.length || buttons.some((button, i) => !button || button.getAttribute('aria-label') !== panels[i].label)) return;
|
|
117
|
+
const nextNav = buttons[0].closest('nav');
|
|
118
|
+
if (!nextNav || buttons.some(button => button.closest('nav') !== nextNav)) return;
|
|
119
|
+
if (nextNav !== nav) {
|
|
120
|
+
detach();
|
|
121
|
+
nav = nextNav;
|
|
122
|
+
nav.addEventListener('pointerenter', hidePreview);
|
|
123
|
+
nav.addEventListener('focusin', hidePreview);
|
|
124
|
+
nav.addEventListener('click', hidePreview, true);
|
|
125
|
+
}
|
|
126
|
+
mark(nav, 'data-dsc-extension-nav', '');
|
|
127
|
+
buttons.forEach((button, i) => {
|
|
128
|
+
const row = panels[i];
|
|
129
|
+
const extension = row.available && row.id !== 'plugins' && row.id !== 'schedules';
|
|
130
|
+
mark(button, 'data-dsc-extension-row', row.id);
|
|
131
|
+
mark(button, 'data-dsc-extension-hidden', extension ? null : '');
|
|
132
|
+
if (!extension) return;
|
|
133
|
+
mark(button, 'title', row.label);
|
|
134
|
+
const glyph = glyphs[i].parentElement;
|
|
135
|
+
mark(glyph, 'data-dsc-extension-glyph', '');
|
|
136
|
+
[...button.children].filter(child => child !== glyph).forEach(child => mark(child, 'data-dsc-extension-label', ''));
|
|
137
|
+
});
|
|
138
|
+
const box = target.current.getBoundingClientRect();
|
|
139
|
+
property(nav, '--dsc-extension-left', box.left + 'px');
|
|
140
|
+
property(nav, '--dsc-extension-top', box.top + 'px');
|
|
141
|
+
property(nav, '--dsc-extension-width', box.width + 'px');
|
|
142
|
+
property(nav, '--dsc-extension-height', Math.max(0, box.height) + 'px');
|
|
143
|
+
};
|
|
144
|
+
const schedule = () => { if (frame === undefined) frame = requestAnimationFrame(sync); };
|
|
145
|
+
const content = new MutationObserver(schedule);
|
|
146
|
+
content.observe(host, { childList: true, characterData: true, attributes: true, attributeFilter: ['aria-label'], subtree: true });
|
|
147
|
+
const size = new ResizeObserver(schedule);
|
|
148
|
+
size.observe(target.current);
|
|
149
|
+
window.addEventListener('resize', schedule);
|
|
150
|
+
schedule();
|
|
151
|
+
return () => {
|
|
152
|
+
cancelAnimationFrame(frame);
|
|
153
|
+
content.disconnect(); size.disconnect();
|
|
154
|
+
window.removeEventListener('resize', schedule);
|
|
155
|
+
detach();
|
|
156
|
+
for (const [node, saved] of attributes) for (const [name, value] of saved) {
|
|
157
|
+
if (value === null) node.removeAttribute(name); else node.setAttribute(name, value);
|
|
158
|
+
}
|
|
159
|
+
for (const [node, saved] of properties) for (const [name, [value, priority]] of saved) {
|
|
160
|
+
if (value) node.style.setProperty(name, value, priority); else node.style.removeProperty(name);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}, [sidebar, target, panels]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 导航与二级侧栏共享呈现状态;悬停计时不调用 layout,也不发起模型请求。
|
|
167
|
+
function createSidebarPresentation() {
|
|
168
|
+
const store = createSnapshotStore({ collapsed: true, preview: null });
|
|
169
|
+
let opening;
|
|
170
|
+
let closing;
|
|
171
|
+
const cancel = () => { clearTimeout(opening); clearTimeout(closing); };
|
|
172
|
+
const hide = () => { cancel(); if (store.getSnapshot().preview !== null) store.set({ ...store.getSnapshot(), preview: null }); };
|
|
173
|
+
const show = category => {
|
|
174
|
+
cancel();
|
|
175
|
+
if (store.getSnapshot().collapsed) store.set({ ...store.getSnapshot(), preview: category });
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
store, hide, show, hold: cancel, dispose: cancel,
|
|
179
|
+
setCollapsed(collapsed) {
|
|
180
|
+
if (store.getSnapshot().collapsed === collapsed) return;
|
|
181
|
+
cancel();
|
|
182
|
+
store.set({ collapsed, preview: null });
|
|
183
|
+
},
|
|
184
|
+
enter(category) {
|
|
185
|
+
cancel();
|
|
186
|
+
if (!store.getSnapshot().collapsed || store.getSnapshot().preview === category) return;
|
|
187
|
+
opening = setTimeout(() => show(category), category === 'home' ? 300 : 100);
|
|
188
|
+
},
|
|
189
|
+
leave() {
|
|
190
|
+
cancel();
|
|
191
|
+
closing = setTimeout(hide, 140);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function makeNavigationRail(ctx, sidebar) {
|
|
197
|
+
return function NavigationRail() {
|
|
198
|
+
const { panels, pluginRows, taskRows, category } = useNavigationDirectory(ctx);
|
|
199
|
+
const { collapsed, preview } = useSource(sidebar.store);
|
|
200
|
+
const [error, setError] = useState('');
|
|
201
|
+
const extensions = useRef(null);
|
|
202
|
+
useNativeExtensionNavigation(sidebar, extensions, panels);
|
|
203
|
+
const pick = id => {
|
|
204
|
+
sidebar.hide();
|
|
205
|
+
setError('');
|
|
206
|
+
try { ctx.layout.selectPanel(id); } catch (e) { setError(errorText(e)); }
|
|
207
|
+
};
|
|
208
|
+
const item = (id, name, icon, action) => h('button', {
|
|
209
|
+
key: id, type: 'button', className: 'dsc-rail__item' + (category === id ? ' dsc-rail__item--active' : ''),
|
|
210
|
+
title: collapsed ? undefined : name, 'data-tip': name, 'data-dsc-preview-trigger': id, 'data-preview-enabled': collapsed,
|
|
211
|
+
'aria-label': name, 'aria-current': category === id ? 'page' : undefined,
|
|
212
|
+
'aria-haspopup': collapsed ? 'dialog' : undefined, 'aria-expanded': collapsed ? preview === id : undefined,
|
|
213
|
+
'aria-controls': collapsed ? 'dsc-sidebar-card' : undefined,
|
|
214
|
+
onPointerEnter: e => { if (e.pointerType !== 'touch') sidebar.enter(id); }, onPointerLeave: sidebar.leave,
|
|
215
|
+
onFocus: () => sidebar.enter(id), onBlur: sidebar.leave,
|
|
216
|
+
onKeyDown: e => { if (e.key === 'Escape') { sidebar.hide(); e.preventDefault(); } }, onClick: action
|
|
217
|
+
}, h(PrototypeIcon, { name: icon }));
|
|
218
|
+
return createPortal(h(React.Fragment, null,
|
|
219
|
+
h('nav', { className: 'dsc-ui dsc-rail dsc-independent-rail', 'aria-label': '主导航' },
|
|
220
|
+
item('home', '首页', 'home', () => pick(null)),
|
|
221
|
+
taskRows.length > 0 && item('tasks', '定时任务', 'tasks', () => pick(taskRows[0].id)),
|
|
222
|
+
pluginRows.length > 0 && item('plugins', '自定义', 'plugins', () => pick('plugins')),
|
|
223
|
+
h('div', { ref: extensions, className: 'dsc-extension-navigation-anchor', 'aria-hidden': true })),
|
|
224
|
+
error && h('div', { className: 'dsc-ui dsc-navigation-error' }, h(ErrorLine, { text: error }))), document.body);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function makeSidebarChrome(ctx, sidebar) {
|
|
229
|
+
const Tree = makeWorkspaceTree(ctx);
|
|
230
|
+
return function SidebarChrome() {
|
|
231
|
+
const { panel, pluginRows, taskRows, category: selectedCategory } = useNavigationDirectory(ctx);
|
|
232
|
+
const { collapsed, preview } = useSource(sidebar.store);
|
|
233
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
234
|
+
const [query, setQuery] = useState('');
|
|
235
|
+
const [error, setError] = useState('');
|
|
236
|
+
const [geometry, setGeometry] = useState(null);
|
|
237
|
+
const marker = useRef(null);
|
|
238
|
+
const insidePointer = useRef(null);
|
|
239
|
+
const lastWidth = useRef(DEFAULT_SIDEBAR_WIDTH);
|
|
240
|
+
const lastSection = useRef('home');
|
|
241
|
+
if (selectedCategory !== 'extension') lastSection.current = selectedCategory;
|
|
242
|
+
const presentation = useRef({ category: lastSection.current, mode: 'docked' });
|
|
243
|
+
const search = useRef(null);
|
|
244
|
+
const searchAnchor = useRef(null);
|
|
245
|
+
useLayoutEffect(() => {
|
|
246
|
+
const column = marker.current?.closest('[data-slot="sidebar"]')?.parentElement;
|
|
247
|
+
const frame = column?.parentElement;
|
|
248
|
+
if (!column || !frame) return;
|
|
249
|
+
const measure = () => {
|
|
250
|
+
const isCollapsed = frame.hasAttribute('data-sidebar-collapsed');
|
|
251
|
+
const box = column.getBoundingClientRect();
|
|
252
|
+
const targetWidth = parseFloat(getComputedStyle(frame).getPropertyValue('--dsh-windows-sidebar-width')) || box.width;
|
|
253
|
+
if (!isCollapsed && targetWidth >= 264) lastWidth.current = targetWidth;
|
|
254
|
+
// 退场仍保留完整内容宽度;不让宿主收起的 0px 列裁掉淡出过程。
|
|
255
|
+
setGeometry({ left: box.left, top: box.top, height: box.height, width: lastWidth.current });
|
|
256
|
+
sidebar.setCollapsed(isCollapsed);
|
|
257
|
+
};
|
|
258
|
+
measure();
|
|
259
|
+
const size = new ResizeObserver(measure);
|
|
260
|
+
size.observe(column);
|
|
261
|
+
const state = new MutationObserver(measure);
|
|
262
|
+
state.observe(frame, { attributes: true, attributeFilter: ['data-sidebar-collapsed'] });
|
|
263
|
+
window.addEventListener('resize', measure);
|
|
264
|
+
return () => { size.disconnect(); state.disconnect(); window.removeEventListener('resize', measure); };
|
|
265
|
+
}, []);
|
|
266
|
+
// 第三方直接打开原 main,保留最近的二级区域,不给它再造一层目录。
|
|
267
|
+
const visibleCategory = collapsed ? preview : lastSection.current;
|
|
268
|
+
const shown = geometry !== null && visibleCategory !== null;
|
|
269
|
+
if (shown) presentation.current = { category: visibleCategory, mode: collapsed ? 'floating' : 'docked' };
|
|
270
|
+
const { category, mode } = presentation.current;
|
|
271
|
+
useEffect(() => { if (shown && searchOpen && category === 'home') search.current?.focus(); }, [shown, searchOpen, category]);
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!collapsed || !preview) return;
|
|
274
|
+
const outside = e => {
|
|
275
|
+
// React 的 capture 会沿 portal 祖先传播,项目菜单/详情卡也属于侧栏。
|
|
276
|
+
if (insidePointer.current !== e && !e.target.closest?.('[data-dsc-preview-trigger]')) sidebar.hide();
|
|
277
|
+
};
|
|
278
|
+
const blur = () => sidebar.hide();
|
|
279
|
+
const escape = e => { if (e.key === 'Escape' && !e.defaultPrevented) { sidebar.hide(); e.preventDefault(); } };
|
|
280
|
+
document.addEventListener('pointerdown', outside);
|
|
281
|
+
document.addEventListener('keydown', escape);
|
|
282
|
+
window.addEventListener('blur', blur);
|
|
283
|
+
return () => { document.removeEventListener('pointerdown', outside); document.removeEventListener('keydown', escape); window.removeEventListener('blur', blur); };
|
|
284
|
+
}, [collapsed, preview]);
|
|
285
|
+
const run = async action => { setError(''); try { await action(); } catch (e) { setError(errorText(e)); } };
|
|
286
|
+
const pick = id => run(() => { ctx.layout.selectPanel(id); sidebar.hide(); });
|
|
287
|
+
const openSearch = () => { if (collapsed) sidebar.show('home'); else pick(null); setSearchOpen(true); };
|
|
288
|
+
const closeSearch = () => { setSearchOpen(false); setQuery(''); searchAnchor.current?.focus(); };
|
|
289
|
+
const pageRows = category === 'tasks' ? taskRows : pluginRows;
|
|
290
|
+
const inset = mode === 'floating' ? 4 : 0;
|
|
291
|
+
const style = geometry ? { left: geometry.left + inset, top: geometry.top + inset, width: Math.min(geometry.width, Math.max(0, window.innerWidth - geometry.left - inset - 8)), height: Math.max(0, geometry.height - inset * 2) } : { visibility: 'hidden' };
|
|
292
|
+
const shell = h('div', {
|
|
293
|
+
ref: node => { if (node) node.inert = !shown; }, id: 'dsc-sidebar-card', className: 'dsc-ui dsc-sidebar-shell', style,
|
|
294
|
+
'data-mode': mode, 'data-open': shown, 'aria-hidden': !shown,
|
|
295
|
+
role: mode === 'floating' ? 'dialog' : undefined, 'aria-label': mode === 'floating' ? (category === 'home' ? '首页侧栏' : category === 'tasks' ? '定时任务侧栏' : '自定义侧栏') : undefined,
|
|
296
|
+
onPointerEnter: sidebar.hold, onPointerLeave: sidebar.leave,
|
|
297
|
+
onPointerDownCapture: e => { insidePointer.current = e.nativeEvent; },
|
|
298
|
+
onFocusCapture: sidebar.hold, onBlurCapture: sidebar.leave,
|
|
299
|
+
onKeyDown: e => { if (collapsed && e.key === 'Escape' && !e.defaultPrevented) { sidebar.hide(); e.preventDefault(); } }
|
|
300
|
+
},
|
|
301
|
+
h('aside', { key: category, className: 'dsc-sidebar', 'aria-label': category === 'home' ? '项目与对话' : category === 'tasks' ? '定时任务' : '自定义' },
|
|
302
|
+
h('div', { className: 'dsc-sidebar__header' }, category === 'home' ? h('div', { className: 'dsc-brand' }, h('img', { className: 'dsc-brand__mark', src: BRAND_MARK, alt: '' }), h('span', { className: 'dsc-brand__wordmark' }, 'deepseek'), h('span', { className: 'dsc-brand__badge' }, 'HARNESS')) : h('strong', { className: 'dsc-section-page-title' }, category === 'tasks' ? '定时任务' : '自定义'),
|
|
303
|
+
h('div', { className: 'dsc-sidebar__header-actions' }, h('button', { type: 'button', ref: searchAnchor, className: 'dsc-icon-button', title: '搜索对话与项目', 'aria-label': '搜索对话与项目', 'aria-expanded': searchOpen, onClick: openSearch }, h(Icon, { name: 'search' })))),
|
|
304
|
+
category === 'home' && h('button', { type: 'button', className: 'dsc-new-chat', title: '新聊天', onClick: () => { setSearchOpen(false); setQuery(''); run(() => { ctx.uiWorkspace.startSession(); sidebar.hide(); }); } }, h(PrototypeIcon, { name: 'newChat' }), h('span', null, '新聊天')),
|
|
305
|
+
category === 'home' && searchOpen && h('label', { className: 'dsc-search dsc-sidebar-search' }, h(Icon, { name: 'search' }), h('input', { ref: search, value: query, onChange: e => setQuery(e.target.value), onKeyDown: e => { if (e.key === 'Escape') { e.preventDefault(); closeSearch(); } }, placeholder: '搜索标题或项目', 'aria-label': '搜索标题或项目' }), h('button', { type: 'button', onClick: closeSearch, 'aria-label': '关闭搜索' }, '×')),
|
|
306
|
+
h(ErrorLine, { text: error }),
|
|
307
|
+
category === 'home' ? h(Tree, { query, tools: false, active: shown, onNavigate: sidebar.hide }) : h('div', { className: 'dsc-secondary-nav' }, pageRows.map(row => h('button', { key: row.id, type: 'button', className: 'dsc-secondary-item' + (row.id === panel.activePanelId ? ' is-active' : ''), 'aria-current': row.id === panel.activePanelId ? 'page' : undefined, onClick: () => pick(row.id) }, h(PrototypeIcon, { name: category === 'tasks' ? 'tasks' : 'plugins' }), row.label)))));
|
|
308
|
+
return h(React.Fragment, null, h('span', { ref: marker, 'aria-hidden': true }), createPortal(shell, document.body));
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function makeShellChrome(ctx, sidebar) {
|
|
313
|
+
const Navigation = makeNavigationRail(ctx, sidebar);
|
|
314
|
+
const Toggle = makeSidebarToggle(ctx);
|
|
315
|
+
return function ShellChrome() {
|
|
316
|
+
const marker = useRef(null);
|
|
317
|
+
useLayoutEffect(() => {
|
|
318
|
+
// 0.2.1-rc.2 的窄适配:由公开 overlay/slot 锚点找到它们所属的列,
|
|
319
|
+
// 外壳仅增加插件标记;宽度通过已有布局动作初始化,不改写 grid 或搬移 DOM。
|
|
320
|
+
const overlay = marker.current?.closest('[data-shell-overlay]');
|
|
321
|
+
const frame = overlay?.parentElement;
|
|
322
|
+
if (!frame) return;
|
|
323
|
+
const saved = [];
|
|
324
|
+
const mark = (element, name, value = '') => {
|
|
325
|
+
if (!element) return;
|
|
326
|
+
saved.push([element, name, element.getAttribute(name)]);
|
|
327
|
+
element.setAttribute(name, value);
|
|
328
|
+
};
|
|
329
|
+
mark(document.body, 'data-dsh-codex-chrome', PLUGIN_VERSION);
|
|
330
|
+
mark(frame, 'data-dsc-frame');
|
|
331
|
+
mark(frame.querySelector('[data-slot="sidebar"]')?.parentElement, 'data-dsc-sidebar-column');
|
|
332
|
+
mark(frame.querySelector('[data-slot="main"]')?.parentElement, 'data-dsc-center-column');
|
|
333
|
+
const restoreWidth = initializeSidebarWidth(ctx);
|
|
334
|
+
return () => {
|
|
335
|
+
restoreWidth?.();
|
|
336
|
+
saved.reverse().forEach(([element, name, previous]) => {
|
|
337
|
+
if (previous === null) element.removeAttribute(name); else element.setAttribute(name, previous);
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
}, []);
|
|
341
|
+
return h(React.Fragment, null, h('span', { ref: marker, hidden: true }), h(Navigation), h(Toggle));
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 此常驻入口不替换 AppFrame,也不修改系统标题栏;collapsed 时仍可重新打开侧栏。
|
|
346
|
+
function makeSidebarToggle(ctx) {
|
|
347
|
+
return function SidebarToggle() {
|
|
348
|
+
return createPortal(h('div', { className: 'dsc-ui dsc-window-sidebar-toggle' }, h('button', { type: 'button', className: 'dsc-icon-button', title: '显示或隐藏侧边栏', 'aria-label': '显示或隐藏侧边栏', onClick: () => ctx.layout.toggleSidebar() }, h('svg', { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'none', stroke: 'currentColor', strokeWidth: 1.2, 'aria-hidden': true }, h('rect', { x: 2, y: 3, width: 12, height: 10, rx: 1.5 }), h('path', { d: 'M6 3v10' })))), document.body);
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 公开的会话头 leading 座负责留位;仅收起二级侧栏时显示,不覆盖标题。
|
|
353
|
+
function makeCollapsedNewChat(ctx) {
|
|
354
|
+
return function CollapsedNewChat() {
|
|
355
|
+
// startSession 返回 void;异步创建、导航和失败通知都由宿主处理。
|
|
356
|
+
return h('div', { className: 'dsc-ui dsc-collapsed-new-chat' },
|
|
357
|
+
h('button', { type: 'button', className: 'dsc-icon-button', title: '新建对话', 'aria-label': '新建对话', onClick: () => ctx.uiWorkspace.startSession() }, h(PrototypeIcon, { name: 'newChat' })));
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function makeGearLauncher(ctx) {
|
|
362
|
+
return function GearLauncher({ openSettings, settingsOpen }) {
|
|
363
|
+
const [open, setOpen] = useState(false);
|
|
364
|
+
const [profile, setProfile] = useState(null);
|
|
365
|
+
const [failed, setFailed] = useState(false);
|
|
366
|
+
const [avatarFailed, setAvatarFailed] = useState(false);
|
|
367
|
+
const anchor = useRef(null);
|
|
368
|
+
useEffect(() => { if (settingsOpen) setOpen(false); }, [settingsOpen]);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (!open) return;
|
|
371
|
+
let active = true;
|
|
372
|
+
setProfile(null); setFailed(false); setAvatarFailed(false);
|
|
373
|
+
const client = { version: '0.2.1-rc.2', locale: ctx.locale.getSnapshot().active, timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 };
|
|
374
|
+
ctx.remote.account.getProfile(client).then(result => {
|
|
375
|
+
if (!result.ok) throw result.error;
|
|
376
|
+
if (active) { setProfile(result.value?.status === 'ready' ? result.value.value : null); setFailed(result.value?.status !== 'ready'); }
|
|
377
|
+
}).catch(() => { if (active) setFailed(true); });
|
|
378
|
+
return () => { active = false; };
|
|
379
|
+
}, [open]);
|
|
380
|
+
const showSettings = () => { setOpen(false); openSettings(); };
|
|
381
|
+
return createPortal(h('div', { className: 'dsc-ui dsc-gear-launcher' },
|
|
382
|
+
h('button', { ref: anchor, type: 'button', className: 'dsc-gear-button', 'aria-label': '设置', 'aria-haspopup': 'menu', 'aria-expanded': open, onClick: () => setOpen(!open) }, h(PrototypeIcon, { name: 'settings' })),
|
|
383
|
+
open && h(Popover, { anchor, onClose: () => setOpen(false), label: '设置与账号', width: 228, className: 'dsc-app-menu' },
|
|
384
|
+
h('button', { type: 'button', className: 'dsc-account-row', role: 'menuitem', onClick: showSettings, title: '在账号子页面中管理' }, profile?.avatarUrl && !avatarFailed ? h('img', { src: profile.avatarUrl, alt: '', onError: () => setAvatarFailed(true) }) : h('span', { className: 'dsc-account-avatar' }, h(Icon, { name: 'chat' })), h('span', null, h('strong', null, profile?.name || profile?.contact || '账号'), h('small', null, profile ? '账号与设置' : failed ? '打开账号设置' : '正在读取账号…'))),
|
|
385
|
+
h('div', { className: 'dsc-menu-separator', role: 'separator' }),
|
|
386
|
+
h(Option, { title: '设置', icon: 'settings', onClick: showSettings }))), document.body);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function CloudMark() {
|
|
391
|
+
return h('svg', { viewBox: '0 0 64 64', width: 48, height: 48, fill: 'none', stroke: 'currentColor', strokeWidth: 3.1, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'M21 9c5-6 13-6 18-1 9-1 17 6 17 14 8 6 9 17 2 24-1 10-10 17-20 14-8 6-19 3-23-5C4 54-1 44 4 35c-4-8 1-18 9-20 1-3 4-5 8-6Z' }), h('path', { d: 'm20 24 6 9-6 9m16 1h11' }));
|
|
392
|
+
}
|
|
393
|
+
// 公开 region/slot 锚点用于测量;不查私有 class,不搬移原编辑器或其它节点。
|
|
394
|
+
function useHeroGeometry(ref) {
|
|
395
|
+
const [geometry, setGeometry] = useState(null);
|
|
396
|
+
useLayoutEffect(() => {
|
|
397
|
+
const region = ref.current?.closest('[data-conversation-content]');
|
|
398
|
+
const composer = region?.querySelector('[data-slot="conversation.composer.bar"]');
|
|
399
|
+
if (!region || !composer) return;
|
|
400
|
+
const measure = () => {
|
|
401
|
+
const area = region.getBoundingClientRect();
|
|
402
|
+
const card = composer.getBoundingClientRect();
|
|
403
|
+
const fallback = composer.closest('[data-chain-overlay-fallback]');
|
|
404
|
+
const visible = area.width > 0 && card.width > 0 && (!fallback || fallback.style.display !== 'none');
|
|
405
|
+
setGeometry(previous => {
|
|
406
|
+
const next = visible ? { left: card.left, top: card.top, width: card.width, centerX: area.left + area.width / 2, centerY: area.top + area.height * .45 } : null;
|
|
407
|
+
return JSON.stringify(previous) === JSON.stringify(next) ? previous : next;
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
measure();
|
|
411
|
+
const resize = new ResizeObserver(measure); resize.observe(region); resize.observe(composer);
|
|
412
|
+
const mutation = new MutationObserver(measure); mutation.observe(region, { subtree: true, attributes: true, attributeFilter: ['data-content-phase', 'style'] });
|
|
413
|
+
window.addEventListener('resize', measure); window.addEventListener('scroll', measure, true);
|
|
414
|
+
return () => { resize.disconnect(); mutation.disconnect(); window.removeEventListener('resize', measure); window.removeEventListener('scroll', measure, true); };
|
|
415
|
+
}, [ref]);
|
|
416
|
+
return geometry;
|
|
417
|
+
}
|
|
418
|
+
function EmptyHero() {
|
|
419
|
+
const ref = useRef(null);
|
|
420
|
+
const geometry = useHeroGeometry(ref);
|
|
421
|
+
return h(React.Fragment, null, h('span', { ref, 'aria-hidden': true }), geometry && createPortal(h('div', { className: 'dsc-ui dsc-empty-hero', style: { left: geometry.centerX, top: geometry.centerY } }, h(CloudMark), h('h1', null, '我们要构建什么?')), document.body));
|
|
422
|
+
}
|
|
423
|
+
function makeHeroWorkspace(ctx) {
|
|
424
|
+
return function HeroWorkspace({ open: ownerOpen, selectedId, onPick, onClose }) {
|
|
425
|
+
const workspaces = useSource(ctx.workspaces.list);
|
|
426
|
+
const [open, setOpen] = useState(false);
|
|
427
|
+
const ref = useRef(null);
|
|
428
|
+
const anchor = useRef(null);
|
|
429
|
+
const geometry = useHeroGeometry(ref);
|
|
430
|
+
const current = workspaces.items.find(row => row.workspaceId === selectedId);
|
|
431
|
+
const shown = ownerOpen || open;
|
|
432
|
+
useLayoutEffect(() => {
|
|
433
|
+
if (!geometry) return;
|
|
434
|
+
const region = ref.current?.closest('[data-conversation-content]');
|
|
435
|
+
if (!region) return;
|
|
436
|
+
const names = ['--dsc-hero-card-top', '--dsc-hero-card-right'];
|
|
437
|
+
const previous = names.map(name => region.style.getPropertyValue(name));
|
|
438
|
+
region.style.setProperty(names[0], geometry.top + 'px');
|
|
439
|
+
region.style.setProperty(names[1], geometry.left + geometry.width + 'px');
|
|
440
|
+
region.setAttribute('data-dsc-hero-layout', '');
|
|
441
|
+
return () => { names.forEach((name, i) => { if (previous[i]) region.style.setProperty(name, previous[i]); else region.style.removeProperty(name); }); region.removeAttribute('data-dsc-hero-layout'); };
|
|
442
|
+
}, [geometry]);
|
|
443
|
+
const close = () => { setOpen(false); onClose(); };
|
|
444
|
+
const menu = shown && h(Popover, { anchor, label: '选择项目', onClose: close, width: 280 },
|
|
445
|
+
h('div', { className: 'dsc-menu-heading' }, '选择项目'),
|
|
446
|
+
workspaces.items.map(row => h(Option, { key: row.workspaceId, title: row.title, detail: row.path, icon: 'folder', selected: row.workspaceId === selectedId, onClick: () => { setOpen(false); onPick(row.workspaceId); } })),
|
|
447
|
+
!workspaces.items.length && h('div', { className: 'dsc-empty' }, '先在左侧添加工作区'));
|
|
448
|
+
const bar = geometry && createPortal(h('div', { className: 'dsc-ui dsc-hero-projectbar', style: { left: geometry.left + 12, top: geometry.top - 37, width: Math.max(120, geometry.width - 24) } },
|
|
449
|
+
h('button', { type: 'button', ref: anchor, className: 'dsc-project-picker', 'aria-haspopup': 'menu', 'aria-expanded': shown, onClick: () => { if (shown) close(); else setOpen(true); } }, h(Icon, { name: 'folder' }), current?.title || '选择项目'), menu), document.body);
|
|
450
|
+
return h(React.Fragment, null, h('span', { ref, 'aria-hidden': true }), bar);
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const React = require('react');
|
|
455
|
+
const { createPortal } = require('react-dom');
|
|
456
|
+
const { RiskConfirmation, Button, IconRefreshOutlineRegular } = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
457
|
+
const { createSnapshotStore } = require('@deepseek-ai/dsh-client-store');
|
|
458
|
+
const h = React.createElement;
|
|
459
|
+
const { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useSyncExternalStore } = React;
|
|
460
|
+
const PLUGIN_ID = 'dsh-codex-style';
|
|
461
|
+
|
|
462
|
+
// 自绘图标沿用本项目原型;功能与目录全部来自宿主。
|
|
463
|
+
const paths = {
|
|
464
|
+
folder: 'M2.3 6.1c0-.75.6-1.35 1.35-1.35H6.2l1.5 1.6h4.85c.75 0 1.35.6 1.35 1.35v4.9c0 .75-.6 1.35-1.35 1.35h-8.9c-.75 0-1.35-.6-1.35-1.35Z',
|
|
465
|
+
folderOpen: 'M2.1 12.2V4.4c0-.8.6-1.4 1.4-1.4h2.9l1.4 1.6h4c.8 0 1.4.6 1.4 1.4v1M2.1 12.2l2.2-4.8h9.3a.8.8 0 0 1 .7 1.1l-1.8 4a1.5 1.5 0 0 1-1.4.9H3.5a1.4 1.4 0 0 1-1.4-1.2Z',
|
|
466
|
+
chat: 'M13 7.5a5 5 0 0 1-5 5H3l-1 1V7.5a5.5 5.5 0 1 1 11 0Z',
|
|
467
|
+
pencil: 'M8.1 2.8H4.6a2 2 0 0 0-2 2v6.6a2 2 0 0 0 2 2h6.6a2 2 0 0 0 2-2V7.9m-2.3-5.2-5.3 5.3L5 10.4l2.4-.6 5.3-5.3a1.3 1.3 0 0 0-1.8-1.8ZM9.8 3.8l1.8 1.8',
|
|
468
|
+
search: 'M10.5 10.5 14 14M11.5 7a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z',
|
|
469
|
+
plus: 'M8 3v10M3 8h10',
|
|
470
|
+
chevron: 'm5 6 3 3 3-3',
|
|
471
|
+
back: 'm10 3-5 5 5 5',
|
|
472
|
+
check: 'm3.5 8 3 3 6-6',
|
|
473
|
+
shield: 'M8 1.8 13 4v4.2c0 2.8-2.3 4.8-5 6-2.7-1.2-5-3.2-5-6V4ZM8 5v3.6m0 2.1v.1',
|
|
474
|
+
pin: 'm5 2 6 0-1 4 2 2H9v5l-1 1V8H4l2-2Z',
|
|
475
|
+
archive: 'M3 5.5v6.3c0 .9.7 1.7 1.7 1.7h6.6c1 0 1.7-.8 1.7-1.7V5.5M2.5 2.5h11v3h-11ZM6.2 8h3.6',
|
|
476
|
+
monitor: 'M2 3h12v8H2zM5.5 13h5M8 11v2',
|
|
477
|
+
arrowUpRight: 'M4.8 11.2 11.2 4.8M5.9 4.8h5.3v5.3',
|
|
478
|
+
close: 'M4 4l8 8M12 4l-8 8',
|
|
479
|
+
settings: 'M6.5 2.5 7 1h2l.5 1.5 1.6.9 1.5-.3 1 1.8-1 1.2v1.8l1 1.2-1 1.8-1.5-.3-1.6.9L9 13H7l-.5-1.5-1.6-.9-1.5.3-1-1.8 1-1.2V6.1l-1-1.2 1-1.8 1.5.3ZM10.2 7a2.2 2.2 0 1 1-4.4 0 2.2 2.2 0 0 1 4.4 0Z'
|
|
480
|
+
};
|
|
481
|
+
function Icon({ name, size = 16, ...props }) {
|
|
482
|
+
return h('svg', { viewBox: '0 0 16 16', width: size, height: size, fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, ...props }, h('path', { d: paths[name] || paths.chat }));
|
|
483
|
+
}
|
|
484
|
+
function useSource(source) {
|
|
485
|
+
const subscribe = useCallback(fn => source.subscribe(fn), [source]);
|
|
486
|
+
const snapshot = useCallback(() => source.getSnapshot(), [source]);
|
|
487
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
488
|
+
}
|
|
489
|
+
function errorText(error) { return error?.message || String(error); }
|
|
490
|
+
function ErrorLine({ text }) { return text ? h('div', { className: 'dsc-error', role: 'alert' }, text) : null; }
|
|
491
|
+
// 用 aria-disabled 代替原生 disabled:避免全局 `.dsc-ui button:disabled { opacity: .45 }` 在提交瞬间造成变暗闪烁。
|
|
492
|
+
function Option({ title, detail, icon, selected, onClick, disabled, warning }) {
|
|
493
|
+
return h('button', { type: 'button', className: 'dsc-option', role: 'menuitemradio', 'aria-checked': !!selected, 'aria-disabled': disabled ? 'true' : undefined, 'data-warning': !!warning, onClick: e => { if (disabled) return; onClick?.(e); } },
|
|
494
|
+
icon && h(Icon, { name: icon }),
|
|
495
|
+
h('span', { className: 'dsc-option-copy' }, h('span', null, title), detail && h('small', null, detail)),
|
|
496
|
+
selected && h(Icon, { name: 'check' }));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 所有浮层位于自己的 portal 根;只读取 anchor 与自己的 DOM,不查宿主私有选择器。
|
|
500
|
+
// placement: 'top' 为默认(锚点上方优先),'right' 用于行尾菜单(锚点右下方展开)。
|
|
501
|
+
function Popover({ anchor, point, onClose, label, width = 300, children, className = '', autoFocus = true, role = 'menu', placement = 'top' }) {
|
|
502
|
+
const ref = useRef(null);
|
|
503
|
+
const [position, setPosition] = useState({ visibility: 'hidden' });
|
|
504
|
+
useLayoutEffect(() => {
|
|
505
|
+
const place = () => {
|
|
506
|
+
if (!anchor.current || !ref.current) return;
|
|
507
|
+
const box = anchor.current.getBoundingClientRect();
|
|
508
|
+
const w = Math.min(width, window.innerWidth - 24);
|
|
509
|
+
const size = ref.current.getBoundingClientRect();
|
|
510
|
+
if (point) {
|
|
511
|
+
setPosition({ position: 'fixed', left: Math.max(12, Math.min(point.x, window.innerWidth - w - 12)), top: Math.max(12, Math.min(point.y, window.innerHeight - size.height - 12)), width: w, maxHeight: Math.max(100, window.innerHeight - 24) });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (placement === 'right') {
|
|
515
|
+
const spaceRight = window.innerWidth - box.right - 12;
|
|
516
|
+
const left = spaceRight >= w ? box.right + 6 : Math.max(12, box.left - w - 6);
|
|
517
|
+
const top = Math.max(12, Math.min(window.innerHeight - size.height - 12, box.top - 4));
|
|
518
|
+
setPosition({ position: 'fixed', left, top, width: w, maxHeight: Math.max(100, window.innerHeight - 24) });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const left = Math.max(12, Math.min(window.innerWidth - w - 12, box.right - w));
|
|
522
|
+
const top = box.top - size.height - 9 >= 12 ? box.top - size.height - 9 : Math.max(12, Math.min(box.bottom + 9, window.innerHeight - size.height - 12));
|
|
523
|
+
setPosition({ position: 'fixed', left, top, width: w, maxHeight: Math.max(100, window.innerHeight - 24) });
|
|
524
|
+
};
|
|
525
|
+
place();
|
|
526
|
+
const observer = new ResizeObserver(place);
|
|
527
|
+
if (ref.current) observer.observe(ref.current);
|
|
528
|
+
window.addEventListener('resize', place);
|
|
529
|
+
window.addEventListener('scroll', place, true);
|
|
530
|
+
return () => { observer.disconnect(); window.removeEventListener('resize', place); window.removeEventListener('scroll', place, true); };
|
|
531
|
+
}, [anchor, point, width, placement]);
|
|
532
|
+
useEffect(() => {
|
|
533
|
+
if (autoFocus) (ref.current?.querySelector('input, [aria-checked="true"], button, [role="slider"]'))?.focus({ preventScroll: true });
|
|
534
|
+
const pointer = e => { if (!ref.current?.contains(e.target) && !anchor.current?.contains(e.target)) onClose(); };
|
|
535
|
+
const key = e => {
|
|
536
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); anchor.current?.focus(); return; }
|
|
537
|
+
if (!ref.current?.contains(e.target) || e.target.matches('input, [role="slider"]') || !['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) return;
|
|
538
|
+
const items = [...ref.current.querySelectorAll('button:not(:disabled)')];
|
|
539
|
+
if (!items.length) return;
|
|
540
|
+
e.preventDefault();
|
|
541
|
+
const index = items.indexOf(document.activeElement);
|
|
542
|
+
const next = e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : (index + (e.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
543
|
+
items[next]?.focus();
|
|
544
|
+
};
|
|
545
|
+
document.addEventListener('pointerdown', pointer, true);
|
|
546
|
+
document.addEventListener('keydown', key, true);
|
|
547
|
+
return () => { document.removeEventListener('pointerdown', pointer, true); document.removeEventListener('keydown', key, true); };
|
|
548
|
+
}, [anchor, onClose, autoFocus]);
|
|
549
|
+
return createPortal(h('div', { ref, className: 'dsc-ui dsc-menu ' + className, style: position, role, 'aria-label': label }, children), document.body);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 行悬停详情卡:按原型实测放置(右侧、与行顶对齐),不复用菜单的向上翻转定位。
|
|
553
|
+
// 项目卡 321×130:名称+图钉 / 聊天数 / 分隔线 / 路径 / 分隔线 / 编辑项目;会话卡 470 宽与行顶差 6px。
|
|
554
|
+
function HoverCard({ anchor, kind, onClose, onEnter, onLeave, children }) {
|
|
555
|
+
const ref = useRef(null);
|
|
556
|
+
const [position, setPosition] = useState({ visibility: 'hidden' });
|
|
557
|
+
useLayoutEffect(() => {
|
|
558
|
+
const place = () => {
|
|
559
|
+
if (!anchor.current || !ref.current) return;
|
|
560
|
+
const box = anchor.current.getBoundingClientRect();
|
|
561
|
+
const width = ref.current.offsetWidth;
|
|
562
|
+
const height = ref.current.offsetHeight;
|
|
563
|
+
const gap = kind === 'project' ? 5 : 12;
|
|
564
|
+
let left = box.right + gap;
|
|
565
|
+
if (left + width > window.innerWidth - 8) left = Math.max(8, Math.min(box.left - gap - width, window.innerWidth - width - 8));
|
|
566
|
+
let top = kind === 'project' ? box.top : box.top - 6;
|
|
567
|
+
if (top + height > window.innerHeight - 8) top = Math.max(8, window.innerHeight - height - 8);
|
|
568
|
+
if (top < 8) top = 8;
|
|
569
|
+
setPosition({ position: 'fixed', left: Math.round(left), top: Math.round(top), visibility: 'visible' });
|
|
570
|
+
};
|
|
571
|
+
place();
|
|
572
|
+
const observer = new ResizeObserver(place);
|
|
573
|
+
observer.observe(ref.current);
|
|
574
|
+
return () => observer.disconnect();
|
|
575
|
+
}, [anchor, kind]);
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
const onKey = event => { if (event.key === 'Escape') { event.preventDefault(); onClose(); } };
|
|
578
|
+
window.addEventListener('resize', onClose);
|
|
579
|
+
window.addEventListener('scroll', onClose, true);
|
|
580
|
+
document.addEventListener('keydown', onKey, true);
|
|
581
|
+
return () => { window.removeEventListener('resize', onClose); window.removeEventListener('scroll', onClose, true); document.removeEventListener('keydown', onKey, true); };
|
|
582
|
+
}, [onClose]);
|
|
583
|
+
return createPortal(h('div', { ref, className: 'dsc-ui dsc-row-pop dsc-row-pop--' + kind, style: position, role: 'dialog', 'aria-label': kind === 'project' ? '项目详情' : '对话详情', onPointerEnter: onEnter, onPointerLeave: onLeave }, children), document.body);
|
|
584
|
+
}
|
|
585
|
+
// 图钉为真实状态指示:DSH 只有会话置顶、没有工作区置顶,因此工作区始终显示空心「未置顶」。
|
|
586
|
+
function PinMark({ filled }) {
|
|
587
|
+
return h('svg', { viewBox: '0 0 16 16', width: 14, height: 14, fill: filled ? 'currentColor' : 'none', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
588
|
+
h('g', { transform: 'rotate(45 8 8)' }, h('path', { d: 'M6 2.5h4v3L12 8.5H4L6 5.5ZM8 8.5V14' })));
|
|
589
|
+
}
|
|
590
|
+
function ProjectHoverCard({ data, anchor, onReveal, onEdit, ...root }) {
|
|
591
|
+
return h(HoverCard, { anchor, kind: 'project', ...root },
|
|
592
|
+
h('div', { className: 'dsc-row-pop__head' },
|
|
593
|
+
h('span', { className: 'dsc-row-pop__name' }, h(Icon, { name: 'folder', size: 15 }), h('span', null, data.title)),
|
|
594
|
+
h('span', { className: 'dsc-row-pop__pin', role: 'img', title: data.pinned ? '已置顶' : '未置顶(DSH 暂不支持工作区置顶)', 'aria-label': data.pinned ? '已置顶' : '未置顶' }, h(PinMark, { filled: data.pinned }))),
|
|
595
|
+
h('div', { className: 'dsc-row-pop__line' }, h(Icon, { name: 'chat', size: 15 }), h('span', null, `${data.count} 次聊天`)),
|
|
596
|
+
data.path && h('div', { className: 'dsc-row-pop__sep' }),
|
|
597
|
+
data.path && h('button', { type: 'button', className: 'dsc-row-pop__line dsc-row-pop__line--path', title: '在文件资源管理器中显示', onClick: onReveal },
|
|
598
|
+
h(Icon, { name: 'folder', size: 15 }), h('span', { className: 'dsc-row-pop__path' }, data.path), h(Icon, { name: 'arrowUpRight', size: 14, className: 'dsc-row-pop__arrow' })),
|
|
599
|
+
h('div', { className: 'dsc-row-pop__sep' }),
|
|
600
|
+
h('button', { type: 'button', className: 'dsc-row-pop__line dsc-row-pop__line--action', onClick: onEdit }, h(Icon, { name: 'settings', size: 15 }), h('span', null, '编辑项目')));
|
|
601
|
+
}
|
|
602
|
+
function ThreadHoverCard({ data, anchor, ...root }) {
|
|
603
|
+
return h(HoverCard, { anchor, kind: 'thread', ...root },
|
|
604
|
+
h('div', { className: 'dsc-row-pop__head' },
|
|
605
|
+
h('p', { className: 'dsc-row-pop__excerpt' }, data.title),
|
|
606
|
+
h('span', { className: 'dsc-row-pop__meta' }, h(Icon, { name: 'monitor', size: 14 }), data.status)),
|
|
607
|
+
data.workspaceTitle && h('div', { className: 'dsc-row-pop__line' }, h(Icon, { name: 'folder', size: 15 }), h('span', null, data.workspaceTitle)),
|
|
608
|
+
data.path && h('div', { className: 'dsc-row-pop__line' }, h(Icon, { name: 'folder', size: 15 }), h('span', { className: 'dsc-row-pop__path' }, data.path)));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 「编辑项目」对话框:名称输入接真实工作区重命名,底部「移除本地项目」接真实工作区删除;
|
|
612
|
+
// 源文件夹只读展示当前路径,不提供添加/更换文件夹(DSH 无该能力)。
|
|
613
|
+
function EditWorkspaceDialog({ workspace, onClose, onRename, onDelete }) {
|
|
614
|
+
const [name, setName] = useState(workspace.title || '');
|
|
615
|
+
const [error, setError] = useState('');
|
|
616
|
+
const [busy, setBusy] = useState('');
|
|
617
|
+
const input = useRef(null);
|
|
618
|
+
useEffect(() => { input.current?.focus(); input.current?.select(); }, []);
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
const onKey = event => { if (event.key === 'Escape') { event.preventDefault(); onClose(); } };
|
|
621
|
+
document.addEventListener('keydown', onKey, true);
|
|
622
|
+
return () => document.removeEventListener('keydown', onKey, true);
|
|
623
|
+
}, [onClose]);
|
|
624
|
+
const save = async () => {
|
|
625
|
+
const title = name.trim();
|
|
626
|
+
if (!title) { setError('项目名称不能为空'); return; }
|
|
627
|
+
if (busy) return;
|
|
628
|
+
if (title === (workspace.title || '')) { onClose(); return; }
|
|
629
|
+
setBusy('save'); setError('');
|
|
630
|
+
try { await onRename(title); onClose(); } catch (e) { setError(errorText(e)); setBusy(''); }
|
|
631
|
+
};
|
|
632
|
+
const remove = async () => {
|
|
633
|
+
if (busy) return;
|
|
634
|
+
if (!window.confirm(`移除本地项目「${workspace.title || ''}」?项目中的对话会保留在“最近”中。`)) return;
|
|
635
|
+
setBusy('remove'); setError('');
|
|
636
|
+
try { await onDelete(); onClose(); } catch (e) { setError(errorText(e)); setBusy(''); }
|
|
637
|
+
};
|
|
638
|
+
return createPortal(h('div', { className: 'dsc-ui dsc-modal', role: 'presentation' },
|
|
639
|
+
h('div', { className: 'dsc-modal__mask', 'aria-hidden': true, onClick: onClose }),
|
|
640
|
+
h('div', { className: 'dsc-modal__panel', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dsc-edit-workspace-title' },
|
|
641
|
+
h('div', { className: 'dsc-modal__head' },
|
|
642
|
+
h('h2', { id: 'dsc-edit-workspace-title' }, '编辑项目'),
|
|
643
|
+
h('button', { type: 'button', className: 'dsc-modal__close', title: '关闭', 'aria-label': '关闭', onClick: onClose }, h(Icon, { name: 'close', size: 16 }))),
|
|
644
|
+
h('div', { className: 'dsc-modal__field' },
|
|
645
|
+
h(Icon, { name: 'folder', size: 16 }),
|
|
646
|
+
h('input', { ref: input, className: 'dsc-modal__input', value: name, onChange: e => setName(e.target.value), onKeyDown: e => { if (e.key === 'Enter') { e.preventDefault(); save(); } }, 'aria-label': '项目名称', placeholder: '项目名称' })),
|
|
647
|
+
h('div', { className: 'dsc-modal__label' }, '源文件夹'),
|
|
648
|
+
h('div', { className: 'dsc-modal__source' }, h(Icon, { name: 'folder', size: 16 }), h('span', { className: 'dsc-modal__path', title: workspace.path || '' }, workspace.path || '未记录路径')),
|
|
649
|
+
h(ErrorLine, { text: error }),
|
|
650
|
+
h('div', { className: 'dsc-modal__actions' },
|
|
651
|
+
h('button', { type: 'button', className: 'dsc-modal__remove', onClick: remove, disabled: !!busy }, busy === 'remove' ? '正在移除…' : '移除本地项目'),
|
|
652
|
+
h('div', { className: 'dsc-modal__spacer' }),
|
|
653
|
+
h('button', { type: 'button', className: 'dsc-modal__cancel', onClick: onClose, disabled: !!busy }, '取消'),
|
|
654
|
+
h('button', { type: 'button', className: 'dsc-modal__save', onClick: save, disabled: !!busy }, busy === 'save' ? '保存中…' : '保存')))), document.body);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function relativeTime(value) {
|
|
658
|
+
const timestamp = typeof value === 'number' ? value : Date.parse(value);
|
|
659
|
+
if (!Number.isFinite(timestamp)) return '';
|
|
660
|
+
const minutes = Math.max(0, Math.floor((Date.now() - timestamp) / 60000));
|
|
661
|
+
return minutes < 1 ? '刚刚' : minutes < 60 ? minutes + '分钟前' : minutes < 1440 ? Math.floor(minutes / 60) + '小时前' : Math.floor(minutes / 1440) + '天前';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 测量标题当前可用宽度;侧栏拖宽、重命名都会重新计算。
|
|
665
|
+
function ThreadTitle({ title }) {
|
|
666
|
+
const viewport = useRef(null);
|
|
667
|
+
const text = useRef(null);
|
|
668
|
+
useLayoutEffect(() => {
|
|
669
|
+
const measure = () => {
|
|
670
|
+
const overflow = Math.max(0, text.current.scrollWidth - viewport.current.clientWidth);
|
|
671
|
+
viewport.current.dataset.overflow = String(overflow > 1);
|
|
672
|
+
viewport.current.style.setProperty('--dsc-title-shift', -overflow + 'px');
|
|
673
|
+
viewport.current.style.setProperty('--dsc-title-duration', Math.max(2.5, overflow / 30) + 's');
|
|
674
|
+
};
|
|
675
|
+
measure();
|
|
676
|
+
const observer = new ResizeObserver(measure);
|
|
677
|
+
observer.observe(viewport.current);
|
|
678
|
+
observer.observe(text.current);
|
|
679
|
+
return () => observer.disconnect();
|
|
680
|
+
}, [title]);
|
|
681
|
+
return h('span', { ref: viewport, className: 'dsc-thread-title' }, h('span', { ref: text, className: 'dsc-thread-title-text' }, title));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// 提示 portal 到 body,避免被滚动区或正在开合的项目裁掉;延迟和进退由 CSS 控制。
|
|
685
|
+
function RowTooltip({ anchor, open, label, onClose }) {
|
|
686
|
+
const ref = useRef(null);
|
|
687
|
+
const [position, setPosition] = useState({});
|
|
688
|
+
useLayoutEffect(() => {
|
|
689
|
+
if (!open) return;
|
|
690
|
+
const box = anchor.current.getBoundingClientRect();
|
|
691
|
+
const tip = ref.current.getBoundingClientRect();
|
|
692
|
+
setPosition({ left: Math.max(8, Math.min(box.left + (box.width - tip.width) / 2, window.innerWidth - tip.width - 8)), top: box.top - tip.height - 7 >= 8 ? box.top - tip.height - 7 : box.bottom + 7 });
|
|
693
|
+
window.addEventListener('resize', onClose);
|
|
694
|
+
window.addEventListener('scroll', onClose, true);
|
|
695
|
+
return () => { window.removeEventListener('resize', onClose); window.removeEventListener('scroll', onClose, true); };
|
|
696
|
+
}, [anchor, open, label, onClose]);
|
|
697
|
+
return createPortal(h('div', { ref, className: 'dsc-ui dsc-row-tooltip', role: 'tooltip', 'aria-hidden': !open, 'data-open': open, style: position }, label), document.body);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function ThreadAction({ label, active, onIntent, onClick, className = '', pressed, children }) {
|
|
701
|
+
const anchor = useRef(null);
|
|
702
|
+
const [hovered, setHovered] = useState(false);
|
|
703
|
+
const [focused, setFocused] = useState(false);
|
|
704
|
+
const close = useCallback(() => { setHovered(false); setFocused(false); }, []);
|
|
705
|
+
useEffect(() => { if (!active) close(); }, [active, close]);
|
|
706
|
+
return h(React.Fragment, null,
|
|
707
|
+
h('button', { ref: anchor, type: 'button', className: 'dsc-thread-action ' + className, 'aria-label': label, 'aria-pressed': pressed,
|
|
708
|
+
onPointerEnter: () => { onIntent(); setHovered(true); }, onPointerLeave: () => setHovered(false),
|
|
709
|
+
onFocus: e => { if (e.currentTarget.matches(':focus-visible')) { onIntent(); setFocused(true); } }, onBlur: () => setFocused(false),
|
|
710
|
+
onClick: () => { close(); onIntent(); onClick(); } }, children),
|
|
711
|
+
h(RowTooltip, { anchor, open: active && (hovered || focused), label, onClose: close }));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function ProjectFold({ open, children, className = '' }) {
|
|
715
|
+
return h('div', { className: 'dsc-project-fold ' + className, 'data-open': open, 'aria-hidden': !open, ref: node => { if (node) node.inert = !open; } },
|
|
716
|
+
h('div', { className: 'dsc-project-clip' }, children));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function makeWorkspaceTree(ctx) {
|
|
720
|
+
return function WorkspaceTree({ query = '', tools = true, active = true, onNavigate }) {
|
|
721
|
+
const catalog = useSource(ctx.sessions.list);
|
|
722
|
+
const workspaces = useSource(ctx.workspaces.list);
|
|
723
|
+
const status = useSource(ctx.uiSession.sessionStatus);
|
|
724
|
+
const panel = useSource(ctx.layout.panelInfo);
|
|
725
|
+
const [expanded, setExpanded] = useState({});
|
|
726
|
+
const [projectsOpen, setProjectsOpen] = useState(true);
|
|
727
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
728
|
+
const [message, setMessage] = useState('');
|
|
729
|
+
const [adding, setAdding] = useState(false);
|
|
730
|
+
const [menu, setMenu] = useState(null);
|
|
731
|
+
const [projectMenu, setProjectMenu] = useState(null);
|
|
732
|
+
const [hover, setHover] = useState(null);
|
|
733
|
+
const [editing, setEditing] = useState(null);
|
|
734
|
+
const menuAnchor = useRef(null);
|
|
735
|
+
const projectAnchor = useRef(null);
|
|
736
|
+
const hoverAnchor = useRef(null);
|
|
737
|
+
const hoverTimer = useRef(null);
|
|
738
|
+
const alive = useRef(true);
|
|
739
|
+
useEffect(() => { alive.current = true; return () => { alive.current = false; clearTimeout(hoverTimer.current); }; }, []);
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
if (active) return;
|
|
742
|
+
clearTimeout(hoverTimer.current);
|
|
743
|
+
setHover(null); setMenu(null); setProjectMenu(null); setEditing(null);
|
|
744
|
+
}, [active]);
|
|
745
|
+
const run = async action => { setMessage(''); try { await action(); } catch (e) { if (alive.current) setMessage(errorText(e)); } };
|
|
746
|
+
const archived = new Set(workspaces.archivedSessionIds);
|
|
747
|
+
const pinned = new Set(workspaces.pinnedSessionIds);
|
|
748
|
+
const items = catalog.ids.map(id => catalog.byId[id]).filter(s => s && s.origin !== 'subagent' && !s.blank && (showArchived || !archived.has(s.id))).map(s => ({ ...s, sessionId: s.id, title: s.title || s.displayTitle }));
|
|
749
|
+
const currentId = catalog.ids.find(id => (catalog.byId[id]?.retainedBy?.mainView ?? 0) > 0);
|
|
750
|
+
const workspaceOf = id => workspaces.items.find(w => w.sessionIds.includes(id));
|
|
751
|
+
const matches = s => !query.trim() || `${s.title || ''} ${workspaceOf(s.sessionId)?.title || ''}`.toLowerCase().includes(query.trim().toLowerCase());
|
|
752
|
+
const ordered = [...items].sort((a, b) => (Number(b.updatedAt) || Date.parse(b.updatedAt) || 0) - (Number(a.updatedAt) || Date.parse(a.updatedAt) || 0));
|
|
753
|
+
const visible = ordered.filter(matches);
|
|
754
|
+
const cancelHover = () => clearTimeout(hoverTimer.current);
|
|
755
|
+
const hideHover = () => { clearTimeout(hoverTimer.current); setHover(null); };
|
|
756
|
+
const closeHover = () => { cancelHover(); hoverTimer.current = setTimeout(() => setHover(null), 140); };
|
|
757
|
+
const enterHover = (target, data) => {
|
|
758
|
+
cancelHover();
|
|
759
|
+
hoverAnchor.current = target;
|
|
760
|
+
if (hover) setHover(data);
|
|
761
|
+
else hoverTimer.current = setTimeout(() => setHover(data), 300);
|
|
762
|
+
};
|
|
763
|
+
const revealWorkspace = hoverData => {
|
|
764
|
+
hideHover();
|
|
765
|
+
run(async () => {
|
|
766
|
+
const sessionRemote = ctx.remote?.session;
|
|
767
|
+
if (typeof sessionRemote?.openWorkspacePath !== 'function') throw new Error('当前宿主不支持在文件管理器中显示文件夹');
|
|
768
|
+
const result = await sessionRemote.openWorkspacePath({ path: hoverData.path, action: 'reveal' });
|
|
769
|
+
if (result?.ok === false) throw result.error;
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
const editWorkspace = workspace => {
|
|
773
|
+
hideHover();
|
|
774
|
+
setEditing(workspace);
|
|
775
|
+
};
|
|
776
|
+
const rename = async row => {
|
|
777
|
+
const next = window.prompt('重命名对话', row.title || '未命名对话');
|
|
778
|
+
if (next === null) return;
|
|
779
|
+
const result = await ctx.sessions.rename(row.sessionId, next);
|
|
780
|
+
if (result?.ok === false) throw result.error;
|
|
781
|
+
};
|
|
782
|
+
const togglePin = s => run(() => pinned.has(s.sessionId) ? ctx.uiWorkspace.unpinSession(s.sessionId) : ctx.uiWorkspace.pinSession(s.sessionId));
|
|
783
|
+
const toggleArchive = s => run(() => archived.has(s.sessionId) ? ctx.uiWorkspace.unarchiveSession(s.sessionId) : ctx.uiWorkspace.archiveSession(s.sessionId));
|
|
784
|
+
// 继续使用原来的菜单与动作,只把入口改为整行右键(含 Shift+F10 / 菜单键)。
|
|
785
|
+
const openThreadMenu = (event, s) => {
|
|
786
|
+
event.preventDefault(); event.stopPropagation();
|
|
787
|
+
menuAnchor.current = event.currentTarget.querySelector('.dsc-thread-main');
|
|
788
|
+
const box = menuAnchor.current.getBoundingClientRect();
|
|
789
|
+
const point = event.type === 'contextmenu' && (event.clientX || event.clientY) ? { x: event.clientX, y: event.clientY } : { x: box.left + 16, y: box.bottom };
|
|
790
|
+
hideHover(); setProjectMenu(null); setMenu({ ...s, point });
|
|
791
|
+
};
|
|
792
|
+
const row = (s, rowActive = active) => {
|
|
793
|
+
const state = status.get(s.sessionId);
|
|
794
|
+
const actionsActive = rowActive && !menu && !projectMenu && !editing;
|
|
795
|
+
const pending = state?.pendingInteraction;
|
|
796
|
+
const activity = pending ? '等待处理' : state?.running ? '运行中' : state?.completionUnread ? '已完成' : relativeTime(s.updatedAt);
|
|
797
|
+
return h('div', { key: s.sessionId, className: 'dsc-thread' + (s.sessionId === currentId && panel.activePanelId === null ? ' is-active' : '') + (archived.has(s.sessionId) ? ' is-archived' : '') + (pinned.has(s.sessionId) ? ' is-pinned' : ''),
|
|
798
|
+
onContextMenu: e => openThreadMenu(e, s), onKeyDown: e => { if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) openThreadMenu(e, s); },
|
|
799
|
+
onPointerEnter: e => enterHover(e.currentTarget, { kind: 'thread', id: s.sessionId, title: s.title || '未命名对话', status: activity, workspaceTitle: workspaceOf(s.sessionId)?.title, path: workspaceOf(s.sessionId)?.path }), onPointerLeave: closeHover },
|
|
800
|
+
h('button', { type: 'button', className: 'dsc-thread-main', 'aria-haspopup': 'menu', onClick: () => { if (archived.has(s.sessionId)) run(() => ctx.uiWorkspace.unarchiveSession(s.sessionId)); else { ctx.uiWorkspace.openSession(s.sessionId); onNavigate?.(); } } },
|
|
801
|
+
h('span', { className: 'dsc-state', 'data-state': pending ? 'pending' : state?.running ? 'running' : state?.completionUnread ? 'done' : 'idle' }, (pending || state?.running || state?.completionUnread) && h(Icon, { name: 'chat', size: 14 })),
|
|
802
|
+
h(ThreadTitle, { title: s.title || '未命名对话' }),
|
|
803
|
+
h('small', { className: 'dsc-thread-time' }, activity)),
|
|
804
|
+
h('div', { className: 'dsc-thread-actions' },
|
|
805
|
+
h(ThreadAction, { label: pinned.has(s.sessionId) ? '取消置顶聊天' : '置顶聊天', className: 'dsc-pin-action', pressed: pinned.has(s.sessionId), active: actionsActive, onIntent: hideHover, onClick: () => togglePin(s) }, h(PinMark, { filled: pinned.has(s.sessionId) })),
|
|
806
|
+
h(ThreadAction, { label: archived.has(s.sessionId) ? '取消归档聊天' : '归档聊天', active: actionsActive, onIntent: hideHover, onClick: () => toggleArchive(s) }, h(Icon, { name: 'archive', size: 14 }))));
|
|
807
|
+
};
|
|
808
|
+
const addWorkspace = async () => {
|
|
809
|
+
setAdding(true);
|
|
810
|
+
await run(async () => {
|
|
811
|
+
const picked = await ctx.uiWorkspace.pickDirectory();
|
|
812
|
+
if (!picked) return;
|
|
813
|
+
const path = typeof picked === 'string' ? picked : picked.path;
|
|
814
|
+
if (!path) return;
|
|
815
|
+
const created = await ctx.workspaces.create({ path });
|
|
816
|
+
await ctx.uiWorkspace.openWorkspace(created.workspaceId);
|
|
817
|
+
onNavigate?.();
|
|
818
|
+
});
|
|
819
|
+
if (alive.current) setAdding(false);
|
|
820
|
+
};
|
|
821
|
+
const projectHeading = h('div', { className: 'dsc-tree-tools', 'data-compact': !tools },
|
|
822
|
+
h('button', { type: 'button', className: 'dsc-projects-toggle', 'aria-label': '项目', 'aria-expanded': projectsOpen, onClick: () => { hideHover(); setMenu(null); setProjectMenu(null); setProjectsOpen(!projectsOpen); } }, h('span', { className: 'dsc-tree-tools-label' }, '项目'), h(Icon, { name: 'chevron', size: 12 })),
|
|
823
|
+
h('button', { type: 'button', className: 'dsc-icon-button', title: '添加工作区', onClick: addWorkspace, disabled: adding }, h(Icon, { name: 'plus' })));
|
|
824
|
+
return h('div', { className: 'dsc-ui dsc-workspaces' },
|
|
825
|
+
query && projectHeading,
|
|
826
|
+
h(ErrorLine, { text: message || (catalog.error && errorText(catalog.error)) || (workspaces.error && errorText(workspaces.error)) }),
|
|
827
|
+
h('div', { className: 'dsc-tree-scroll' },
|
|
828
|
+
query ? h(React.Fragment, null, h('div', { className: 'dsc-section-title' }, '搜索结果'), visible.map(s => row(s))) : h(React.Fragment, null,
|
|
829
|
+
pinned.size > 0 && h('section', null, h('div', { className: 'dsc-section-title' }, '置顶'), visible.filter(s => pinned.has(s.sessionId)).map(s => row(s))),
|
|
830
|
+
projectHeading,
|
|
831
|
+
h(ProjectFold, { open: projectsOpen, className: 'dsc-project-list-fold' }, h('div', { className: 'dsc-project-list' }, workspaces.items.map(w => {
|
|
832
|
+
const children = visible.filter(s => w.sessionIds.includes(s.sessionId) && !pinned.has(s.sessionId));
|
|
833
|
+
const open = expanded[w.workspaceId] ?? w.sessionIds.includes(currentId);
|
|
834
|
+
return h('section', { key: w.workspaceId }, h('div', { className: 'dsc-project', onPointerEnter: e => enterHover(e.currentTarget, { kind: 'project', id: w.workspaceId, workspace: w, title: w.title || '未命名项目', count: children.length, path: w.path, pinned: w.pinned === true }), onPointerLeave: closeHover },
|
|
835
|
+
h('button', { type: 'button', className: 'dsc-project-main', onClick: () => { hideHover(); setMenu(null); setProjectMenu(null); setExpanded({ ...expanded, [w.workspaceId]: !open }); }, 'aria-expanded': open }, h(Icon, { name: open ? 'folderOpen' : 'folder' }), h('span', null, w.title || '未命名项目')),
|
|
836
|
+
h('button', { type: 'button', className: 'dsc-row-action', title: '项目菜单', 'aria-label': '项目菜单', 'aria-haspopup': 'menu', 'aria-expanded': projectMenu?.workspaceId === w.workspaceId, onClick: e => { projectAnchor.current = e.currentTarget; setHover(null); setProjectMenu(projectMenu?.workspaceId === w.workspaceId ? null : w); } }, '⋯'),
|
|
837
|
+
h('button', { type: 'button', className: 'dsc-row-action', title: '在此项目新建对话', 'aria-label': '在此项目新建对话', onClick: () => { ctx.uiWorkspace.startSession(w.workspaceId); onNavigate?.(); } }, h(Icon, { name: 'pencil', size: 14 }))),
|
|
838
|
+
h(ProjectFold, { open }, h('div', { className: 'dsc-project-children' }, children.length ? children.map(s => row(s, active && projectsOpen && open)) : h('small', { className: 'dsc-empty' }, '暂无对话'))));
|
|
839
|
+
}))),
|
|
840
|
+
h('div', { className: 'dsc-section-title' }, '最近'), visible.filter(s => !workspaceOf(s.sessionId) && !pinned.has(s.sessionId)).map(s => row(s))),
|
|
841
|
+
!visible.length && catalog.phase === 'ready' && h('div', { className: 'dsc-empty' }, query ? '没有匹配的对话' : '选择项目,开始新对话'),
|
|
842
|
+
catalog.phase !== 'ready' && h('div', { className: 'dsc-empty', role: 'status' }, '正在加载会话…')),
|
|
843
|
+
h('label', { className: 'dsc-archive-toggle' }, h('input', { type: 'checkbox', checked: showArchived, onChange: e => setShowArchived(e.target.checked) }), '显示已归档对话'),
|
|
844
|
+
menu && h(Popover, { anchor: menuAnchor, point: menu.point, onClose: () => setMenu(null), label: '对话菜单', width: 228 },
|
|
845
|
+
h(Option, { title: '重命名', icon: 'pencil', onClick: () => { const target = menu; setMenu(null); run(() => rename(target)); } }),
|
|
846
|
+
h(Option, { title: pinned.has(menu.sessionId) ? '取消置顶' : '置顶', icon: 'pin', onClick: () => { const target = menu; setMenu(null); togglePin(target); } }),
|
|
847
|
+
h(Option, { title: '分叉对话', icon: 'chat', onClick: () => { const target = menu; setMenu(null); run(() => ctx.uiWorkspace.forkSession(target.sessionId)); } }),
|
|
848
|
+
h(Option, { title: archived.has(menu.sessionId) ? '取消归档' : '归档', icon: 'archive', onClick: () => { const target = menu; setMenu(null); toggleArchive(target); } })),
|
|
849
|
+
// Codex 风格项目菜单:仅提供编辑与移除项目两项,编辑复用现有编辑项目对话框;从行右侧下方展开。
|
|
850
|
+
projectMenu && h(Popover, { anchor: projectAnchor, onClose: () => setProjectMenu(null), label: '项目菜单', width: 228, placement: 'right' },
|
|
851
|
+
h(Option, { title: '编辑', icon: 'pencil', onClick: () => { const target = projectMenu; setProjectMenu(null); editWorkspace(target); } }),
|
|
852
|
+
h(Option, { title: '移除项目', icon: 'close', onClick: () => { const target = projectMenu; setProjectMenu(null); if (!window.confirm(`移除项目“${target.title || '未命名项目'}”?`)) return; run(() => ctx.workspaces.delete(target.workspaceId)); } })),
|
|
853
|
+
hover && !menu && !projectMenu && (hover.kind === 'project'
|
|
854
|
+
? h(ProjectHoverCard, { key: hover.id, data: hover, anchor: hoverAnchor, onClose: hideHover, onEnter: cancelHover, onLeave: closeHover, onReveal: () => revealWorkspace(hover), onEdit: () => editWorkspace(hover.workspace) })
|
|
855
|
+
: h(ThreadHoverCard, { key: hover.id, data: hover, anchor: hoverAnchor, onClose: hideHover, onEnter: cancelHover, onLeave: closeHover })),
|
|
856
|
+
editing && h(EditWorkspaceDialog, { key: editing.workspaceId, workspace: editing, onClose: () => setEditing(null), onRename: title => ctx.workspaces.rename(editing.workspaceId, title), onDelete: () => ctx.workspaces.delete(editing.workspaceId) }));
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// 权限目录只读 RPC + 事件通知;世代改变时清空旧 Host 的目录。
|
|
861
|
+
function permissionDirectory(ctx) {
|
|
862
|
+
const store = createSnapshotStore({ value: null, error: null, revision: 0 });
|
|
863
|
+
let epoch = 0;
|
|
864
|
+
let disposed = false;
|
|
865
|
+
let generation;
|
|
866
|
+
const refresh = () => {
|
|
867
|
+
const id = ctx.connection.generation.getSnapshot()?.id;
|
|
868
|
+
const currentEpoch = ++epoch;
|
|
869
|
+
store.set({ value: null, error: null, revision: currentEpoch });
|
|
870
|
+
generation = id;
|
|
871
|
+
if (id === undefined) return;
|
|
872
|
+
ctx.remote.permissionPresets.catalog().then(result => {
|
|
873
|
+
if (!result.ok) throw result.error;
|
|
874
|
+
if (!disposed && epoch === currentEpoch && ctx.connection.generation.getSnapshot()?.id === id) store.set({ value: result.value, error: null, revision: currentEpoch });
|
|
875
|
+
}).catch(error => {
|
|
876
|
+
if (!disposed && epoch === currentEpoch) store.set({ value: null, error: errorText(error), revision: currentEpoch });
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
const offCatalog = ctx.remote.$on('permission-presets/catalog-changed', refresh);
|
|
880
|
+
const offGeneration = ctx.connection.generation.subscribe(() => {
|
|
881
|
+
if (ctx.connection.generation.getSnapshot()?.id !== generation) refresh();
|
|
882
|
+
});
|
|
883
|
+
refresh();
|
|
884
|
+
return { store, refresh, dispose() { disposed = true; ++epoch; offCatalog(); offGeneration(); } };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// 基础 bundle 只配置 sandbox/approval,不提供 description;内置项在宿主缺省时补回可核实的范围说明。
|
|
888
|
+
const PERMISSION_COPY = {
|
|
889
|
+
'read-only': {
|
|
890
|
+
title: '仅可查看',
|
|
891
|
+
hostNames: ['Read Only'],
|
|
892
|
+
description: '只读沙箱会阻止文件修改;需要扩大访问范围时会请求批准。'
|
|
893
|
+
},
|
|
894
|
+
'workspace-write': {
|
|
895
|
+
title: '工作区内修改',
|
|
896
|
+
hostNames: ['Workspace Write'],
|
|
897
|
+
description: '可在工作区及允许的临时目录内修改文件;扩大访问范围前会请求批准。'
|
|
898
|
+
},
|
|
899
|
+
'danger-full-access': {
|
|
900
|
+
title: '完全权限',
|
|
901
|
+
hostNames: ['Full Access', 'Full access'],
|
|
902
|
+
description: '可访问完整文件系统,且不再逐项请求批准。'
|
|
903
|
+
},
|
|
904
|
+
auto: {
|
|
905
|
+
title: '自动审查 · EXP',
|
|
906
|
+
hostNames: [],
|
|
907
|
+
description: '无沙箱运行;每次原生工具调用和 PTC 内层调用前,由同一模型进行实验性审查。'
|
|
908
|
+
},
|
|
909
|
+
custom: {
|
|
910
|
+
title: '自定义',
|
|
911
|
+
hostNames: ['Custom'],
|
|
912
|
+
description: '当前沙箱与审批设置不匹配任何已配置的权限预设。'
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
function permissionCopy(option) {
|
|
916
|
+
const fallback = PERMISSION_COPY[option.value];
|
|
917
|
+
const hostName = typeof option.name === 'string' && option.name.trim() ? option.name : option.value;
|
|
918
|
+
const useLocalizedTitle = fallback && (option.value === 'auto' || hostName === option.value || fallback.hostNames.includes(hostName));
|
|
919
|
+
return {
|
|
920
|
+
title: useLocalizedTitle ? fallback.title : hostName,
|
|
921
|
+
description: option.description || fallback?.description || '具体范围由当前 DSH 配置提供。'
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function makePermissionPicker(ctx, directory) {
|
|
926
|
+
return function PermissionPicker({ sessionId, useProjection, locked }) {
|
|
927
|
+
const selection = useProjection('permissions');
|
|
928
|
+
const catalog = useSource(directory.store);
|
|
929
|
+
const [open, setOpen] = useState(false);
|
|
930
|
+
const [busy, setBusy] = useState(false);
|
|
931
|
+
const [confirmation, setConfirmation] = useState(null);
|
|
932
|
+
const [acknowledged, setAcknowledged] = useState(false);
|
|
933
|
+
const [help, setHelp] = useState(false);
|
|
934
|
+
const [error, setError] = useState('');
|
|
935
|
+
const anchor = useRef(null);
|
|
936
|
+
const alive = useRef(true);
|
|
937
|
+
useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
|
|
938
|
+
useEffect(() => { setOpen(false); setConfirmation(null); setAcknowledged(false); }, [catalog.revision, locked, sessionId]);
|
|
939
|
+
// 不用消失的控件掩盖目录失败;没有真实会话投影时只显示状态,不伪造可选权限。
|
|
940
|
+
if (!selection) return h('span', { className: 'dsc-ui dsc-control-status', role: 'status', title: '宿主尚未提供此会话的权限状态' }, h(Icon, { name: 'shield' }), '权限未就绪');
|
|
941
|
+
if (!catalog.value) return h('span', { className: 'dsc-ui dsc-control-status', role: 'status' }, h(Icon, { name: 'shield' }), catalog.error ? '权限加载失败' : '权限加载中…', catalog.error && h('button', { type: 'button', onClick: directory.refresh, title: catalog.error }, '重试'));
|
|
942
|
+
const options = catalog.value.options;
|
|
943
|
+
const current = options.find(x => x.value === selection.currentValue);
|
|
944
|
+
const currentCopy = current ? permissionCopy(current) : permissionCopy({ value: selection.currentValue });
|
|
945
|
+
const submit = async value => {
|
|
946
|
+
const binding = ctx.sessions.binding(sessionId);
|
|
947
|
+
if (!binding || locked || busy || !options.some(x => x.value === value)) return;
|
|
948
|
+
setBusy(true); setError('');
|
|
949
|
+
try {
|
|
950
|
+
const result = await binding.session.command('/permission ' + value);
|
|
951
|
+
if (!result.ok) throw result.error;
|
|
952
|
+
if (!result.value.matched) throw new Error('宿主没有接受权限命令');
|
|
953
|
+
} catch (e) { if (alive.current) setError(errorText(e)); }
|
|
954
|
+
finally { if (alive.current) setBusy(false); }
|
|
955
|
+
};
|
|
956
|
+
const choose = value => {
|
|
957
|
+
if (value === selection.currentValue) { setOpen(false); return; }
|
|
958
|
+
if (value === 'danger-full-access' || value === 'auto') { setOpen(false); setAcknowledged(false); setConfirmation(value); return; }
|
|
959
|
+
setOpen(false); submit(value);
|
|
960
|
+
};
|
|
961
|
+
return h('div', { className: 'dsc-ui dsc-permission' },
|
|
962
|
+
h('button', { type: 'button', ref: anchor, className: 'dsc-pill', 'data-warning': selection.currentValue === 'danger-full-access', title: '更改权限', disabled: locked || busy, 'aria-haspopup': 'menu', 'aria-expanded': open, onClick: () => setOpen(!open) }, h(Icon, { name: 'shield' }), busy ? '保存中…' : currentCopy.title),
|
|
963
|
+
open && h(Popover, { anchor, onClose: () => setOpen(false), label: '更改权限', width: 322, className: 'dsc-permission-menu' },
|
|
964
|
+
h('div', { className: 'dsc-permission-heading' }, h('span', null, '应如何批准 DSH 操作?'), h('button', { type: 'button', 'aria-expanded': help, onClick: () => setHelp(!help) }, '了解更多')),
|
|
965
|
+
help && h('p', { className: 'dsc-permission-help' }, '权限决定 DSH 可执行的操作范围。选择完全访问或自动审查时,请阅读确认说明;每个选项的具体范围见下方描述。'),
|
|
966
|
+
options.map(option => {
|
|
967
|
+
const copy = permissionCopy(option);
|
|
968
|
+
return h(Option, { key: option.value, title: copy.title, detail: copy.description, icon: 'shield', warning: option.value === 'danger-full-access', selected: option.value === selection.currentValue, onClick: () => choose(option.value) });
|
|
969
|
+
})),
|
|
970
|
+
h(ErrorLine, { text: error }),
|
|
971
|
+
confirmation && h(RiskConfirmation, { open: true, title: confirmation === 'auto' ? '启用自动审查' : '启用完全访问权限',
|
|
972
|
+
description: confirmation === 'auto' ? '实验性自动审查会在完整沙箱访问下处理审批。请确认你理解自动审查的执行范围。' : 'DSH 将获得完整文件系统和命令执行访问,且不再逐项请求批准。',
|
|
973
|
+
acknowledgeLabel: '我理解并接受此权限范围', cancelLabel: '取消', closeLabel: '关闭', confirmLabel: '确认启用', acknowledged, disabled: locked || busy,
|
|
974
|
+
onAcknowledgedChange: setAcknowledged, onCancel: () => setConfirmation(null), onConfirm: () => { if (!acknowledged) return; const value = confirmation; setConfirmation(null); submit(value); } }));
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function ReasoningSlider({ levels, index, modelName, pending, onCommit, onSelectModel, onReset }) {
|
|
979
|
+
const [preview, setPreview] = useState(index);
|
|
980
|
+
const [dragging, setDragging] = useState(false);
|
|
981
|
+
const [burst, setBurst] = useState(0);
|
|
982
|
+
const track = useRef(null);
|
|
983
|
+
const current = useRef(index);
|
|
984
|
+
const target = useRef(null);
|
|
985
|
+
const count = levels.length;
|
|
986
|
+
// 提交期间宿主状态可能先回旧档再落到新档;锁定目标档位,避免预览被中途值拉回造成闪烁。
|
|
987
|
+
useEffect(() => {
|
|
988
|
+
if (dragging) return;
|
|
989
|
+
const wanted = target.current;
|
|
990
|
+
if (wanted === null) { current.current = index; setPreview(index); return; }
|
|
991
|
+
if (index === wanted) { target.current = null; current.current = index; setPreview(index); return; }
|
|
992
|
+
if (pending) return;
|
|
993
|
+
const timer = setTimeout(() => {
|
|
994
|
+
if (target.current !== null && !pending) {
|
|
995
|
+
target.current = null;
|
|
996
|
+
current.current = index;
|
|
997
|
+
setPreview(index);
|
|
998
|
+
}
|
|
999
|
+
}, 200);
|
|
1000
|
+
return () => clearTimeout(timer);
|
|
1001
|
+
}, [index, dragging, pending]);
|
|
1002
|
+
const stars = useMemo(() => Array.from({ length: 18 }, (_, i) => ({ left: `${8 + (i * 37) % 85}%`, top: `${22 + (i * 29) % 57}%`, width: `${1.2 + (i % 5) * .24}px`, height: `${1.2 + (i % 5) * .24}px`, '--dx': `${12 + i % 13}px`, '--dy': `${2 + i % 5}px`, '--pdur': `${2.6 + (i % 9) * .2}s`, '--pdelay': `${-(i % 6) * .7}s` })), []);
|
|
1003
|
+
const ratio = count > 1 ? preview / (count - 1) : 0;
|
|
1004
|
+
const level = levels[preview];
|
|
1005
|
+
const update = next => {
|
|
1006
|
+
const n = Math.max(0, Math.min(count - 1, next));
|
|
1007
|
+
if (levels[n].id === 'max' && levels[current.current]?.id !== 'max') setBurst(x => x + 1);
|
|
1008
|
+
current.current = n; setPreview(n);
|
|
1009
|
+
};
|
|
1010
|
+
const fromPointer = e => { const box = track.current.getBoundingClientRect(); return Math.round(Math.max(0, Math.min(1, (e.clientX - box.left - 14) / Math.max(1, box.width - 28))) * (count - 1)); };
|
|
1011
|
+
const commit = () => {
|
|
1012
|
+
if (pending || levels[current.current].id === levels[index]?.id) return;
|
|
1013
|
+
target.current = current.current;
|
|
1014
|
+
onCommit(levels[current.current]);
|
|
1015
|
+
};
|
|
1016
|
+
return h('div', { className: 'dsc-reasoning', 'data-level': level.id },
|
|
1017
|
+
h('button', { type: 'button', className: 'dsc-reasoning-reset', 'aria-disabled': pending ? 'true' : undefined, onClick: () => { if (!pending) onReset(); }, title: '重置为默认推理强度', 'aria-label': '重置为默认推理强度' }, h('svg', { viewBox: '0 0 16 16', width: 15, height: 15, fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'M13.2 8a5.2 5.2 0 1 1-1.6-3.7M13.4 2.7v2.9h-2.9' }))),
|
|
1018
|
+
h('div', { className: 'dsc-reasoning-heading' }, h('strong', null, level.name), h('button', { type: 'button', className: 'dsc-reasoning-model', onClick: () => { if (!pending) onSelectModel(); }, 'aria-disabled': pending ? 'true' : undefined, title: '修改模型' }, modelName, h(Icon, { name: 'chevron', size: 11, style: { transform: 'rotate(-90deg)' } }))),
|
|
1019
|
+
h('div', { ref: track, role: 'slider', tabIndex: pending ? -1 : 0, className: 'dsc-slider', 'data-level': level.id, 'aria-label': '思考强度', 'aria-valuemin': 0, 'aria-valuemax': count - 1, 'aria-valuenow': preview, 'aria-valuetext': level.name, 'aria-disabled': !!pending,
|
|
1020
|
+
style: { '--ratio': ratio, '--fill-ratio': `calc((14px + (100% - 28px) * ${ratio}) / 100%)` },
|
|
1021
|
+
onPointerDown: e => { if (pending) return; e.preventDefault(); e.currentTarget.focus(); e.currentTarget.setPointerCapture(e.pointerId); setDragging(true); update(fromPointer(e)); },
|
|
1022
|
+
onPointerMove: e => { if (e.currentTarget.hasPointerCapture(e.pointerId)) update(fromPointer(e)); },
|
|
1023
|
+
onPointerUp: e => { if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; update(fromPointer(e)); e.currentTarget.releasePointerCapture(e.pointerId); setDragging(false); commit(); },
|
|
1024
|
+
onPointerCancel: e => { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId); setDragging(false); current.current = index; setPreview(index); },
|
|
1025
|
+
onKeyDown: e => { if (pending) return; const n = e.key === 'Home' ? 0 : e.key === 'End' ? count - 1 : ['ArrowLeft', 'ArrowDown'].includes(e.key) ? current.current - 1 : ['ArrowRight', 'ArrowUp'].includes(e.key) ? current.current + 1 : null; if (n === null) return; e.preventDefault(); e.stopPropagation(); update(n); commit(); } },
|
|
1026
|
+
h('div', { className: 'dsc-slider-track' }, h('div', { className: 'dsc-slider-fill', style: { transform: `scaleX(${.061 + .939 * ratio})` } }), h('div', { className: 'dsc-slider-max' }), h('div', { className: 'dsc-stars' }, stars.map((style, i) => h('i', { key: i, style })))),
|
|
1027
|
+
levels.map((x, i) => h('span', { key: x.id ?? 'default', className: 'dsc-tick' + (i <= preview ? ' is-on' : ''), style: { left: `calc(14px + (100% - 28px) * ${count > 1 ? i / (count - 1) : 0})` } })),
|
|
1028
|
+
h('div', { className: 'dsc-thumb-position', style: { transform: `translateX(calc((100% - 28px) * ${ratio}))` } }, h('span', { className: 'dsc-thumb' }), burst > 0 && level.id === 'max' && h('span', { key: burst, className: 'dsc-burst' }, Array.from({ length: 12 }, (_, i) => h('i', { key: i, style: { '--bx': `${Math.cos(i * Math.PI / 6) * 27}px`, '--by': `${Math.sin(i * Math.PI / 6) * 27}px` } }))))),
|
|
1029
|
+
h('span', { className: 'dsc-sr-only', role: 'status' }, pending ? '正在保存…' : level.description || level.name));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 模型控件渲染异常时在页面内显示原因,避免静默让位给宿主原生控件。
|
|
1033
|
+
class ModelPickerBoundary extends React.Component {
|
|
1034
|
+
constructor(props) { super(props); this.state = { error: null }; }
|
|
1035
|
+
static getDerivedStateFromError(error) { return { error }; }
|
|
1036
|
+
componentDidCatch(error, info) { console.error('codex-style: 模型控件渲染失败', error, info); }
|
|
1037
|
+
render() {
|
|
1038
|
+
if (!this.state.error) return this.props.children;
|
|
1039
|
+
const message = this.state.error?.message || String(this.state.error);
|
|
1040
|
+
return h('div', { className: 'dsc-ui dsc-error', role: 'alert', style: { maxWidth: 360 } }, `模型控件渲染失败:${message}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function makeModelPicker(ctx) {
|
|
1045
|
+
function ModelPicker({ sessionId, locked, directory }) {
|
|
1046
|
+
const rawState = useSource(directory.store);
|
|
1047
|
+
const state = { current: rawState?.current ?? null, groups: rawState?.groups ?? [], failures: rawState?.failures ?? [], status: rawState?.status, error: rawState?.error, pending: rawState?.pending, retainedEffort: rawState?.retainedEffort };
|
|
1048
|
+
const [pane, setPane] = useState(null);
|
|
1049
|
+
const [shown, setShown] = useState(null);
|
|
1050
|
+
const [closing, setClosing] = useState(false);
|
|
1051
|
+
const [query, setQuery] = useState('');
|
|
1052
|
+
const [error, setError] = useState('');
|
|
1053
|
+
const anchor = useRef(null);
|
|
1054
|
+
const alive = useRef(true);
|
|
1055
|
+
const subagent = ctx.sessions.subagentAddress(sessionId) !== undefined;
|
|
1056
|
+
useEffect(() => { alive.current = true; if (!subagent && typeof directory.load === 'function') directory.load().catch(e => { if (alive.current) setError(errorText(e)); }); return () => { alive.current = false; }; }, [directory, subagent]);
|
|
1057
|
+
const hasCurrent = state.current !== null;
|
|
1058
|
+
useEffect(() => { if (locked || !hasCurrent) setPane(null); }, [locked, hasCurrent]);
|
|
1059
|
+
// 关闭时保留一小段淡出,避免弹层硬切除;视图切换由 Popover 的 key 重放进场动画。
|
|
1060
|
+
useEffect(() => {
|
|
1061
|
+
if (pane) { setShown(pane); setClosing(false); return; }
|
|
1062
|
+
if (!shown) return;
|
|
1063
|
+
setClosing(true);
|
|
1064
|
+
const timer = setTimeout(() => { setShown(null); setClosing(false); }, 160);
|
|
1065
|
+
return () => clearTimeout(timer);
|
|
1066
|
+
}, [pane, shown]);
|
|
1067
|
+
const current = state.current;
|
|
1068
|
+
const group = state.groups.find(x => x.id === current?.provider);
|
|
1069
|
+
const model = group?.models?.find(x => x.id === current?.model);
|
|
1070
|
+
const reasoning = model?.reasoning;
|
|
1071
|
+
const levels = reasoning ? [...(reasoning.defaultEffort === undefined ? [{ id: undefined, name: '默认' }] : []), ...(reasoning.efforts ?? [])] : [];
|
|
1072
|
+
const index = Math.max(0, levels.findIndex(x => x.id === (current?.reasoningEffort ?? reasoning?.defaultEffort)));
|
|
1073
|
+
const pending = !!state.pending;
|
|
1074
|
+
const select = async selection => {
|
|
1075
|
+
if (locked || pending || subagent) return false;
|
|
1076
|
+
setError('');
|
|
1077
|
+
try { const result = typeof directory.select === 'function' ? await directory.select(selection) : { ok: false, error: new Error('当前宿主不支持切换模型') }; if (!result.ok) throw result.error; return true; }
|
|
1078
|
+
catch (e) { if (alive.current) setError(errorText(e)); return false; }
|
|
1079
|
+
};
|
|
1080
|
+
if (subagent) return null;
|
|
1081
|
+
return h('div', { className: 'dsc-ui dsc-model' }, h('button', { ref: anchor, type: 'button', className: 'dsc-pill dsc-model-trigger', disabled: locked, 'aria-haspopup': 'dialog', 'aria-expanded': pane !== null, 'aria-busy': pending, title: '选择推理强度', onClick: () => setPane(pane ? null : reasoning ? 'effort' : 'root') },
|
|
1082
|
+
model ? h(React.Fragment, null,
|
|
1083
|
+
h('span', { className: 'dsc-model-name' }, model.name),
|
|
1084
|
+
levels[index] && h('span', { className: 'dsc-effort-name' }, levels[index].name)) : h('span', { className: 'dsc-model-name' }, '选择模型'), h(Icon, { name: 'chevron', size: 12 })),
|
|
1085
|
+
shown && h(Popover, { key: shown, anchor, onClose: () => setPane(null), width: shown === 'effort' ? 255 : 330, label: shown === 'effort' ? '选择推理强度' : '修改模型', role: 'dialog', className: (shown === 'effort' ? 'dsc-reasoning-pop' : 'dsc-model-panel') + (closing ? ' dsc-menu-closing' : '') },
|
|
1086
|
+
shown === 'effort' ? (model && levels.length ? h(ReasoningSlider, { levels, index, modelName: model.name, pending, onSelectModel: () => setPane('root'), onReset: () => select({ provider: current.provider, model: current.model, ...(reasoning.defaultEffort === undefined ? {} : { reasoningEffort: reasoning.defaultEffort }) }), onCommit: level => select({ provider: current.provider, model: current.model, ...(level.id === undefined ? {} : { reasoningEffort: level.id }) }) }) : h('div', { className: 'dsc-empty' }, pending ? '正在加载推理档位…' : '该模型暂无可用的推理档位')) : h(React.Fragment, null,
|
|
1087
|
+
h('div', { className: 'dsc-menu-heading' }, '修改模型'),
|
|
1088
|
+
h('input', { className: 'dsc-model-search', placeholder: '搜索模型或渠道', value: query, readOnly: pending, onChange: e => setQuery(e.target.value) }),
|
|
1089
|
+
state.groups.map(g => {
|
|
1090
|
+
const models = (g.models ?? []).filter(m => `${g.name} ${m.name}`.toLowerCase().includes(query.toLowerCase()));
|
|
1091
|
+
return models.length ? h('section', { key: g.id }, h('div', { className: 'dsc-model-group' }, g.name), models.map(m => h(Option, { key: m.id, title: m.name, detail: m.description, selected: g.id === current?.provider && m.id === current?.model, disabled: pending, onClick: async () => { const isCurrent = g.id === current?.provider && m.id === current?.model; if (isCurrent) { const currentReasoning = m.reasoning; const hasLevels = currentReasoning ? (currentReasoning.defaultEffort === undefined ? 1 : 0) + (currentReasoning.efforts?.length ?? 0) : 0; setPane(hasLevels > 0 ? 'effort' : null); return; } const ok = await select({ provider: g.id, model: m.id, ...(m.reasoning?.defaultEffort === undefined ? {} : { reasoningEffort: m.reasoning.defaultEffort }) }); if (!ok) return; const snapshot = directory.store.getSnapshot(); const nextReasoning = (snapshot?.groups ?? []).find(item => item.id === g.id)?.models?.find(item => item.id === m.id)?.reasoning; const nextLevels = nextReasoning ? (nextReasoning.defaultEffort === undefined ? 1 : 0) + (nextReasoning.efforts?.length ?? 0) : 0; setPane(nextLevels > 0 ? 'effort' : null); } }))) : null;
|
|
1092
|
+
}),
|
|
1093
|
+
state.status === 'loading' && h('div', { className: 'dsc-empty' }, '正在加载模型…'),
|
|
1094
|
+
!state.groups.length && state.status !== 'loading' && h('div', { className: 'dsc-empty' }, '暂无可用模型'),
|
|
1095
|
+
state.failures.map(f => h(ErrorLine, { key: f.id, text: `${f.name}: ${f.message}` }))),
|
|
1096
|
+
h(ErrorLine, { text: error || state.error })),
|
|
1097
|
+
!pane && h(ErrorLine, { text: error || state.error }));
|
|
1098
|
+
}
|
|
1099
|
+
return function ModelPickerGuard(props) {
|
|
1100
|
+
if (!props.directory || !props.directory.store) return h('span', { className: 'dsc-ui dsc-control-status' }, '模型目录未就绪');
|
|
1101
|
+
return h(ModelPickerBoundary, null, h(ModelPicker, props));
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function apply(ctx) {
|
|
1106
|
+
console.warn('[codex-style] apply 开始 v' + PLUGIN_VERSION);
|
|
1107
|
+
const tokenValues = {
|
|
1108
|
+
'--dsw-alias-bg-base': '#181818', '--dsw-alias-bg-layer-1': '#212121', '--dsw-alias-bg-layer-2': '#282828', '--dsw-alias-bg-layer-3': '#2d2d2d', '--dsw-alias-bg-overlay': '#282828',
|
|
1109
|
+
'--dsw-alias-bg-module-platform': '#212121', '--dsw-alias-label-primary': '#fff', '--dsw-alias-label-secondary': 'rgba(255,255,255,.7)', '--dsw-alias-label-tertiary': 'rgba(255,255,255,.5)',
|
|
1110
|
+
'--dsw-alias-label-caption': 'rgba(255,255,255,.5)', '--dsw-alias-label-dimmed': 'rgba(255,255,255,.32)', '--dsw-alias-label-primary-dimmed': 'rgba(255,255,255,.6)', '--dsw-alias-label-primary-bluish': '#eef3ff', '--dsw-alias-label-primary-inverted': '#131313',
|
|
1111
|
+
// 本版始终呈现深色;宿主 preference=system/light 时也要成对覆盖按钮背景与文字。
|
|
1112
|
+
'--dsw-alias-button-elevated-fill': 'var(--cx-active)', '--dsw-alias-button-floating-hover': 'rgba(255,255,255,.12)',
|
|
1113
|
+
'--dsw-alias-border-l1': 'rgba(255,255,255,.08)', '--dsw-alias-border-l2': 'rgba(255,255,255,.08)', '--dsw-alias-border-l3': 'rgba(255,255,255,.12)', '--dsw-alias-border-l4': 'rgba(255,255,255,.16)',
|
|
1114
|
+
'--dsw-alias-brand-primary': '#0285ff', '--dsw-alias-link': '#339cff', '--dsw-alias-interactive-bg-hover': 'rgba(255,255,255,.08)', '--dsw-alias-interactive-bg-hover-solid': '#282828',
|
|
1115
|
+
// 2026-10-23 按用户 Codex 截图采样:顶栏/窗口 chrome #202020、用户气泡 #222222(对话区保持 #181818)。
|
|
1116
|
+
'--dsw-specific-sidebar-fill': '#202020', '--dsw-specific-bubble': '#222222', '--dsw-specific-input-major': 'rgba(55,55,55,.94)', '--dsw-specific-menu': '#2d2d2d', '--dsw-specific-selector': 'transparent',
|
|
1117
|
+
// 设置页左栏与侧栏导航项:light 主题下宿主给浅色底,插件把文字固定为白色,会成白底白字;这里按宿主暗色主题成对补齐。
|
|
1118
|
+
'--dsw-specific-sidebar-nav-item-active': '#43454a', '--dsw-specific-sidebar-nav-item-hover': '#2c2c2e', '--dsw-specific-sidebar-nav-item-active-accent': '#353638',
|
|
1119
|
+
// markdown 代码外观:light 主题下 inline code / 代码块顶栏也是浅底,需一并转成与对话背景有区分的深色。
|
|
1120
|
+
'--dsw-alias-markdown-code-block': '#212121', '--dsw-alias-markdown-inline-code': '#292929', '--dsw-alias-markdown-code-block-banner': '#2c2c2e',
|
|
1121
|
+
'--dsw-alias-markdown-code-segment-selected': '#353638', '--dsw-alias-markdown-code-segment-unselected': '#1b1b1c',
|
|
1122
|
+
'--dsw-alias-state-business-primary': '#339cff', '--dsw-alias-state-business-tertiary': '#0285ff26', '--dsw-alias-state-error-primary': '#ff6764', '--dsw-alias-state-warn-primary': '#ff8549', '--dsw-alias-state-warn-secondary': '#ff8549bb', '--dsw-alias-state-warn-tertiary': '#ff854926',
|
|
1123
|
+
'--dsw-alias-scrollbar-bg-l1': '#ffffff26', '--dsw-alias-scrollbar-bg-l2': '#ffffff32', '--dsw-alias-scrollbar-hover-l2': '#ffffff59',
|
|
1124
|
+
'--dsw-radius-panel': '25px', '--dsw-menu-surface-fill': '#2d2d2d', '--dsw-elevation-soft': '0 0 0 .5px rgba(255,255,255,.16),0 3px 8px #0000000a'
|
|
1125
|
+
};
|
|
1126
|
+
ctx.effect(() => ctx.theme.overrideTokens(PLUGIN_ID, Object.fromEntries(Object.entries(tokenValues).map(([k, v]) => [k, { light: v, dark: v }]))), 'codex-style: theme');
|
|
1127
|
+
// 宿主偏好是 system 且系统为浅色时只会启用 light 色板;组件私有的 light/dark 分支(如变更卡的 --changes-fill)
|
|
1128
|
+
// 不在 token 覆盖范围内。注册并保持一个暗色主题,让 data-ds-dark-theme 生效,再用上面的 token 层覆盖 Codex 配色。
|
|
1129
|
+
const DARK_THEME_ID = 'dsh-codex-dark';
|
|
1130
|
+
ctx.effect(() => {
|
|
1131
|
+
let disposeTheme;
|
|
1132
|
+
try {
|
|
1133
|
+
disposeTheme = ctx.theme.register({ id: DARK_THEME_ID, colorScheme: 'dark', tokens: {} });
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
// 注册失败时退回纯 token 覆盖,不影响插件其余部分。
|
|
1136
|
+
console.warn('codex-style: 暗色主题注册失败', error);
|
|
1137
|
+
return () => {};
|
|
1138
|
+
}
|
|
1139
|
+
const previous = ctx.theme.getTheme()?.preference;
|
|
1140
|
+
const keep = snapshot => {
|
|
1141
|
+
if (snapshot?.preference === DARK_THEME_ID) return;
|
|
1142
|
+
try { ctx.theme.setTheme(DARK_THEME_ID); } catch (error) { console.warn('codex-style: 无法启用内置暗色主题', error); }
|
|
1143
|
+
};
|
|
1144
|
+
keep(ctx.theme.getTheme());
|
|
1145
|
+
const off = ctx.on('theme/change', keep);
|
|
1146
|
+
return () => {
|
|
1147
|
+
off();
|
|
1148
|
+
try { if (previous !== undefined && previous !== DARK_THEME_ID) ctx.theme.setTheme(previous); } catch (error) { console.warn('codex-style: 恢复原主题失败', error); }
|
|
1149
|
+
disposeTheme();
|
|
1150
|
+
};
|
|
1151
|
+
}, 'codex-style: dark theme');
|
|
1152
|
+
ctx.effect(() => {
|
|
1153
|
+
const previous = document.body.getAttribute('data-dsh-codex-style');
|
|
1154
|
+
document.body.setAttribute('data-dsh-codex-style', PLUGIN_VERSION);
|
|
1155
|
+
const style = document.createElement('style'); style.dataset.dshCodexStyle = PLUGIN_VERSION; style.textContent = CSS_TEXT; document.head.appendChild(style);
|
|
1156
|
+
return () => { style.remove(); if (previous === null) document.body.removeAttribute('data-dsh-codex-style'); else document.body.setAttribute('data-dsh-codex-style', previous); };
|
|
1157
|
+
}, 'codex-style: stylesheet');
|
|
1158
|
+
const register = (scope, name, component, inject) => scope.slots.inject(name, () => {
|
|
1159
|
+
const priorities = scope.slots.entries(name).map(entry => entry.options.priority ?? 0);
|
|
1160
|
+
return scope.slots.register({ name, priority: Math.min(0, ...priorities) - 1, ...(inject ? { inject } : {}) }, component);
|
|
1161
|
+
});
|
|
1162
|
+
const components = {
|
|
1163
|
+
'sidebar.brand.mark': () => h('img', { className: 'dsc-brand-mark', src: BRAND_MARK, alt: '' }),
|
|
1164
|
+
'sidebar.brand.name': () => h('span', { className: 'dsc-ui dsc-brand' }, h('strong', null, 'deepseek'), h('small', null, 'HARNESS')),
|
|
1165
|
+
'settings.trigger': () => h('span', { className: 'dsc-ui dsc-settings-trigger' }, h(Icon, { name: 'settings', size: 18 }), '设置')
|
|
1166
|
+
};
|
|
1167
|
+
for (const [name, component] of Object.entries(components)) register(ctx, name, component);
|
|
1168
|
+
const updates = createPluginUpdateService(PLUGIN_VERSION);
|
|
1169
|
+
ctx.effect(() => { updates.start(); return () => updates.dispose(); }, 'codex-style: automatic update check');
|
|
1170
|
+
ctx.slots.inject('plugins.detail.actions', () => ctx.slots.register({ name: 'plugins.detail.actions', id: 'codex-style:check-update' }, props => h(PluginUpdateAction, { ...props, updates })));
|
|
1171
|
+
ctx.inject(['pluginNavigation'], scope => scope.slots.inject('shell.overlay', () => scope.slots.register({ name: 'shell.overlay', id: 'codex-style:update-notice' }, () => h(PluginUpdateNotice, { updates, onOpen: () => scope.reflect.get('pluginNavigation').openBundle(PLUGIN_PACKAGE) }))));
|
|
1172
|
+
// 等各区域自己的服务就绪;权限 RPC 的命名空间不能由 remote 根服务存在来推断。
|
|
1173
|
+
ctx.inject(['sessions', 'uiSession', 'workspaces', 'uiWorkspace', 'layout', 'shortcuts', 'remote', 'locale'], scope => {
|
|
1174
|
+
const sidebar = createSidebarPresentation();
|
|
1175
|
+
scope.effect(() => () => sidebar.dispose(), 'codex-style: sidebar presentation');
|
|
1176
|
+
register(scope, 'sidebar.workspaces', makeSidebarChrome(scope, sidebar));
|
|
1177
|
+
scope.slots.inject('shell.overlay', () => scope.slots.register({ name: 'shell.overlay', id: 'codex-style:shell' }, makeShellChrome(scope, sidebar)));
|
|
1178
|
+
register(scope, 'conversation.header.leading', makeCollapsedNewChat(scope));
|
|
1179
|
+
});
|
|
1180
|
+
ctx.inject(['locale', 'remote', 'remote.account'], scope => register(scope, 'settings.launcher', makeGearLauncher(scope)));
|
|
1181
|
+
ctx.inject(['workspaces'], scope => {
|
|
1182
|
+
register(scope, 'conversation.hero.brand.mark', EmptyHero);
|
|
1183
|
+
register(scope, 'conversation.hero.workspace', makeHeroWorkspace(scope));
|
|
1184
|
+
});
|
|
1185
|
+
// 模型控件:主动探测 modelDirectories 并重试注册,避免因服务就绪通知延迟而静默缺失、回退到宿主原生控件。
|
|
1186
|
+
// directoryFor / select 内部会访问 session Remote;注入列表与宿主模型选择插件保持一致。
|
|
1187
|
+
ctx.inject(['sessions', 'slots', 'locale', 'remote', 'remote.session'], scope => {
|
|
1188
|
+
let mounted = false;
|
|
1189
|
+
const mount = () => {
|
|
1190
|
+
if (mounted) return true;
|
|
1191
|
+
const directories = scope.get('modelDirectories');
|
|
1192
|
+
console.warn('[codex-style] modelDirectories 探测:', Boolean(directories));
|
|
1193
|
+
if (!directories) return false;
|
|
1194
|
+
const Picker = makeModelPicker(scope);
|
|
1195
|
+
scope.slots.inject('conversation.input.model', () => {
|
|
1196
|
+
const tryRegister = priority => scope.slots.register({
|
|
1197
|
+
name: 'conversation.input.model',
|
|
1198
|
+
priority,
|
|
1199
|
+
inject: sessionId => ({ directory: directories.directoryFor(sessionId) })
|
|
1200
|
+
}, Picker);
|
|
1201
|
+
try {
|
|
1202
|
+
// single slot 按最低 priority 渲染:取现有注册的最低值再低 1,才能覆盖宿主原生控件。
|
|
1203
|
+
const registered = scope.slots.entries('conversation.input.model').map(entry => entry.options?.priority ?? 0);
|
|
1204
|
+
let priority = Math.min(0, ...registered) - 1;
|
|
1205
|
+
let dispose;
|
|
1206
|
+
try {
|
|
1207
|
+
dispose = tryRegister(priority);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
priority = Math.min(priority, -1000);
|
|
1210
|
+
dispose = tryRegister(priority);
|
|
1211
|
+
}
|
|
1212
|
+
console.warn('[codex-style] 自绘模型控件已注册,priority', priority);
|
|
1213
|
+
return dispose;
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
console.error('[codex-style] 自绘模型控件注册失败', error);
|
|
1216
|
+
return () => {};
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
mounted = true;
|
|
1220
|
+
return true;
|
|
1221
|
+
};
|
|
1222
|
+
if (mount()) return;
|
|
1223
|
+
let attempts = 0;
|
|
1224
|
+
const timer = setInterval(() => { attempts += 1; if (mount() || attempts > 40) clearInterval(timer); }, 250);
|
|
1225
|
+
scope.effect(() => () => clearInterval(timer), 'codex-style: model directory probe');
|
|
1226
|
+
});
|
|
1227
|
+
ctx.inject(['connection', 'sessions', 'remote', 'remote.permissionPresets'], scope => {
|
|
1228
|
+
const permissions = permissionDirectory(scope);
|
|
1229
|
+
scope.effect(() => () => permissions.dispose(), 'codex-style: permission directory');
|
|
1230
|
+
register(scope, 'conversation.input.permission', makePermissionPicker(scope, permissions));
|
|
1231
|
+
});
|
|
1232
|
+
// 对话正文列与重绘后的输入框对齐:直接测量输入框卡片的视觉宽度并同步给正文列,
|
|
1233
|
+
// 避免宿主变量(正文 +32px)与实际渲染的卡宽不一致。
|
|
1234
|
+
ctx.effect(() => {
|
|
1235
|
+
let frame = 0;
|
|
1236
|
+
const sync = () => {
|
|
1237
|
+
frame = 0;
|
|
1238
|
+
const bar = document.querySelector('[data-slot="conversation.composer.bar"]');
|
|
1239
|
+
if (!bar) return;
|
|
1240
|
+
let width = 0;
|
|
1241
|
+
for (const card of bar.querySelectorAll('[class*="_card"]')) width = Math.max(width, card.getBoundingClientRect().width);
|
|
1242
|
+
if (width <= 0) width = bar.getBoundingClientRect().width;
|
|
1243
|
+
if (width > 0) document.documentElement.style.setProperty('--dsc-composer-visual-width', `${Math.round(width)}px`);
|
|
1244
|
+
};
|
|
1245
|
+
const schedule = () => { if (!frame) frame = requestAnimationFrame(sync); };
|
|
1246
|
+
const observer = new ResizeObserver(schedule);
|
|
1247
|
+
observer.observe(document.body);
|
|
1248
|
+
for (const el of document.querySelectorAll('[data-slot="conversation.composer.bar"], [data-conversation-content]')) observer.observe(el);
|
|
1249
|
+
window.addEventListener('resize', schedule);
|
|
1250
|
+
const timer = setInterval(schedule, 1200);
|
|
1251
|
+
schedule();
|
|
1252
|
+
return () => {
|
|
1253
|
+
observer.disconnect();
|
|
1254
|
+
window.removeEventListener('resize', schedule);
|
|
1255
|
+
clearInterval(timer);
|
|
1256
|
+
if (frame) cancelAnimationFrame(frame);
|
|
1257
|
+
document.documentElement.style.removeProperty('--dsc-composer-visual-width');
|
|
1258
|
+
};
|
|
1259
|
+
}, 'codex-style: composer width sync');
|
|
1260
|
+
}
|
|
1261
|
+
exports.name = 'codex-style';
|
|
1262
|
+
exports.inject = ['slots', 'theme'];
|
|
1263
|
+
exports.apply = apply;
|
|
1264
|
+
|
|
1265
|
+
// 只读检查与新版提醒;固定 scoped 身份,不能查询 npm 上同名的第三方包。
|
|
1266
|
+
const UPDATE_PACKAGE = '@xinxing1708/dsh-codex-style';
|
|
1267
|
+
const UPDATE_REGISTRIES = [
|
|
1268
|
+
{ id: 'npmmirror', label: 'npmmirror 中国镜像', url: 'https://registry.npmmirror.com/' },
|
|
1269
|
+
{ id: 'tencent', label: '腾讯云 npm 镜像', url: 'https://mirrors.cloud.tencent.com/npm/' },
|
|
1270
|
+
{ id: 'npm', label: 'npm 官方源', url: 'https://registry.npmjs.org/' }
|
|
1271
|
+
];
|
|
1272
|
+
|
|
1273
|
+
function updateVersionParts(value) {
|
|
1274
|
+
if (typeof value !== 'string') return null;
|
|
1275
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
|
|
1276
|
+
if (!match) return null;
|
|
1277
|
+
const pre = match[4]?.split('.');
|
|
1278
|
+
if (pre?.some(part => /^\d+$/.test(part) && part.length > 1 && part[0] === '0')) return null;
|
|
1279
|
+
return { core: match.slice(1, 4).map(BigInt), pre };
|
|
1280
|
+
}
|
|
1281
|
+
function compareUpdateVersions(left, right) {
|
|
1282
|
+
const a = updateVersionParts(left), b = updateVersionParts(right);
|
|
1283
|
+
if (!a || !b) return null;
|
|
1284
|
+
for (let i = 0; i < 3; i++) if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i] ? 1 : -1;
|
|
1285
|
+
if (!a.pre || !b.pre) return a.pre ? -1 : b.pre ? 1 : 0;
|
|
1286
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
1287
|
+
const x = a.pre[i], y = b.pre[i];
|
|
1288
|
+
if (x === y) continue;
|
|
1289
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
1290
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
|
|
1291
|
+
if (xn && yn) return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
1292
|
+
if (xn !== yn) return xn ? -1 : 1;
|
|
1293
|
+
return x > y ? 1 : -1;
|
|
1294
|
+
}
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function checkPluginUpdate(currentVersion, { signal, fetcher = globalThis.fetch, timeoutMs = 6500 } = {}) {
|
|
1299
|
+
signal?.throwIfAborted();
|
|
1300
|
+
if (!updateVersionParts(currentVersion)) return { state: 'error', message: '无法读取当前插件版本,请重新打开插件详情。' };
|
|
1301
|
+
// 并行读取多个源,取已返回的最高版本,避免先响应的滞后镜像遮住新版。
|
|
1302
|
+
const sources = await Promise.all(UPDATE_REGISTRIES.map(async registry => {
|
|
1303
|
+
const controller = new AbortController();
|
|
1304
|
+
const abort = () => controller.abort();
|
|
1305
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
1306
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
1307
|
+
try {
|
|
1308
|
+
const response = await fetcher(registry.url + UPDATE_PACKAGE.replace('/', '%2F') + '/latest', {
|
|
1309
|
+
signal: controller.signal, mode: 'cors', credentials: 'omit', cache: 'no-store', redirect: 'error', referrerPolicy: 'no-referrer', headers: { Accept: 'application/json' }
|
|
1310
|
+
});
|
|
1311
|
+
if (response.status === 404) return { ...registry, status: 'missing' };
|
|
1312
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
1313
|
+
const metadata = await response.json();
|
|
1314
|
+
if (metadata.name !== UPDATE_PACKAGE || !updateVersionParts(metadata.version)) throw new Error('无效的包元数据');
|
|
1315
|
+
return { ...registry, status: 'ok', version: metadata.version };
|
|
1316
|
+
} catch {
|
|
1317
|
+
return { ...registry, status: 'error' };
|
|
1318
|
+
} finally {
|
|
1319
|
+
clearTimeout(timer);
|
|
1320
|
+
signal?.removeEventListener('abort', abort);
|
|
1321
|
+
}
|
|
1322
|
+
}));
|
|
1323
|
+
signal?.throwIfAborted();
|
|
1324
|
+
const available = sources.filter(source => source.status === 'ok');
|
|
1325
|
+
if (!available.length) {
|
|
1326
|
+
const unpublished = sources.find(source => source.id === 'npm')?.status === 'missing';
|
|
1327
|
+
return { state: unpublished ? 'unpublished' : 'error', sources, message: unpublished ? '该包尚未发布到 npm,或已被撤回,暂时无法查询版本。' : '暂时无法从更新源读取版本,请稍后重试。' };
|
|
1328
|
+
}
|
|
1329
|
+
const latest = available.reduce((best, source) => {
|
|
1330
|
+
const compared = compareUpdateVersions(source.version, best.version);
|
|
1331
|
+
return compared > 0 || (compared === 0 && source.id === 'npm') ? source : best;
|
|
1332
|
+
});
|
|
1333
|
+
const comparison = compareUpdateVersions(latest.version, currentVersion);
|
|
1334
|
+
return { state: comparison > 0 ? 'update' : comparison < 0 ? 'ahead' : 'current', latestVersion: latest.version, source: latest.label, mirror: latest.id !== 'npm', sources };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
const UPDATE_CACHE_KEY = 'dsh-codex-style:update-check:v1';
|
|
1338
|
+
const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
1339
|
+
const UPDATE_RETRY_MS = 60 * 60 * 1000;
|
|
1340
|
+
function createPluginUpdateService(currentVersion, { query = checkPluginUpdate } = {}) {
|
|
1341
|
+
let lastSuccess = 0, retryAt = 0, timer, pending, controller, started = false, disposed = false;
|
|
1342
|
+
let cached = {};
|
|
1343
|
+
try { cached = JSON.parse(window.localStorage.getItem(UPDATE_CACHE_KEY) || '{}') || {}; } catch { /* 存储不可用时仍可在本次运行检查。 */ }
|
|
1344
|
+
const validCache = cached.currentVersion === currentVersion && Number.isFinite(cached.checkedAt) && cached.checkedAt > 0 && cached.checkedAt <= Date.now() && updateVersionParts(cached.latestVersion);
|
|
1345
|
+
if (validCache) lastSuccess = cached.checkedAt;
|
|
1346
|
+
const comparison = validCache ? compareUpdateVersions(cached.latestVersion, currentVersion) : null;
|
|
1347
|
+
const store = createSnapshotStore({
|
|
1348
|
+
state: validCache ? comparison > 0 ? 'update' : comparison < 0 ? 'ahead' : 'current' : 'idle',
|
|
1349
|
+
checking: false,
|
|
1350
|
+
...(validCache ? { latestVersion: cached.latestVersion, source: UPDATE_REGISTRIES.find(row => row.label === cached.source)?.label, mirror: cached.mirror === true } : {}),
|
|
1351
|
+
dismissedVersion: updateVersionParts(cached.dismissedVersion) ? cached.dismissedVersion : undefined
|
|
1352
|
+
});
|
|
1353
|
+
const save = () => {
|
|
1354
|
+
const state = store.getSnapshot();
|
|
1355
|
+
try { window.localStorage.setItem(UPDATE_CACHE_KEY, JSON.stringify({ currentVersion, checkedAt: lastSuccess, latestVersion: state.latestVersion, source: state.source, mirror: state.mirror, dismissedVersion: state.dismissedVersion })); } catch { /* 不阻断检查或提醒。 */ }
|
|
1356
|
+
};
|
|
1357
|
+
const schedule = delay => {
|
|
1358
|
+
clearTimeout(timer);
|
|
1359
|
+
if (started && !disposed) timer = setTimeout(runScheduled, Math.max(0, delay));
|
|
1360
|
+
};
|
|
1361
|
+
const dueIn = () => Math.max(lastSuccess ? lastSuccess + UPDATE_INTERVAL_MS : 0, retryAt) - Date.now();
|
|
1362
|
+
async function runScheduled() {
|
|
1363
|
+
if (disposed) return;
|
|
1364
|
+
if (dueIn() > 0) { schedule(dueIn()); return; }
|
|
1365
|
+
if (window.navigator.onLine === false) { retryAt = Date.now() + UPDATE_RETRY_MS; schedule(dueIn()); return; }
|
|
1366
|
+
await check();
|
|
1367
|
+
}
|
|
1368
|
+
function check() {
|
|
1369
|
+
if (disposed) return Promise.reject(new Error('更新检查已停用'));
|
|
1370
|
+
if (pending) return pending;
|
|
1371
|
+
controller = new AbortController();
|
|
1372
|
+
const signal = controller.signal;
|
|
1373
|
+
store.set({ ...store.getSnapshot(), checking: true });
|
|
1374
|
+
pending = Promise.resolve().then(() => query(currentVersion, { signal })).then(result => {
|
|
1375
|
+
if (disposed || signal.aborted) return result;
|
|
1376
|
+
store.set({ ...store.getSnapshot(), message: undefined, ...result, checking: false });
|
|
1377
|
+
if (['current', 'update', 'ahead'].includes(result.state)) { lastSuccess = Date.now(); retryAt = 0; save(); }
|
|
1378
|
+
else retryAt = Date.now() + UPDATE_RETRY_MS;
|
|
1379
|
+
return result;
|
|
1380
|
+
}).catch(error => {
|
|
1381
|
+
if (!disposed && !signal.aborted) {
|
|
1382
|
+
store.set({ ...store.getSnapshot(), state: 'error', checking: false, message: '检查失败,请稍后重试。' });
|
|
1383
|
+
retryAt = Date.now() + UPDATE_RETRY_MS;
|
|
1384
|
+
}
|
|
1385
|
+
return { state: 'error' };
|
|
1386
|
+
}).finally(() => { pending = null; controller = null; if (!disposed) schedule(dueIn()); });
|
|
1387
|
+
return pending;
|
|
1388
|
+
}
|
|
1389
|
+
const onOnline = () => { retryAt = 0; schedule(dueIn()); };
|
|
1390
|
+
const onVisible = () => { if (document.visibilityState === 'visible') schedule(dueIn()); };
|
|
1391
|
+
return {
|
|
1392
|
+
store, check,
|
|
1393
|
+
dismiss() { store.set({ ...store.getSnapshot(), dismissedVersion: store.getSnapshot().latestVersion }); save(); },
|
|
1394
|
+
start() {
|
|
1395
|
+
if (started || disposed) return;
|
|
1396
|
+
started = true;
|
|
1397
|
+
window.addEventListener('online', onOnline);
|
|
1398
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
1399
|
+
schedule(Math.max(8000, dueIn()));
|
|
1400
|
+
},
|
|
1401
|
+
dispose() {
|
|
1402
|
+
disposed = true; started = false;
|
|
1403
|
+
clearTimeout(timer); controller?.abort();
|
|
1404
|
+
window.removeEventListener('online', onOnline);
|
|
1405
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function PluginUpdateNotice({ updates, onOpen }) {
|
|
1411
|
+
const state = useSource(updates.store);
|
|
1412
|
+
if (compareUpdateVersions(state.latestVersion, PLUGIN_VERSION) !== 1 || state.dismissedVersion === state.latestVersion) return null;
|
|
1413
|
+
return h('aside', { className: 'dsc-ui dsc-update-notice', role: 'status', 'aria-live': 'polite', 'aria-label': 'Codex Style 更新提醒' },
|
|
1414
|
+
h('div', { className: 'dsc-update-heading' }, h('strong', null, 'Codex Style 有新版本'), h('button', { type: 'button', className: 'dsc-update-close', 'aria-label': '忽略此版本提醒', onClick: () => updates.dismiss() }, h(Icon, { name: 'close' }))),
|
|
1415
|
+
h('p', { className: 'dsc-update-note' }, 'v' + PLUGIN_VERSION + ' → v' + state.latestVersion),
|
|
1416
|
+
h('button', { type: 'button', className: 'dsc-update-retry', onClick: onOpen }, '查看更新'));
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function PluginUpdateAction({ subject, updates }) {
|
|
1420
|
+
const pkg = subject?.pkg;
|
|
1421
|
+
if (subject?.kind !== 'bundle' || !pkg?.installed) return null;
|
|
1422
|
+
const legacy = PLUGIN_PACKAGE === PLUGIN_ID && pkg.name === PLUGIN_ID && pkg.rows?.some(row => row.rowId === 'ui-skin-codex-style' && row.moduleName === PLUGIN_ID);
|
|
1423
|
+
if (pkg.name !== UPDATE_PACKAGE && !legacy) return null;
|
|
1424
|
+
return h(PluginUpdateControl, { key: pkg.name + '@' + pkg.version, currentVersion: pkg.version, updates });
|
|
1425
|
+
}
|
|
1426
|
+
function PluginUpdateControl({ currentVersion, updates }) {
|
|
1427
|
+
const anchor = useRef(null);
|
|
1428
|
+
const service = useMemo(() => updates || createPluginUpdateService(currentVersion), [updates, currentVersion]);
|
|
1429
|
+
const [open, setOpen] = useState(false);
|
|
1430
|
+
const result = useSource(service.store);
|
|
1431
|
+
const busy = result.checking;
|
|
1432
|
+
useEffect(() => () => { if (!updates) service.dispose(); }, [service, updates]);
|
|
1433
|
+
const close = useCallback(() => { setOpen(false); }, []);
|
|
1434
|
+
const check = async () => {
|
|
1435
|
+
setOpen(true);
|
|
1436
|
+
await service.check();
|
|
1437
|
+
};
|
|
1438
|
+
const messages = { checking: '正在检查更新…', current: '已是最新版本', ahead: '当前版本较新', update: '发现新版本', unpublished: '暂未查到发布版本', error: '检查失败' };
|
|
1439
|
+
return h('span', { className: 'dsc-update-control', 'data-update-state': busy ? 'checking' : result.state },
|
|
1440
|
+
h(Button, { variant: 'outline', size: 'sm', ref: anchor, disabled: busy, icon: h(IconRefreshOutlineRegular, { size: 13, className: busy ? 'dsc-update-spin' : undefined }), onClick: check, 'aria-haspopup': 'dialog', 'aria-expanded': open }, busy ? '检查中…' : '检查更新'),
|
|
1441
|
+
open && h(Popover, { anchor, onClose: close, label: '检查插件更新', role: 'dialog', width: 336, className: 'dsc-update-panel' },
|
|
1442
|
+
h('div', { className: 'dsc-update-heading' }, h('strong', null, '插件更新'), h('button', { type: 'button', className: 'dsc-update-close', onClick: close, 'aria-label': '关闭更新检查' }, h(Icon, { name: 'close' }))),
|
|
1443
|
+
h('p', { className: 'dsc-update-message', role: 'status', 'aria-live': 'polite', 'data-state': result.state }, messages[busy ? 'checking' : result.state]),
|
|
1444
|
+
h('dl', { className: 'dsc-update-versions' }, h('dt', null, '当前版本'), h('dd', null, currentVersion ? 'v' + currentVersion : '未知'), result.latestVersion && h(React.Fragment, null, h('dt', null, busy || ['error', 'unpublished'].includes(result.state) ? '上次查到' : '最新版本'), h('dd', null, 'v' + result.latestVersion))),
|
|
1445
|
+
result.source && h('p', { className: 'dsc-update-note' }, '来源:' + result.source, result.mirror && h('span', null, '镜像同步可能稍有延迟。')),
|
|
1446
|
+
result.message && h('p', { className: 'dsc-update-note' }, result.message),
|
|
1447
|
+
result.state === 'update' && h('p', { className: 'dsc-update-note' }, '新版本已发布,请按安装说明更新插件。'),
|
|
1448
|
+
!busy && h('button', { type: 'button', className: 'dsc-update-retry', onClick: check }, '重新检查')));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
return module.exports;
|
|
1452
|
+
}
|
|
1453
|
+
});
|