From 42645f2f5108ea9dc6a616a95ce7de0d747036f7 Mon Sep 17 00:00:00 2001 From: Aisland Date: Tue, 18 Aug 2026 01:56:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E2=80=94=E2=80=94=E9=9C=80=E6=B1=82=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86+=E9=A1=B9=E7=9B=AE=E9=80=89=E6=8B=A9(=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=BD=93=E5=89=8D=E9=A1=B9=E7=9B=AE)+=E5=8F=91?= =?UTF-8?q?=E9=80=81=E8=87=AA=E5=8A=A8=E6=96=B0=E5=BB=BA=E4=B8=93=E5=B1=9E?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=B9=B6=E6=B3=A8=E5=85=A5worktable=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_content/src/client/index.tsx | 50 ++++++- 01_content/src/client/locales.ts | 20 ++- 01_content/src/client/split.tsx | 136 +++++++++++++----- 01_content/src/client/styles.ts | 18 +++ .../worklogs/2026-08-16_worktable-v2.md | 32 +++-- 04_test/functional-diag.cjs | 28 ++++ 6 files changed, 231 insertions(+), 53 deletions(-) diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index 2013eda..8a4c978 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -262,6 +262,35 @@ function findSidebar(start: HTMLElement | null): HTMLElement | null { /** 子座位注册 id 序列(模块级 store;apply 里订阅 slots 变化写入)。 */ const registryStore: { ids: string[]; listeners: Set<() => void> } = { ids: [], listeners: new Set() } +/** 自定义窗口 → 宿主会话桥(apply 时注入;不可用时 CustomPane 降级提示) */ +let sessionBridge: { sessions: any; conversation: any } | null = null + +/** 自定义窗口任务:新建一个专属会话,注入工作台背景 + 用户需求,并打开该会话 */ +export async function createCustomSession(projectId: string, projectName: string, requirement: string): Promise { + const b = sessionBridge + if (!b || typeof b.sessions?.create !== 'function') throw new Error('sessions unavailable') + const text = [ + '【工作台自定义窗口任务】', + '以下背景由 dsh-worktable(工作台)插件自动附加,用于帮助你理解任务:', + '1. dsh-worktable 是什么:它是本机 DeepSeek Harness 的自建容器插件——侧边栏底部的「工作台」区块管理多个项目;', + ' 每个项目打开后是一个平铺工作区(若干内容窗 + 对话窗);内容窗可放浏览器、资源管理器、终端、', + ' 文件预览(MD/TXT/代码/本地网页)等,支持 6 种布局预设、变更视图、窗内容以标签页组织、状态持久化。', + ' 它是用户自建的工具,官方文档中没有它的说明;不了解之处可直接向用户提问。', + '2. 本对话的用途:专门负责「' + projectName + '」项目中一个窗口(内容窗)的自定义设计——', + ' 用户希望把这个窗口做成自定义内容。', + '3. 用户需求:' + requirement, + '4. 请基于以上背景,帮助用户完成这个窗口的自定义设计(例如产出可直接放入窗口的内容、页面或实现方案);', + ' 该窗口位于「' + projectName + '」项目内,如需要也可协助该项目后续的其他自定义工作。', + ].join('\n') + const sessionId = await b.sessions.create({}) + let sent = false + if (typeof b.conversation?.sendSession === 'function') { + try { await b.conversation.sendSession(sessionId, text); sent = true } catch { sent = false } + } + try { b.sessions.open?.(sessionId) } catch {} + if (!sent) await b.conversation?.send?.(text) +} + /** 会话作用域快照(模块级;apply 里订阅 ctx.sessions.list 写入,组件与引擎只读) */ const sessionScopeStore: { snapshot: { sessionId: string; cwd: string; jobs: any[]; subagents: any[] } | null @@ -357,6 +386,10 @@ function WorktableSection(props: any) { const [activeSplitId, setActiveSplitId] = useState(() => splitStore.active && splitStore.spec ? splitStore.spec.id : null, ) + /** 供分栏引擎读取的最新项目快照(setSplitEnv 闭包取最新值用) */ + const projectsRef = useRef<{ projects: ProjectsState; metas: Record; aliveRegisteredIds: string[] }>( + { projects, metas, aliveRegisteredIds: registeredIds }, + ) // 会话作用域(当前会话 + 工作目录)与后台任务:注入分栏引擎环境 // 注意:不走 props.useSessions hook(其宿主包装在部分版本会触发 useSyncExternalStore @@ -369,6 +402,17 @@ function WorktableSection(props: any) { }, getJobs: () => sessionScopeStore.snapshot?.jobs ?? [], getSubagents: () => sessionScopeStore.snapshot?.subagents ?? [], + custom: { + getProjects: () => { + const p = projectsRef.current + return [ + ...p.aliveRegisteredIds.map((id) => ({ id, name: p.projects.nameOverrides[id] ?? p.metas[id]?.name ?? id })), + ...p.projects.layouts.map((l) => ({ id: l.id, name: p.projects.nameOverrides[l.id] ?? l.title })), + ] + }, + currentProjectId: () => (splitStore.active && splitStore.spec ? splitStore.spec.id : null), + submit: (projectId, projectName, requirement) => createCustomSession(projectId, projectName, requirement), + }, }) return () => setSplitEnv(null) }, []) @@ -538,6 +582,7 @@ function WorktableSection(props: any) { () => registeredIds.filter((id) => !projects.removed.includes(id)), [registeredIds, projects.removed], ) + projectsRef.current = { projects, metas, aliveRegisteredIds } const allIds = useMemo(() => [...aliveRegisteredIds, ...layoutIds], [aliveRegisteredIds, layoutIds]) const effectiveOrder = useMemo(() => { const known = new Set(allIds) @@ -1277,9 +1322,12 @@ function WorktableSection(props: any) { ) } -export const inject = ['slots', 'locale', 'sessions'] +export const inject = ['slots', 'locale', 'sessions', 'conversation'] export function apply(ctx: any) { + // 自定义窗口 → 新建会话桥:保存宿主 sessions/conversation 服务引用(模块级) + sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null } + ctx.effect(() => { const style = document.createElement('style') style.setAttribute('data-dsh-plugin', 'dsh-worktable') diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index 89b05a8..1db4463 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -58,8 +58,14 @@ export const zh = { 'pane.tasks': '任务管理', 'pane.terminal': '终端', 'pane.custom': '自定义', - 'pane.customUrlPh': '地址(/xxx/ 或 https://)', - 'pane.open': '打开', + 'custom.title': '自定义这个窗口', + 'custom.hint': '发送后将自动新建一个会话,专门负责这个窗口的自定义设计。', + 'custom.placeholder': '告诉 AI 你想让这个窗口做什么,例如:做一个展示我旅行足迹的时间线页面…', + 'custom.project': '所属项目', + 'custom.send': '发送并新建对话', + 'custom.done': '已新建专属对话', + 'custom.doneHint': 'AI 正在新会话中为你设计这个窗口。关闭本标签即可返回窗口选择。', + 'custom.fail': '创建对话失败', 'pane.wip': '开发中(后续版本接入)', 'pane.explorerNoCwd': '当前会话没有工作目录', 'pane.openLater': '该类型暂不支持预览', @@ -141,8 +147,14 @@ export const en = { 'pane.tasks': 'Tasks', 'pane.terminal': 'Terminal', 'pane.custom': 'Custom', - 'pane.customUrlPh': 'URL (/xxx/ or https://)', - 'pane.open': 'Open', + 'custom.title': 'Customize this pane', + 'custom.hint': 'Sending will create a new conversation dedicated to designing this pane.', + 'custom.placeholder': 'Tell the AI what you want this pane to do, e.g. a timeline page of my travels…', + 'custom.project': 'Project', + 'custom.send': 'Send & create conversation', + 'custom.done': 'Conversation created', + 'custom.doneHint': 'The AI is designing your pane in the new conversation. Close this tab to return to the pane picker.', + 'custom.fail': 'Failed to create conversation', 'pane.wip': 'Coming in a later version', 'pane.explorerNoCwd': 'No working directory for the current session', 'pane.openLater': 'Preview not supported for this type', diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx index 8143360..3571d93 100644 --- a/01_content/src/client/split.tsx +++ b/01_content/src/client/split.tsx @@ -23,7 +23,7 @@ hljs.registerLanguage('json', hljsJson) * 内容与 chatSide 的变更经 onSpecMutated 回调交给工作台持久化(布局条目)。 */ -export type BuiltinType = 'browser' | 'explorer' | 'scm' | 'tasks' | 'terminal' +export type BuiltinType = 'browser' | 'explorer' | 'scm' | 'tasks' | 'terminal' | 'custom' export type SplitContent = | { kind: 'iframe'; url: string; title?: string } @@ -125,6 +125,7 @@ const BUILTIN_ICONS: Record = { scm: '🔀', tasks: '✅', terminal: '▸_', + custom: '✨', } const BUILTIN_LABEL_KEYS: Record = { @@ -133,6 +134,7 @@ const BUILTIN_LABEL_KEYS: Record = { scm: 'pane.scm', tasks: 'pane.tasks', terminal: 'pane.terminal', + custom: 'pane.custom', } function tabTitleOf(content: SplitContent): string { @@ -183,6 +185,12 @@ type SplitEnv = { getScope: () => SplitScope | null getJobs: () => SplitJob[] getSubagents: () => any[] + /** 自定义窗口:项目列表 + 当前项目 + 提交创建专属会话 */ + custom?: { + getProjects: () => { id: string; name: string }[] + currentProjectId: () => string | null + submit: (projectId: string, projectName: string, requirement: string) => Promise + } } let splitEnv: SplitEnv | null = null export function setSplitEnv(env: SplitEnv | null) { @@ -1235,6 +1243,85 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) { ) } +/** 自定义窗口:居中对话框。用户写下需求 + 选择所属项目 → 发送 = 新建专属会话并由 AI 辅助设计。 */ +function CustomPane() { + const custom = splitEnv?.custom + const [requirement, setRequirement] = useState('') + const [projectId, setProjectId] = useState(null) + const [projects, setProjects] = useState<{ id: string; name: string }[]>(() => custom?.getProjects?.() ?? []) + const [busy, setBusy] = useState(false) + const [done, setDone] = useState(false) + const [fail, setFail] = useState('') + // 默认项目 = 当前工作区所属项目(用户刚在用的项目) + useEffect(() => { + const list = custom?.getProjects?.() ?? [] + setProjects(list) + const cur = custom?.currentProjectId?.() ?? null + setProjectId(cur && list.some((p) => p.id === cur) ? cur : (list[0]?.id ?? null)) + }, [custom]) + const submit = async () => { + const text = requirement.trim() + if (!text || !projectId || !custom) return + setBusy(true) + setFail('') + try { + const proj = projects.find((p) => p.id === projectId) + await custom.submit(projectId, proj?.name ?? projectId, text) + setDone(true) + } catch (e) { + setFail(String(e)) + } finally { + setBusy(false) + } + } + if (!custom) { + return
{T('pane.wip')}
+ } + if (done) { + return ( +
+ ✅ +

{T('custom.done')}

+

{T('custom.doneHint')}

+
+ ) + } + return ( +
+
+ ✨ {T('custom.title')} +

{T('custom.hint')}

+