From 696f8070b9a47e3371ec8da643b10a1d351521e6 Mon Sep 17 00:00:00 2001 From: Aisland Date: Wed, 19 Aug 2026 01:55:36 +0800 Subject: [PATCH] feat: anim pane type + bind button redesign (aligned double-circle icon, status popup) + auto-bind rule + workspaces route BOM tolerance --- 01_content/src/client/index.tsx | 62 ++++++-- 01_content/src/client/locales.ts | 18 ++- 01_content/src/client/split.tsx | 41 +++++- 01_content/src/client/styles.ts | 27 +++- 01_content/src/index.ts | 3 +- .../worklogs/2026-08-16_worktable-v2.md | 22 +++ 04_test/autobind-mini.cjs | 63 ++++++++ 04_test/bindpop-mini.cjs | 80 +++++++++++ 04_test/probe-batch5.cjs | 134 ++++++++++++++++++ 9 files changed, 428 insertions(+), 22 deletions(-) create mode 100644 04_test/autobind-mini.cjs create mode 100644 04_test/bindpop-mini.cjs create mode 100644 04_test/probe-batch5.cjs diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index 99b4925..f94774a 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -382,7 +382,7 @@ async function promptIntoSession(sessionId: string, text: string): Promise export type NewSessionGroup = { kind: 'none' } | { kind: 'existing'; workspaceId: string } | { kind: 'new'; parent: string; name: string } /** 自定义窗口任务:新建一个专属会话(可选指定分组),注入工作台背景 + 用户需求,并打开该会话 */ -export async function createCustomSession(projectId: string, projectName: string, requirement: string, group?: NewSessionGroup): Promise { +export async function createCustomSession(projectId: string, projectName: string, requirement: string, group?: NewSessionGroup): Promise { const b = sessionBridge if (!b || typeof b.sessions?.create !== 'function') throw new Error('sessions unavailable') const text = [ @@ -424,6 +424,7 @@ export async function createCustomSession(projectId: string, projectName: string const sessionId = await b.sessions.create(workspaceId ? { workspaceId } : {}) try { await b.sessions.open?.(sessionId) } catch {} await promptIntoSession(sessionId, text) + return sessionId } /** 把自定义需求直接发到用户选定的已有会话(默认当前会话) */ @@ -553,7 +554,7 @@ function WorktableSection(props: any) { // 注意:不走 props.useSessions hook(其宿主包装在部分版本会触发 useSyncExternalStore // 崩溃),改为 apply 里订阅 ctx.sessions.list 后写入模块级 store,此处直接读取。 useEffect(() => { - setSplitEnv({ + const env = { getScope: () => { const s = sessionScopeStore.snapshot return s ? { sessionId: s.sessionId, cwd: s.cwd } : null @@ -587,9 +588,20 @@ function WorktableSection(props: any) { }, createWorkspace: (path: string) => sessionBridge?.workspaces?.create?.({ path }), createWorkspaceDir: (parent: string, name: string) => sessionBridge?.workspaces?.createDirectory?.(parent, name), + // 自动绑定规则:当前项目未绑定 → 绑定到刚新建/发送的会话;已绑定 → 不改(返回 'kept') + autoBind: (sessionId: string): 'auto' | 'kept' | 'none' => { + const pid = splitStore.active && splitStore.spec ? splitStore.spec.id : null + if (!pid || typeof sessionId !== 'string' || !sessionId) return 'none' + const already = projectsRef.current.projects.bindings[pid] + if (already) return 'kept' + persistProjects((prev) => (prev.bindings[pid] ? prev : { ...prev, bindings: { ...prev.bindings, [pid]: sessionId } })) + return 'auto' + }, }, - }) - return () => setSplitEnv(null) + } + setSplitEnv(env) + try { (window as any).__dshCustomEnv = env } catch {} + return () => { setSplitEnv(null); try { (window as any).__dshCustomEnv = null } catch {} } }, []) const floatRef = useRef(null) @@ -742,6 +754,15 @@ function WorktableSection(props: any) { } }, []) +/** 绑定会话信息:文件夹(工作区)+ 会话标题(供绑定弹窗状态行展示) */ +function bindInfoOf(groups: { title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[], sid: string): { folder: string; title: string } { + for (const g of groups) { + const s = g.sessions.find((x) => x.id === sid) + if (s) return { folder: g.title || '未分组', title: s.title } + } + return { folder: '', title: sid } +} + /** 自定义布局提示词模板(复制到剪贴板,可发给任意 dsh 会话让 agent 实现) */ function buildCustomLayoutPrompt(req: string): string { return [ @@ -1102,7 +1123,10 @@ function buildCustomLayoutPrompt(req: string): string { bindBtn = document.createElement('span') bindBtn.className = 'dsh-wt_bindBtn' bindBtn.setAttribute('role', 'button') - bindBtn.textContent = '🔗' + const circles = document.createElement('span') + circles.className = 'dsh-wt_bindCircles' + circles.setAttribute('aria-hidden', 'true') + bindBtn.appendChild(circles) el.appendChild(bindBtn) const cs = getComputedStyle(el) if (cs.position === 'static') el.style.position = 'relative' @@ -1110,7 +1134,7 @@ function buildCustomLayoutPrompt(req: string): string { bindBtn.setAttribute('data-wt-bind', id) const bound = projects.bindings[id] bindBtn.setAttribute('data-bound', bound ? 'true' : 'false') - bindBtn.title = t('bind.title') + bindBtn.title = bound ? t('bind.titleBound') : t('bind.titleUnbound') const icon = el.children[0] as HTMLElement | null if (icon) { const ovr = projects.iconOverrides[id] @@ -1542,7 +1566,25 @@ function buildCustomLayoutPrompt(req: string): string { {bindPick && (
{t('bind.title')} -
{projects.nameOverrides[bindPick.id] ?? metas[bindPick.id]?.name ?? projects.layouts.find((l) => l.id === bindPick.id)?.title ?? bindPick.id}
+ {(() => { + const sid = projects.bindings[bindPick.id] + if (!sid) { + return ( +
+ + {t('bind.unbound')} +
+ ) + } + const info = bindInfoOf(bindGroups, sid) + return ( +
+ 📂 {info.folder} + | + {info.title} +
+ ) + })()}

{t('bind.hint')}

{projects.bindings[bindPick.id] && ( @@ -1622,7 +1664,7 @@ function buildCustomLayoutPrompt(req: string): string { type="button" className="dsh-wt_layout" data-on={activeSplitId === l.id ? 'true' : 'false'} - style={{ order: effectiveOrder.indexOf(l.id) + 1000 }} + style={{ order: effectiveOrder.indexOf(l.id) + 1000, position: 'relative' }} onClick={() => { openSplit(l); reportUsed(l.id) }} > { e.stopPropagation(); openBindPick(l.id, e.currentTarget as HTMLElement) }} - >🔗 + > › ))} diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index 9911715..034a8d1 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -58,6 +58,8 @@ export const zh = { 'pane.tasks': '任务管理', 'pane.terminal': '终端', 'pane.custom': '自定义', + 'pane.anim': '动画', + 'pane.animUrlPh': '粘贴动画站点地址后回车', 'custom.title': '自定义这个窗口', 'custom.hint': '发送后将自动新建一个会话,专门负责这个窗口的自定义设计。', 'custom.placeholder': '告诉 AI 你想让这个窗口做什么,例如:做一个展示我旅行足迹的时间线页面…', @@ -83,7 +85,10 @@ export const zh = { 'custom.groupNeedPath': '请填写分组父目录和新分组名', 'bind.title': '绑定对话', 'bind.unbind': '解绑', - 'bind.hint': '选择后,打开该项目时右侧对话窗自动切换到这个会话;解绑后不再切换。', + 'bind.unbound': '未绑定对话', + 'bind.titleUnbound': '绑定对话(未绑定 ○○):点击选择要跟随的对话', + 'bind.titleBound': '已绑定对话(●●):点击更换或解绑', + 'bind.hint': '一个项目只能绑定一个对话。选择后,打开该项目时右侧对话窗会自动切换过去;已绑定的项目不会因新建/发送对话而自动改绑。', 'customLayout.add': '自定义', 'customLayout.addTitle': '自定义一个布局(生成提示词)', 'customLayout.title': '自定义布局', @@ -91,6 +96,8 @@ export const zh = { 'customLayout.copy': '复制提示词到剪贴板', 'customLayout.copied': '提示词已复制到剪贴板。去任意一个 DSH 对话里粘贴发送,对方的 dsh 就知道要做什么了。', 'customLayout.copyFail': '复制失败,请手动全选输入框内容复制。', + 'custom.autoBound': '已自动把这个项目绑定到这个对话(卡片上的 ●● 按钮可更换或解绑)。', + 'custom.keptBinding': '该项目已有绑定的对话,按规则未自动更改。', 'pane.wip': '开发中(后续版本接入)', 'pane.explorerNoCwd': '当前会话没有工作目录', 'pane.openLater': '该类型暂不支持预览', @@ -172,6 +179,8 @@ export const en = { 'pane.tasks': 'Tasks', 'pane.terminal': 'Terminal', 'pane.custom': 'Custom', + 'pane.anim': 'Animation', + 'pane.animUrlPh': 'Paste the animation site URL and press Enter', '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…', @@ -197,7 +206,10 @@ export const en = { 'custom.groupNeedPath': 'Fill in the parent folder and new group name', 'bind.title': 'Bind conversation', 'bind.unbind': 'Unbind', - 'bind.hint': 'When bound, opening this project switches the right chat pane to this conversation. Unbind to stop switching.', + 'bind.unbound': 'No conversation bound', + 'bind.titleUnbound': 'Bind conversation (unbound ○○): pick the conversation to follow', + 'bind.titleBound': 'Conversation bound (●●): click to change or unbind', + 'bind.hint': 'A project can bind exactly one conversation. When bound, opening the project switches the right chat pane to it; an existing binding is never auto-changed by creating/sending conversations.', 'customLayout.add': 'Custom', 'customLayout.addTitle': 'Customize a layout (generate a prompt)', 'customLayout.title': 'Custom layout', @@ -205,6 +217,8 @@ export const en = { 'customLayout.copy': 'Copy prompt to clipboard', 'customLayout.copied': 'Prompt copied to clipboard. Paste it into any DSH conversation and send — that dsh will know what to do.', 'customLayout.copyFail': 'Copy failed — select the text above and copy it manually.', + 'custom.autoBound': 'This project was auto-bound to this conversation (use the ●● button on its card to change or unbind).', + 'custom.keptBinding': 'This project already has a bound conversation, so it was not auto-changed.', '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 ccf02b3..0e0f48c 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' | 'custom' +export type BuiltinType = 'browser' | 'anim' | 'explorer' | 'scm' | 'tasks' | 'terminal' | 'custom' export type SplitContent = | { kind: 'iframe'; url: string; title?: string } @@ -123,6 +123,7 @@ const PERSIST_KEY = 'dsh.worktable.split.v2' /** 内置内容窗图标 */ const BUILTIN_ICONS: Record = { browser: '🌐', + anim: '🎬', explorer: '📁', scm: '🔀', tasks: '✅', @@ -132,6 +133,7 @@ const BUILTIN_ICONS: Record = { const BUILTIN_LABEL_KEYS: Record = { browser: 'pane.browser', + anim: 'pane.anim', explorer: 'pane.explorer', scm: 'pane.scm', tasks: 'pane.tasks', @@ -861,6 +863,31 @@ function BrowserPane() { ) } +/** 动画播放窗:iframe 壳 + 地址栏(站内自带项目/场景列表、播放、画幅切换、导出等全部控件) */ +function AnimPane() { + const [url, setUrl] = useState('') + const [src, setSrc] = useState('about:blank') + const go = () => { + const u = url.trim() + setSrc(/^(\/|https?:\/\/)/i.test(u) ? u : 'about:blank') + } + return ( + <> +
+ setUrl(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') go() }} + /> + +
+