feat: explorer copy-path & @-reference actions; icons on tabs and + menu

- Tabs, the + menu, and empty-pane cards carry per-type icons (folder /
  branch / terminal / code file); a terminal glyph joins the icon set
- Hovering an explorer row reveals an @文件 button at the far right that
  appends @<relative path> to the composer draft (session-scoped input
  service, space-separated append)
- Right-clicking a row opens a context menu: copy relative / absolute
  path (writeClipboard + transient in-row "copied" label)
- Rows become div role=button (Enter/Space) to host the inner button;
  relativeTo helper unit-tested
This commit is contained in:
Menghuan1918
2026-08-08 23:46:14 +08:00
parent 88b08de957
commit 6e36a99c88
11 changed files with 306 additions and 26 deletions
+28 -3
View File
@@ -22,6 +22,8 @@ import {
import { Workbench, type WorkbenchActions } from './split-pane.tsx'
import type { NewTabOption } from './TabBar.tsx'
import type { TabDragPayload } from './TabBar.tsx'
import { tabTypeIcon } from './icons.tsx'
import { relativeTo } from './paths.ts'
import { ExplorerView } from './ExplorerView.tsx'
import { EditorView } from './EditorView.tsx'
import { TerminalView } from './TerminalView.tsx'
@@ -38,9 +40,10 @@ function TabContent(props: {
cwd: string | undefined
expanded: string[]
onToggleDir: (path: string) => void
onReferenceFile: (path: string) => void
ctx: Context
}) {
const { tab, sessionId, cwd, expanded, onToggleDir, ctx } = props
const { tab, sessionId, cwd, expanded, onToggleDir, onReferenceFile, ctx } = props
const scope = { sessionId, cwd }
switch (tab.type) {
case 'explorer':
@@ -51,6 +54,7 @@ function TabContent(props: {
expanded={expanded}
onToggle={onToggleDir}
onOpenFile={(path) => { openSidebarFile(ctx, sessionId, path) }}
onReferenceFile={onReferenceFile}
/>
)
case 'git':
@@ -68,12 +72,13 @@ function buildNewTabOptions(state: SidebarState): NewTabOption[] {
.flatMap(leaf => leaf.tabs)
.filter(tab => tab.type === 'terminal').length
return [
{ id: 'explorer', label: t('openExplorer') },
{ id: 'git', label: t('openGit') },
{ id: 'explorer', label: t('openExplorer'), icon: tabTypeIcon('explorer', 16) },
{ id: 'git', label: t('openGit'), icon: tabTypeIcon('git', 16) },
{
id: 'terminal',
label: terminalCount >= TERMINAL_LIMIT ? `${t('newTerminal')} (${t('terminalLimit')})` : t('newTerminal'),
disabled: terminalCount >= TERMINAL_LIMIT,
icon: tabTypeIcon('terminal', 16),
},
]
}
@@ -214,6 +219,25 @@ export function Sidebar(props: { ctx: Context }) {
})
}
/**
* The explorer's @-reference button: append `@<relative path>` to the
* session's composer draft (space-separated). Resolves the session-scope
* ctx and the conversation input service at click time; a missing service
* or scope degrades to a logged no-op, never a crash.
*/
const referenceInChat = useCallback((path: string): void => {
try {
const actx = ctx.sessions.scope(sessionId)
if (actx === undefined) return
const input = ctx.conversation.input.for(actx)
const mention = `@${relativeTo(cwd ?? '', path)}`
const draft = input.state.getSnapshot().draft
input.setDraft(draft.trim() === '' ? mention : `${draft} ${mention}`)
} catch (error) {
console.warn('[dsh-better-sidebar] reference insert failed:', error)
}
}, [ctx, sessionId, cwd])
const renderTab = (tab: SidebarTab) => (
<TabContent
tab={tab}
@@ -221,6 +245,7 @@ export function Sidebar(props: { ctx: Context }) {
cwd={cwd}
expanded={state.expanded}
onToggleDir={(path) => { sidebarStore.reduce(s => toggleExpanded(s, path)) }}
onReferenceFile={referenceInChat}
ctx={ctx}
/>
)