包装 ctx.workspaces.openPath(聊天区文件打开的唯一漏斗),接管时 经 openSidebarFile 开进 sidebar 编辑器而非系统默认应用;由 interceptOpenPath pref(默认开)与 editor tab 启用态双重门控, 拒接时原样放行,dispose 还原原始方法引用(HMR 安全)。 - prefs-shared/config/prefs: 新增 interceptOpenPath 字段(默认 true) - openpath-intercept.ts: 零依赖包装器,纯插件层面实现,不改 DSH - intercept.tsx: registerOpenPathInterception 接线(当前会话 scope) - SideCardSection: 常规区新增开关卡片(zh/en 文案) - 测试: 包装器单测 6 个 + 接线集成测试 + pref 解析/设置页断言更新
95 lines
4.2 KiB
TypeScript
95 lines
4.2 KiB
TypeScript
/**
|
|
* Interception of the chat's produced-files row: the turn-tail chain entry
|
|
* that replaces ui-deliverables' row when the closing turn produced files.
|
|
* The takeover looks identical (same chip row); the chips open the file in
|
|
* the sidebar instead of the host OS. Priority -1 runs before the default-0
|
|
* deliverables entry; when nothing was produced the selector returns null
|
|
* and the original row renders unchanged.
|
|
*/
|
|
import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
import type { Context } from '../context-types.ts'
|
|
import type { SidebarStore } from './state.ts'
|
|
import { t } from './locales.ts'
|
|
import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
|
|
import { wrapOpenPath } from './openpath-intercept.ts'
|
|
import css from './sidebar.module.css'
|
|
|
|
/** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
|
|
export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): void {
|
|
const summary = ctx.sessions.list.getSnapshot().byId[sessionId]
|
|
const absolute = resolveSidebarPath(summary?.cwd, path)
|
|
const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\'))
|
|
const title = at === -1 ? absolute : absolute.slice(at + 1)
|
|
// Route through the sidebar service so the editor descriptor's dedupeKey
|
|
// (per-path) applies; the id is path-derived so multiple editors coexist.
|
|
ctx.betterSidebar?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
|
|
}
|
|
|
|
/** The intercepted produced-files row (visual twin of the deliverables chips). */
|
|
export function SidebarProducedFiles(props: {
|
|
matched: readonly string[]
|
|
openInSidebar: (path: string) => void
|
|
}) {
|
|
const { matched, openInSidebar } = props
|
|
const shown = matched.slice(0, 6)
|
|
const hidden = matched.length - shown.length
|
|
return (
|
|
<div className={css.producedRow}>
|
|
<span className={css.producedLabel}>{t('produced')}</span>
|
|
{shown.map(path => {
|
|
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
const name = at === -1 ? path : path.slice(at + 1)
|
|
return (
|
|
<button
|
|
key={path}
|
|
type="button"
|
|
className={css.producedChip}
|
|
title={path}
|
|
onClick={() => { openInSidebar(path) }}
|
|
>
|
|
<IconCodeOutline16 size={12} />
|
|
<span>{name}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
{hidden > 0 && <span className={css.producedMore}>+{hidden}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Register the turn-tail interception (returns the disposer). */
|
|
export function registerTurnTailInterception(ctx: Context, store: SidebarStore): () => void {
|
|
return ctx.slots.register({
|
|
name: 'conversation.chat.turnTail',
|
|
// Decline the takeover while the editor tab type is disabled in the side
|
|
// card settings: the produced-files row falls back to the default
|
|
// deliverables behavior instead of offering chips that cannot open.
|
|
select: (owner) => {
|
|
if (store.getPrefs().tabsEnabled['editor'] === false) return null
|
|
return selectProducedFiles(owner)
|
|
},
|
|
priority: -1,
|
|
registrant: 'dsh-better-sidebar',
|
|
inject: (sessionId: string) => ({
|
|
openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) },
|
|
}),
|
|
}, SidebarProducedFiles)
|
|
}
|
|
|
|
/**
|
|
* Register the chat file-open interception: wraps `ctx.workspaces.openPath`
|
|
* — the single funnel every chat-side file open goes through (tool-row path
|
|
* links, the produced-files row, prose mentions) — so opens land in the
|
|
* sidebar editor instead of the Host OS. Gated by BOTH the `interceptOpenPath`
|
|
* pref and the editor tab's enable switch; declined opens fall through to
|
|
* the original method. Returns the disposer restoring the original (HMR-safe).
|
|
*/
|
|
export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
|
|
return wrapOpenPath(ctx.workspaces, {
|
|
takeoverEnabled: () => store.getPrefs().interceptOpenPath !== false
|
|
&& store.getPrefs().tabsEnabled['editor'] !== false,
|
|
currentSessionId: () => ctx.sessions.list.getSnapshot().current,
|
|
openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
|
|
})
|
|
}
|