/** * 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 (
{t('produced')} {shown.map(path => { const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) const name = at === -1 ? path : path.slice(at + 1) return ( ) })} {hidden > 0 && +{hidden}}
) } /** * Register the turn-tail interception (returns the disposer). * * The slot is a CHILD slot the host's ui-conversation declares in its * `conversation.chat.node` children table (kind: chain, scope: session). * Registering it directly races the declaration — the ui-slots core's * load-time validation throws "not declared (a parent entry's children * table must declare it)" when the parent entry is not on the ledger yet. * slots.inject waits for the declaration: the callback runs synchronously * when the slot is already declared, otherwise it runs inside the declaring * register() call once the declaration commits; declaration collapse * disposes the entry and a later declaration re-registers it. This mirrors * @deepseek-ai/dsh-client-ui-deliverables' registration of the same slot. */ export function registerTurnTailInterception(ctx: Context, store: SidebarStore): () => void { return ctx.slots.inject('conversation.chat.turnTail', () => 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. Also // while the sidebar is externally disabled (aionui-panel chosen). select: (owner) => { if (store.getSuspended()) return null 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.getSuspended() && store.getPrefs().interceptOpenPath !== false && store.getPrefs().tabsEnabled['editor'] !== false, currentSessionId: () => ctx.sessions.list.getSnapshot().current, openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) }, }) }