feat(web): open the workspace from the sidebar file tree

This commit is contained in:
yudshj authored and imccyu committed 2026-09-29 15:15:38 +08:00
1 parent 2d15b9b3e7
commit be0e31233c
18 files changed
+154 -40

No files matched your search

@@ -0,0 +1,6 @@
- text: {{cwd}}/keyboard-project
- button "Reload"
- button "Open in Finder"
- button "More ways to open"
- list:
- listitem: Empty directory
+24
View File
@@ -160,6 +160,30 @@ describe.skipIf(mode === 'record')('web e2e: workspace shortcuts', () => {
await bind('Open locally')
await page.keyboard.press('Meta+Shift+Comma')
await expect.poll(() => launched).toEqual([{ app: 'vscode', path: workspace.path }])
await page.keyboard.press('Meta+Alt+P')
const files = page.locator('[data-files-state="tree"]')
await files.locator('[data-files-row="empty"]').waitFor()
const opener = files.locator('[data-open-target="directory"]')
await opener.getByRole('button', { name: 'Open in VS Code', exact: true }).click()
await expect.poll(() => launched).toEqual([
{ app: 'vscode', path: workspace.path }, { app: 'vscode', path: workspace.path },
])
await opener.getByRole('button', { name: 'More ways to open', exact: true }).click()
await page.getByRole('menuitem', { name: 'Finder', exact: true }).click()
await expect.poll(() => launched.at(-1)).toEqual({ app: 'finder', path: workspace.path })
await opener.getByRole('button', { name: 'Open in Finder', exact: true }).waitFor()
await page.keyboard.press('Meta+Shift+Comma')
await expect.poll(() => launched).toEqual([
{ app: 'vscode', path: workspace.path }, { app: 'vscode', path: workspace.path },
{ app: 'finder', path: workspace.path }, { app: 'finder', path: workspace.path },
])
await compareOrRefreshGolden(join(root, 'files-open.expected.md'),
await captureStableAria(page, '[data-files-state="tree"]', scaffold.workspaceCwd), mode)
await opener.getByRole('button', { name: 'More ways to open', exact: true }).click()
await page.getByRole('menuitem', { name: 'Finder (default)', exact: true }).waitFor()
await page.keyboard.press('Escape')
await page.getByRole('menu').waitFor({ state: 'hidden' })
expect(console.pageErrors).toEqual([])
})
})
@@ -18,11 +18,11 @@
en: 056e2bd9e3c78dd8
zh: be35d2ba53be0442
/deepseek-ai-dsh-client-ui-open-in-app/use-this-package/what-to-expect:
en: c93c175a3583820a
zh: fa812b70176d3d60
en: 856b3127245f4d21
zh: 6a1910efa522134f
/deepseek-ai-dsh-client-ui-open-in-app/understand-the-implementation:
en: c8ac8873c45eb568
zh: fd623c411f79d90f
en: 2c5976da969fede9
zh: 5eb1702233a55048
/deepseek-ai-dsh-client-ui-open-in-app/further-exploration:
en: 9deb509f65b6edfd
zh: d6cd63b2abc3731b
@@ -36,5 +36,5 @@
en: 3d77fca62d3c8e86
zh: 3125d32a158e690a
/deepseek-ai-dsh-client-ui-open-in-app/known-limitations-and-deferred-work/dev-note:
en: 739fd95270d14d0a
zh: d5a6b59133ac6b3f
en: c79e15f681adeacd
zh: 6a30399297bde201
+3 -3
View File
@@ -29,7 +29,7 @@ Mount this plugin in the Web composition beside [`dsh-host-open-in-app`](../../h
### What to expect
The Session header and document header use the same 24px-high split button with 9px corners. Both headers show only the icon; their tooltip names the default application or the reveal action. Both show the default action’s icon, mark its application with “(default)” in the menu, disable while their own gesture runs, and report failures through a transient toast. The directory adapter uses the existing cross-platform application catalog and remembers the last successful choice in `dsh.open-in-app.choice`; a missing choice falls back to the first available application.
The Session header, file-tree header, and document header use the same 24px-high split button with 9px corners. These headers show only the icon; their tooltip names the default application or the reveal action. They show the default action’s icon, mark its application with “(default)” in the menu, disable while their own gesture runs, and report failures through a transient toast. The file-tree opener targets its Session’s workspace and shares the application choice and launch state with the Session header. The directory adapter uses the existing cross-platform application catalog and remembers the last successful choice in `dsh.open-in-app.choice`; a missing choice falls back to the first available application.
The **Open locally** shortcut captures the main Session's directory and the same remembered application as the header button. The effective binding appears in the button tooltip and `aria-keyshortcuts`; Web follows the [shortcut service’s platform defaults](../shortcuts/README.md). The command runs only while the Conversation is selected and the header button has a directory and installed application to open. An in-flight launch also blocks the command. Pointer and keyboard gestures share the controller's launch state, so repeated gestures cannot launch twice or change the remembered choice mid-launch.
@@ -45,7 +45,7 @@ Mounted controls with the same association reader and file share one query and i
<details>
<summary>Implementation internals — click to expand</summary>
The plugin registers the split button on `conversation.session.header.utilities` through the standard slot/inject currency and registers the `open-in-app` dictionaries as one effect. A page-lifetime controller ([`src/client/controller.ts`](src/client/controller.ts)) owns the once-per-page availability read, the persisted choice snapshot store, and the launch POST; the component receives the shared sources through the inject `hooks` compartment, so every Session header shares one truth. Document-relative route forms and wire payload types come from the host package's browser-safe `@deepseek-ai/dsh-host-open-in-app/shared` subpath. The controller guards in-flight launches and publishes their captured directory and status; header controls derive delayed busy and transient error visuals from that source.
The plugin registers the split button on `conversation.session.header.utilities` and `sidebar.right.tab.files.actions` through the standard slot/inject currency and registers the `open-in-app` dictionaries as one effect. A page-lifetime controller ([`src/client/controller.ts`](src/client/controller.ts)) owns the once-per-page availability read, the persisted choice snapshot store, and the launch POST; the component receives the shared sources through the inject `hooks` compartment, so every Session header shares one truth. Document-relative route forms and wire payload types come from the host package's browser-safe `@deepseek-ai/dsh-host-open-in-app/shared` subpath. The controller guards in-flight launches and publishes their captured directory and status; header controls derive delayed busy and transient error visuals from that source.
The directory and file adapters supply application metadata and operations to [`OpenTargetButton`](src/client/OpenTargetButton.tsx), which owns menu ordering, default markers, icons, sizing, and gesture feedback. The file header and empty state share `FileOpenTarget`, while `OpenPathInjected.applications` queries `session.workspacePathApplications` through [`open-path.ts`](src/client/open-path.ts). Opening uses `session.openWorkspacePath`; the Host revalidates an explicitly selected handler before launch. The directory adapter keeps the existing catalog routes. `FileRouteAction` supplies the same control to delivery cards and change review through `deliverables.file.actions` and `deliverables.review.file.actions`; their authenticated routes retain Session file authorization. A failed or unavailable file query therefore needs no platform-specific UI implementation.
@@ -94,4 +94,4 @@ The feature-level decisions, including the split into the host package and this
</details>
**Runtime invariant:** No companion is published. The plugin registers one dictionary effect and five slot entries whose disposal the HMR-safety spec proves; application availability, the choice, and the desktop answer live in the controllers' snapshot stores with no second copy to diverge.
**Runtime invariant:** No companion is published. The plugin registers one dictionary effect and six slot entries whose disposal the HMR-safety spec proves; application availability, the choice, and the desktop answer live in the controllers' snapshot stores with no second copy to diverge.
+3 -3
View File
@@ -29,7 +29,7 @@ kind: "package-reference"
### 预期行为
会话标题栏和文档标题栏共用同一个高 24px、圆角 9px 的分体按钮。两个标题栏都只显示图标,悬停提示显示默认应用名称或文件定位动作。两者都显示默认动作的图标,在菜单的默认应用后标注“(默认)”,仅在自己的操作执行期间禁用,并通过短暂提示报告失败。目录适配器使用已有的跨平台应用列表,将最后一次成功选择保存在 `dsh.open-in-app.choice` 中;原选择不可用时回退到第一个可用应用。
会话标题栏、文件树标题栏和文档标题栏共用同一个高 24px、圆角 9px 的分体按钮。这些标题栏都只显示图标,悬停提示显示默认应用名称或文件定位动作。它们都显示默认动作的图标,在菜单的默认应用后标注“(默认)”,仅在自己的操作执行期间禁用,并通过短暂提示报告失败。文件树打开按钮以所属 Session 的工作区为目标,并与会话头部共享应用选择和启动状态。目录适配器使用已有的跨平台应用列表,将最后一次成功选择保存在 `dsh.open-in-app.choice` 中;原选择不可用时回退到第一个可用应用。
**在本地打开**快捷键捕获主会话的目录,并使用与头部按钮相同的已记住应用。按钮 tooltip 和 `aria-keyshortcuts` 显示当前有效绑定;Web 遵循[快捷键服务的平台默认值](../shortcuts/README.zh.md)。只有选中会话界面、且头部按钮有可打开的目录和已安装应用时,命令才执行。启动尚未结束时也会阻止命令。鼠标与键盘操作共享 controller 的启动状态,重复操作不会启动两次,也不会在启动期间更改已记住的选择。
@@ -45,7 +45,7 @@ kind: "package-reference"
<details>
<summary>实现内幕——点击展开</summary>
插件通过标准 slot/inject 机制把分体按钮注册到 `conversation.session.header.utilities`,并以一个 effect 注册 `open-in-app` 词典。一个页面生命周期的 controller([`src/client/controller.ts`](src/client/controller.ts))拥有每页一次的可用性读取、持久化选择的 snapshot store 与启动 POST;组件经 inject 的 `hooks` 隔间接收共享源,因此所有会话头部共享同一份事实。文档相对的路由形式与 wire 载荷类型来自主机包的浏览器安全子路径 `@deepseek-ai/dsh-host-open-in-app/shared`。controller 守卫执行中的启动,并发布所捕获的目录与状态;头部控件从该源派生延迟出现的等待态和短暂错误态。
插件通过标准 slot/inject 机制把分体按钮注册到 `conversation.session.header.utilities` 和 `sidebar.right.tab.files.actions`,并以一个 effect 注册 `open-in-app` 词典。一个页面生命周期的 controller([`src/client/controller.ts`](src/client/controller.ts))拥有每页一次的可用性读取、持久化选择的 snapshot store 与启动 POST;组件经 inject 的 `hooks` 隔间接收共享源,因此所有会话头部共享同一份事实。文档相对的路由形式与 wire 载荷类型来自主机包的浏览器安全子路径 `@deepseek-ai/dsh-host-open-in-app/shared`。controller 守卫执行中的启动,并发布所捕获的目录与状态;头部控件从该源派生延迟出现的等待态和短暂错误态。
目录和文件适配器把应用信息与操作交给 [`OpenTargetButton`](src/client/OpenTargetButton.tsx),由它统一管理菜单顺序、默认标记、图标、尺寸和操作反馈。文件标题栏和空态共用 `FileOpenTarget`,`OpenPathInjected.applications` 通过 [`open-path.ts`](src/client/open-path.ts) 查询 `session.workspacePathApplications`。打开操作使用 `session.openWorkspacePath`,Host 在启动前重新验证指定的关联应用。`FileRouteAction` 通过 `deliverables.file.actions` 和 `deliverables.review.file.actions` 为交付卡片和变更对比页提供同一控件,其认证路由保留会话文件校验。目录适配器继续使用已有的应用列表路由,文件查询失败或不可用时无需增加平台专用的界面实现。
@@ -94,4 +94,4 @@ Host 在打开或定位前通过当前文件系统验证路径。没有对应 Ho
</details>
**运行时不变式:** 不发布伴生入口。插件注册一个词典 effect 和五个 slot 条目,HMR 安全性 spec 证明它们都会在资源释放时撤销;应用可用性、选择与桌面回答存储在控制器的快照存储中,不存在可能与之分歧的第二份副本。
**运行时不变式:** 不发布伴生入口。插件注册一个词典 effect 和六个 slot 条目,HMR 安全性 spec 证明它们都会在资源释放时撤销;应用可用性、选择与桌面回答存储在控制器的快照存储中,不存在可能与之分歧的第二份副本。
@@ -69,6 +69,7 @@
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-session": "workspace:*",
"@deepseek-ai/dsh-client-ui-sidebar-documentpreview": "workspace:*",
"@deepseek-ai/dsh-client-ui-sidebar-files": "workspace:*",
"@deepseek-ai/dsh-client-ui-slots": "workspace:*",
"@deepseek-ai/dsh-host-open-in-app": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
@@ -7,7 +7,7 @@ import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/cli
import { NS } from './locales.ts'
import { OpenTargetButton } from './OpenTargetButton.tsx'
/** Browser operations and state injected into the Session Header contribution. */
/** Browser operations and state shared by the Session header and file tree contributions. */
export interface OpenInAppActionInjected {
hooks: {
openInAppApps: ObservableSnapshot<readonly string[] | null>
@@ -20,9 +20,9 @@ export interface OpenInAppActionInjected {
iconUrl: (appId: string) => string
}
/** Full props for the Session-header open-in-app split button. */
/** Full props for the workspace directory split button. */
export type OpenInAppActionProps =
PropsRuntime<'conversation.session.header.utilities'>
PropsRuntime<'conversation.session.header.utilities' | 'sidebar.right.tab.files.actions'>
& PropsLocale<typeof NS>
& InjectFace<OpenInAppActionInjected>
@@ -11,6 +11,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
import type {} from '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/client'
import type {} from '@deepseek-ai/dsh-client-ui-sidebar-files/client'
import type {} from '@deepseek-ai/dsh-api-gateway/client'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type {} from '@deepseek-ai/dsh-api-session-controller/remote'
@@ -39,8 +40,8 @@ export type { OpenPathEmptyActionProps } from './OpenPathEmptyAction.tsx'
export const inject = ['sessions', 'slots', 'locale', 'remote', 'remote.session', 'shortcuts', 'layout']
/**
* Client plugin body: register the dictionaries, the header split button, and
* the document preview's path controls.
* Client plugin body: register dictionaries, workspace directory controls, and
* document preview path controls.
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
@@ -77,22 +78,24 @@ export function apply(ctx: ClientContext): void {
} }
},
}), 'open-in-app: workspace command')
const directoryInjected = (): OpenInAppActionInjected => ({
hooks: {
openInAppApps: controller.apps,
openInAppChoice: controller.choice,
openInAppLaunch: controller.operation,
shortcuts: ctx.shortcuts.catalog,
},
launch: (appId, path) => controller.launch(appId, path),
choose: (appId) => { controller.choose(appId) },
iconUrl: appId => `${OPEN_IN_APP_ICON_PREFIX_ROUTE}/${appId}`,
})
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
name: 'conversation.session.header.utilities',
id: 'open-in-app',
order: -10,
locale: NS,
inject: (): OpenInAppActionInjected => ({
hooks: {
openInAppApps: controller.apps,
openInAppChoice: controller.choice,
openInAppLaunch: controller.operation,
shortcuts: ctx.shortcuts.catalog,
},
launch: (appId, path) => controller.launch(appId, path),
choose: (appId) => { controller.choose(appId) },
iconUrl: appId => `${OPEN_IN_APP_ICON_PREFIX_ROUTE}/${appId}`,
}),
id: 'open-in-app', order: -10, locale: NS, inject: directoryInjected,
}, OpenInAppAction))
ctx.slots.inject('sidebar.right.tab.files.actions', () => ctx.slots.register({
name: 'sidebar.right.tab.files.actions',
id: 'open-in-app', locale: NS, inject: directoryInjected,
}, OpenInAppAction))
const applications: OpenPathInjected['applications'] = (path, signal) => paths.applications(path, signal)
const pathInjected = (): OpenPathInjected => ({
@@ -45,6 +45,7 @@ async function bench() {
name: 'root',
children: {
'conversation.session.header.utilities': { kind: 'list', scope: 'session' },
'sidebar.right.tab.files.actions': { kind: 'list', scope: 'session' },
'sidebar.right.tab.document.actions': { kind: 'list', scope: 'session' },
'sidebar.right.tab.document.unpreviewable': { kind: 'list', scope: 'session' },
'deliverables.file.actions': { kind: 'list', scope: 'session' },
@@ -193,14 +194,18 @@ describe('open-in-app browser half', () => {
expect(ctx.slots.entries('deliverables.file.actions')).toHaveLength(0)
})
it('registers the header split button, and fiber teardown removes it (HMR safety)', async () => {
it('shares the directory control and controller across headers and removes both on teardown', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ apps: [] }), { status: 200 })))
const { ctx, fiber } = await bench()
const entry = ctx.slots.entries('conversation.session.header.utilities')[0]
const files = ctx.slots.entries('sidebar.right.tab.files.actions')[0]
expect(files?.component).toBe(OpenInAppAction)
expect(files?.inject).toBe(entry?.inject)
expect(entry?.component).toBe(OpenInAppAction)
expect(entry?.options).toMatchObject({ id: 'open-in-app' })
await fiber.dispose()
expect(headerEntryIds(ctx)).not.toContain('open-in-app')
expect(ctx.slots.entries('sidebar.right.tab.files.actions')).toHaveLength(0)
})
it('injects the controller face: availability sources, launch carrier, choice, and icon URLs', async () => {
@@ -47,6 +47,9 @@
{
"path": "../ui-sidebar-documentpreview/tsconfig.client.json"
},
{
"path": "../ui-sidebar-files"
},
{
"path": "../ui-slots"
},
@@ -15,8 +15,8 @@
en: 9411fb4809692e09
zh: dd13990d39c5be7f
/deepseek-ai-dsh-client-ui-sidebar-files/what-it-registers:
en: a13aadd9b0294870
zh: d30dc67f177f6c2d
en: 3ce68ed86f6fa80f
zh: e55ba82f50226106
/deepseek-ai-dsh-client-ui-sidebar-files/the-tree:
en: a0ae0b1f223b3df1
zh: 8ddc325519bd75b2
+1 -1
View File
@@ -27,7 +27,7 @@ Browse a Session's workspace tree and open files in Sidebar previews. The root a
The `workspace.files` command opens or focuses the file page in the focused pane; from the conversation it uses the current Session's active dock pane. A guide gives way to the file page, and repeated opens retain one file page per pane. Desktop defaults to Mod+P. The guide entry displays the effective shortcut; Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves this command unbound by default.
- **The type** — `ctx.sidebarRightTabs.register(...)` with kind `files`, id `@deepseek-ai/dsh-client-ui-sidebar-files`, band `builtin`, no patterns, and one guide entry (order 10, its title and description from the `sidebarFiles` namespace, its glyph a yellow folder) that opens the type.
- **The body** — the keyed `sidebar.right.pane.tab` seat under that id: a header row under the strip, then the tree. The shared [`PathLabel`](../ui-primitives/README.md#component-catalog) displays the root path with subdued directories and a primary final segment. A clipped path retains its trailing characters with a left-edge fade; hovering reveals the full path. The reload control stays at its right.
- **The body** — the keyed `sidebar.right.pane.tab` seat under that id: a header row under the strip, then the tree. The shared [`PathLabel`](../ui-primitives/README.md#component-catalog) displays the root path with subdued directories and a primary final segment. A clipped path retains its trailing characters with a left-edge fade; hovering reveals the full path. The reload control and the Session-scoped `sidebar.right.tab.files.actions` list stay at its right; [`ui-open-in-app`](../ui-open-in-app/README.md) contributes the workspace directory opener.
- **The chip title** — the keyed `sidebar.right.pane.tab.title` seat under that id: a shared `FileTypeIcon` folder glyph at 16px before the type's label. The tree's own rows never draw this sheet.
Source files under `src/client/`: `definition.tsx` (the type), `store.ts` (what it keeps), `face.ts` (Remote reads and watches), `directory-node.ts` (open directories and their lifetimes), `FilesBody.tsx` (what it draws, with its ordering and failure-line helpers), `FilesTitle.tsx` (the chip title), `locales.ts` (what it says), and `index.ts` (the wiring).
@@ -27,7 +27,7 @@ kind: "package-reference"
命令 `workspace.files` 在焦点分栏内打开或聚焦文件页;从聊天区触发时使用当前会话的活动停靠分栏。文件页替换开始页,同一分栏内重复打开仍保留一个文件页。桌面默认键为 Mod+P。开始页入口显示有效快捷键;Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 默认不绑定此命令。
- **类型**:`ctx.sidebarRightTabs.register(...)`,kind 为 `files`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-files`,档位 `builtin`,没有 patterns,另有一个打开该类型的引导页入口(order 10,标题与描述取自 `sidebarFiles` 命名空间,图标为黄色文件夹)。
- **正文**:以该 id 为键的 `sidebar.right.pane.tab` slot:strip 下的一行标题行,然后是树。共享的 [`PathLabel`](../ui-primitives/README.zh.md#component-catalog) 显示根路径,目录使用弱化颜色,最后一段使用主色。路径过长时保留尾部字符并在左侧渐隐;悬停显示完整路径。重新读取控件位于右端。
- **正文**:以该 id 为键的 `sidebar.right.pane.tab` slot:strip 下的一行标题行,然后是树。共享的 [`PathLabel`](../ui-primitives/README.zh.md#component-catalog) 显示根路径,目录使用弱化颜色,最后一段使用主色。路径过长时保留尾部字符并在左侧渐隐;悬停显示完整路径。重新读取控件和 Session 作用域的 `sidebar.right.tab.files.actions` 列表位于右端;[`ui-open-in-app`](../ui-open-in-app/README.zh.md) 在此提供工作区目录打开按钮。
- **标签页标题**:以该 id 为键的 `sidebar.right.pane.tab.title` slot:类型标签前的一枚 16px 共享 `FileTypeIcon` 文件夹图标。树本身的行不画这枚图标。
`src/client/` 下的源文件:`definition.tsx`(类型是什么)、`store.ts`(它保存什么)、`face.ts`(Remote 读取与监听)、`directory-node.ts`(打开的目录及其生命周期)、`FilesBody.tsx`(它画什么,含排序与失败行两个辅助函数)、`FilesTitle.tsx`(标签页标题)、`locales.ts`(它说什么)、`index.ts`(接线)。
@@ -6,13 +6,13 @@
* draw for each absolute path and what a click means: a directory toggles, a
* file opens through the owner's `tabActions` for a `file:` viewer to claim, and
* anything else is shown but refuses to open. The header uses the shared
* PathLabel for the root, followed by reload for the expanded directories.
* PathLabel for the root, followed by reload and workspace directory actions.
*/
import { useEffect, useLayoutEffect, useRef } from 'react'
import type { ReactNode } from 'react'
import clsx from 'clsx'
import type { RemoteFailure } from '@deepseek-ai/dsh-api-remotes/client'
import type { PropsLocale, PropsRuntime, PropsStore, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
import {
FileTypeIcon, IconFolderCloseRegular, IconFolderOpenRegular, IconRefreshOutlineRegular, Tooltip, classifyFileType,
IconPauseOutlineRegular, IconPlayOutlineRegular, PathLabel,
@@ -28,6 +28,7 @@ import css from './FilesBody.module.css'
/** The body's composed props: the tab it draws, its store, its face, and its copy. */
export type FilesBodyProps =
& PropsRuntime<'sidebar.right.pane.tab'>
& PropsRenderSlots<'sidebar.right.tab.files.actions'>
& PropsStore<ReturnType<typeof createFilesStore>>
& FilesInjected
& PropsLocale<'sidebarFiles'>
@@ -135,7 +136,7 @@ function Level({ path, tree }: { path: string; tree: TreeContext }): ReactNode {
/** The file tree's body: the workspace root and whatever the reader has opened under it. */
export function FilesBody({
useTabInfo, sessionId, useSessions, useStore, actions, start, refresh, setAutoRefresh, toggle, t,
useTabInfo, sessionId, useSessions, useStore, actions, start, refresh, setAutoRefresh, toggle, t, renderSlot,
}: FilesBodyProps): ReactNode {
const { tab } = useTabInfo()
useEffect(() => tab.actions.bindCommands({ refresh: () => { refresh(tab.id) } }), [tab.actions, tab.id, refresh])
@@ -210,6 +211,7 @@ export function FilesBody({
<IconRefreshOutlineRegular />
</button>
</Tooltip>
{renderSlot('sidebar.right.tab.files.actions', {})}
</div>
<div
ref={bodyRef}
@@ -28,6 +28,13 @@ export type { DirLevel, FilesState, FilesTabState, LevelState } from './store.ts
export type { FilesInjected, ListWorkspaceDirectory, WorkspaceFilesListRemote } from './face.ts'
export type { FilesBodyProps } from './FilesBody.tsx'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
/** Workspace directory actions after the file tree's reload control. */
'sidebar.right.tab.files.actions': { kind: 'list'; scope: 'session' }
}
}
/** This package's copy namespace. */
const NS = 'sidebarFiles'
@@ -70,7 +77,10 @@ export function apply(ctx: ClientContext): void {
const store = createFilesStore()
const inject = filesFace(createList(ctx.remote), createWatch(ctx.remote))
ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register(
{ name: 'sidebar.right.pane.tab', key: FILES_ID, locale: NS, store, inject },
{
name: 'sidebar.right.pane.tab', key: FILES_ID, locale: NS, store, inject,
children: { 'sidebar.right.tab.files.actions': { kind: 'list', scope: 'session' } },
},
FilesBody,
)), 'ui-sidebar-files: files tab body')
ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register(
@@ -87,6 +87,7 @@ function harness(cwd: string | null, refreshShortcut?: ReturnType<FilesBodyProps
actions: tabActions, refreshShortcut,
},
}),
renderSlot: () => null,
sessionId: SESSION,
useSessions: <S,>(sel: (s: SessionListState) => S) => sel(sessions),
useStore: hookOf(instance),
@@ -3453,6 +3453,62 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.document.unpreviewable\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.document.unpreviewable\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts:78',
},
{
key: 'sidebar.right.tab.files.actions',
kind: 'list',
scope: 'session',
summary: 'Workspace directory actions after the file tree\'s reload control.',
doc: 'Workspace directory actions after the file tree\'s reload control.',
registerOptions: [
{
name: 'id',
requirement: 'required',
type: 'string',
doc: 'Your cell key. Use an id of your own: a fresh id is added beside the shipped entries, while reusing a shipped id puts you in THAT cell and replaces it. Owners that filter by id address you by it.',
},
{
name: 'order',
requirement: 'optional',
type: 'number',
doc: 'Position among the entries, ascending (default 0).',
},
{
name: 'label',
requirement: 'optional',
type: 'string | (() => string)',
doc: 'Display text where the owner projects one (nav rows, tabs). A thunk is re-read on every projection, so localized text follows the active locale without re-registering.',
},
],
ownerProps: [],
ownerPropsReferences: [],
standardProps: [
'useResource: UseResource',
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
'usePanelInfo: UsePanelInfo',
'useSessions: UseSessions',
'useSessionStatus: UseSessionStatus',
'useSessionRetainInfo: UseSessionRetainInfo',
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
'useChat: UseChat',
'useConversation: UseConversation',
'useInput: SnapshotSelectorHook<InputState>',
'inputActions: InputActions',
'useSession: SessionSnapshotSelector',
'sessionId: SessionId',
'useProjection: UseProjection',
'useTrajectory: UseTrajectory',
],
keyDomain: '',
hookContext: '',
slotInject: '',
declaredBy: 'an entry in \'sidebar.right.pane.tab\' (client-ui-sidebar-files), so it exists while that entry is mounted',
occupants: [
'client-ui-open-in-app OpenInAppAction id \'open-in-app\'',
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.files.actions\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.files.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-sidebar-files/src/client/index.ts:34',
},
{
key: 'sidebar.right.tab.guide',
kind: 'chain',
+3
View File
@@ -4142,6 +4142,9 @@ importers:
'@deepseek-ai/dsh-client-ui-sidebar-documentpreview':
specifier: workspace:*
version: link:../ui-sidebar-documentpreview
'@deepseek-ai/dsh-client-ui-sidebar-files':
specifier: workspace:*
version: link:../ui-sidebar-files
'@deepseek-ai/dsh-client-ui-slots':
specifier: workspace:*
version: link:../ui-slots