Host half (src/index.ts): /sidebar/api JSON API (session cwd, fs tree/read/ write, git status/diff/stage/commit/branch/log/show), /sidebar/file media route, /sidebar/ws/terminal WebSocket over node-pty; routes share the /api trust fence (Host-header loopback or connection.trustedHosts). Client half (src/client): portal-mounted right panel with a per-session split-pane workbench (recursive row/col splits, draggable dividers, tab drag-move), tab bar with max-width 160px + plus-menu (explorer/git/terminal multi-open rules), lazy file explorer rooted at the session cwd, editable text editor (dirty dot, Ctrl/Cmd+S), image + Markdown preview, xterm terminal with reconnect, and a VSCode-style git panel. Per-session state persists to localStorage (dsh-sidebar:v1:<sessionId>). Intercepts the chat turn-tail produced-files row (priority -1 chain entry) to open files in the sidebar. UI styled with the dsh --dsw-* theme tokens and ui-primitives. Build: tsdown emits the host lib plus the __ModuleLoader__ client bundle (externals = module table words; xterm/css inlined). Tests: 19 unit + smoke specs (pure state/tree/git parsers + real git/fs integration).
75 lines
2.8 KiB
TypeScript
75 lines
2.8 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 { openTab } from './state.ts'
|
|
import { sidebarStore } from './state.ts'
|
|
import { t } from './locales.ts'
|
|
import { resolveSidebarPath, selectProducedFiles } from './produced-files.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, 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)
|
|
sidebarStore.reduce(state => openTab(state, {
|
|
id: `editor:${absolute}`,
|
|
type: 'editor',
|
|
title,
|
|
path: 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): () => void {
|
|
return ctx.slots.register({
|
|
name: 'conversation.chat.turnTail',
|
|
select: selectProducedFiles,
|
|
priority: -1,
|
|
registrant: 'dsh-better-sidebar',
|
|
inject: (sessionId: string) => ({
|
|
openInSidebar: (path: string) => { openSidebarFile(ctx, sessionId, path) },
|
|
}),
|
|
}, SidebarProducedFiles)
|
|
}
|