feat: 自定义窗口——需求对话框+项目选择(默认当前项目)+发送自动新建专属会话并注入worktable上下文
This commit is contained in:
@@ -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<void> {
|
||||
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<string | null>(() =>
|
||||
splitStore.active && splitStore.spec ? splitStore.spec.id : null,
|
||||
)
|
||||
/** 供分栏引擎读取的最新项目快照(setSplitEnv 闭包取最新值用) */
|
||||
const projectsRef = useRef<{ projects: ProjectsState; metas: Record<string, ProjectMeta>; 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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
+101
-35
@@ -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<BuiltinType, string> = {
|
||||
scm: '🔀',
|
||||
tasks: '✅',
|
||||
terminal: '▸_',
|
||||
custom: '✨',
|
||||
}
|
||||
|
||||
const BUILTIN_LABEL_KEYS: Record<BuiltinType, string> = {
|
||||
@@ -133,6 +134,7 @@ const BUILTIN_LABEL_KEYS: Record<BuiltinType, string> = {
|
||||
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<void>
|
||||
}
|
||||
}
|
||||
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<string | null>(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 <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('pane.wip')}</span></div>
|
||||
}
|
||||
if (done) {
|
||||
return (
|
||||
<div className="dsh-wt_customBox">
|
||||
<span className="dsh-wt_customDone" aria-hidden>✅</span>
|
||||
<p className="dsh-wt_customDoneText">{T('custom.done')}</p>
|
||||
<p className="dsh-wt_customDoneHint">{T('custom.doneHint')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="dsh-wt_customBox">
|
||||
<div className="dsh-wt_customCard">
|
||||
<span className="dsh-wt_customTitle">✨ {T('custom.title')}</span>
|
||||
<p className="dsh-wt_customHint">{T('custom.hint')}</p>
|
||||
<textarea
|
||||
className="dsh-wt_customInput"
|
||||
autoFocus
|
||||
placeholder={T('custom.placeholder')}
|
||||
value={requirement}
|
||||
onChange={(e) => setRequirement(e.target.value)}
|
||||
/>
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.project')}</span>
|
||||
<select
|
||||
className="dsh-wt_customSelect"
|
||||
value={projectId ?? ''}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="dsh-wt_customSend"
|
||||
disabled={busy || !requirement.trim() || !projectId}
|
||||
onClick={submit}
|
||||
>{busy ? '…' : T('custom.send')}</button>
|
||||
{fail && <p className="dsh-wt_customFail">{T('custom.fail')}:{fail}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 单个标签页的内容渲染 */
|
||||
function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number }) {
|
||||
const content = props.tab.content
|
||||
@@ -1249,6 +1336,7 @@ function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number }) {
|
||||
if (content.type === 'scm') return <GitPane />
|
||||
if (content.type === 'tasks') return <JobsPane />
|
||||
if (content.type === 'terminal') return <TerminalPane />
|
||||
if (content.type === 'custom') return <CustomPane />
|
||||
return (
|
||||
<div className="dsh-wt_paneWip">
|
||||
<span className="dsh-wt_paneWipIcon" aria-hidden>{BUILTIN_ICONS[content.type]}</span>
|
||||
@@ -1296,8 +1384,6 @@ function PaneBody(props: { pane: SplitPane; row: PaneRow; index: number }) {
|
||||
/** 未指派内容:4 选 1 选择器。按钮固定大小、整体居中;
|
||||
* 按窗位宽高比自适应排列:宽窗横排 4 连 / 方窗 2×2 / 竖窗竖排。 */
|
||||
function PanePicker(props: { row: PaneRow; index: number }) {
|
||||
const [custom, setCustom] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
const [mode, setMode] = useState<'row' | 'grid' | 'col'>('grid')
|
||||
const hostRef = useRef<HTMLDivElement | null>(null)
|
||||
useEffect(() => {
|
||||
@@ -1315,40 +1401,20 @@ function PanePicker(props: { row: PaneRow; index: number }) {
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
const pick = (content: SplitContent) => splitStore.openTab(props.row, props.index, content)
|
||||
const applyCustom = () => {
|
||||
const u = url.trim()
|
||||
if (/^(\/|https?:\/\/)/i.test(u)) pick({ kind: 'iframe', url: u })
|
||||
}
|
||||
return (
|
||||
<div ref={hostRef} className={'dsh-wt_panePicker dsh-wt_panePicker-' + mode}>
|
||||
{custom ? (
|
||||
<div className="dsh-wt_paneCustom">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={T('pane.customUrlPh')}
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') applyCustom() }}
|
||||
/>
|
||||
<button type="button" className="dsh-wt_paneCustomGo" onClick={applyCustom}>{T('pane.open')}</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'browser' })}>
|
||||
<span aria-hidden>🌐</span>{T('pane.browser')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'explorer' })}>
|
||||
<span aria-hidden>📁</span>{T('pane.explorer')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'terminal' })}>
|
||||
<span aria-hidden>▸_</span>{T('pane.terminal')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => setCustom(true)}>
|
||||
<span aria-hidden>✨</span>{T('pane.custom')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'browser' })}>
|
||||
<span aria-hidden>🌐</span>{T('pane.browser')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'explorer' })}>
|
||||
<span aria-hidden>📁</span>{T('pane.explorer')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'terminal' })}>
|
||||
<span aria-hidden>▸_</span>{T('pane.terminal')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'custom' })}>
|
||||
<span aria-hidden>✨</span>{T('pane.custom')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -189,6 +189,24 @@ export const css = [
|
||||
'.dsh-wt_panePicker-row{flex-direction:row;flex-wrap:wrap}',
|
||||
'.dsh-wt_panePicker-grid{display:grid;grid-template-columns:auto auto;grid-template-rows:auto auto;gap:10px;place-content:center}',
|
||||
'.dsh-wt_panePicker-col{flex-direction:column}',
|
||||
// 自定义窗口对话框(居中卡片)
|
||||
'.dsh-wt_customBox{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:14px;overflow:auto}',
|
||||
'.dsh-wt_customCard{display:flex;flex-direction:column;gap:10px;width:100%;max-width:360px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:10px;background:var(--dsw-alias-bg-base,#0b0e14);box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}',
|
||||
'.dsh-wt_customTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
'.dsh-wt_customHint{font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-secondary,#9aa4b2);margin:0}',
|
||||
'.dsh-wt_customInput{width:100%;box-sizing:border-box;min-height:96px;padding:8px 10px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;outline:none;resize:vertical}',
|
||||
'.dsh-wt_customInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_customRow{display:flex;align-items:center;gap:8px}',
|
||||
'.dsh-wt_customLabel{flex:none;font-size:11.5px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
'.dsh-wt_customSelect{flex:1;min-width:0;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;outline:none}',
|
||||
'.dsh-wt_customSend{align-self:flex-end;padding:6px 14px;border:none;border-radius:6px;background:var(--dsw-alias-state-accent-primary,#4f8ef7);color:#fff;font:inherit;font-size:12px;cursor:pointer}',
|
||||
'.dsh-wt_customSend:disabled{opacity:.55;cursor:default}',
|
||||
'.dsh-wt_customFail{font-size:11px;color:var(--dsw-alias-state-danger,#e5484d);margin:0}',
|
||||
'.dsh-wt_customDone{font-size:26px;line-height:32px}',
|
||||
'.dsh-wt_customDoneText{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb);margin:4px 0 0}',
|
||||
'.dsh-wt_customDoneHint{font-size:11.5px;color:var(--dsw-alias-label-secondary,#9aa4b2);margin:0}',
|
||||
'.dsh-wt_customDone{display:block}',
|
||||
'.dsh-wt_customBox:has(.dsh-wt_customDone){flex-direction:column;gap:4px;text-align:center}',
|
||||
'.dsh-wt_panePick{flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;width:92px;height:78px;padding:8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:10px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02));color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:11px;line-height:15px;cursor:pointer;transition:border-color .15s,background .15s,transform .15s}',
|
||||
'.dsh-wt_panePick:hover{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05));transform:translateY(-1px)}',
|
||||
'.dsh-wt_panePick span{font-size:22px;line-height:24px}',
|
||||
|
||||
@@ -639,17 +639,23 @@
|
||||
原生视图内部是浏览器扩展页,插件注入不了按钮、拦不到鼠标,也驱动不了内部滚动——
|
||||
向用户如实说明后按指示回退);② 视图选项改名「设置」,点开直接内嵌「排序方式 + 管理项目
|
||||
展开列表」,不再二次点击;③ 管理项目里给现有项目加「变更视图」:换预设拓扑,
|
||||
## 补记(网页 iframe 内部滚动位置保活)
|
||||
## 补记(自定义窗口:需求对话框 + 新建专属会话 + 项目选择 + 上下文注入)
|
||||
|
||||
- 用户反馈:MD 位置已能记忆,网页(iframe)内部滚动位置切走再切回仍丢。
|
||||
- 根因:保活池用 display:none 隐藏 → iframe 视口被压成 0×0,其内部文档滚动被钳制清零;
|
||||
MD 等普通 div 不受影响,iframe 受影响。
|
||||
- 处理:保活包装器 display:none → visibility:hidden(保留布局尺寸,内部滚动不丢;
|
||||
不可见即不可交互,无副作用)。fixture-site 加高 body 用于滚动测试。
|
||||
- 测试事故(二次):visibility 保活让隐藏层保留尺寸,回归脚本里 height>0 的可见性筛选
|
||||
全部失效 → STEP16 再次误点隐藏层保存按钮写坏 styles.ts(已 git 恢复)。修复:
|
||||
① 所有可见性筛选改 getComputedStyle(visibility)!=='hidden';
|
||||
② STEP16 保存前 fail-safe 校验激活标签目标路径必须含 wt-edit-test.md 才点击保存。
|
||||
- 验证(STEP19 + 探针):iframe contentWindow.scrollTo(0,400) → 切走 → 切回 scrollY=400;
|
||||
全 19 STEP 回归 ERROR_COUNT: 0。
|
||||
- 备注:纯客户端改动,F5 即生效。checkpoint tag: checkpoint-2026-08-17-pre-iframescroll。
|
||||
- 用户需求:点「自定义」→ 新开标签(关标签可回退)→ 居中对话框:写需求 + 选所属项目
|
||||
(默认 = 刚在用的项目)+ 提示发送后将新建一个对话 → 发送 = 自动新建会话,
|
||||
会话需知道:worktable 是什么(自建工具,官方文档没有)、本对话是解决一个窗口的自定义、
|
||||
用户需求原文、窗口位于哪个项目(便于窗口级与项目级后续自定义)。
|
||||
- 实现:
|
||||
① BuiltinType 新增 custom;PanePicker ✨ 直接 openTab 自定义标签(旧内联 URL 表单移除);
|
||||
② CustomPane:居中卡片对话框(需求 textarea + 项目 select + 提示文案 + 发送按钮),
|
||||
默认项目 = splitStore.spec.id(当前工作区所属项目),不在列表时回退第一个;发送成功后 ✓ 态;
|
||||
③ 会话创建桥:inject 增加 'conversation'(探针实测 sessions.create/open、conversation.send/
|
||||
sendSession 可用;api 不可注入已排除);createCustomSession:sessions.create({}) → 优先
|
||||
conversation.sendSession(sessionId, text) 否则 open 后 send;注入上下文消息:
|
||||
「worktable 是什么 / 本对话用途 / 用户需求 / 所在项目」四段式;
|
||||
④ WorktableSection 经 projectsRef 快照向引擎注入 custom.getProjects/currentProjectId/submit;
|
||||
locale custom.* 八键 + 对话框 CSS。
|
||||
- 验证(functional-diag STEP20,仅 UI 不点发送防真实建会话):tabType=custom、对话框/
|
||||
textarea/提示语/项目下拉/发送禁用(空需求)全部正确;全 20 STEP ERROR_COUNT: 0。
|
||||
真实建会话闭环需用户在 GUI 实测。
|
||||
- 备注:纯客户端改动,F5 即生效。checkpoint tag: checkpoint-2026-08-17-pre-customdialog。
|
||||
|
||||
@@ -637,6 +637,34 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
})()`);
|
||||
console.log('STEP19:', JSON.stringify(step19));
|
||||
|
||||
// 自定义窗口:点 ✨ → 新标签打开居中对话框;默认项目 = 当前项目(仅测 UI,不点发送以免真实建会话)
|
||||
const step20 = await evaluate(`(async function(){
|
||||
var out={};
|
||||
try{
|
||||
var st=window.__dshWorktable.splitStore;
|
||||
var vis=function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); };
|
||||
st.open({id:'t-custom',title:'c',top:null,main:[{id:'p1',title:'p',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
|
||||
await new Promise(function(r){setTimeout(r,400)});
|
||||
var pick=[].slice.call(document.querySelectorAll('.dsh-wt_panePick')).find(function(el){ var t=el.textContent||''; return (t.indexOf('\u81ea\u5b9a\u4e49')>=0||t.indexOf('Custom')>=0) && (getComputedStyle(el).visibility!=='hidden'); });
|
||||
if(pick){ pick.click(); }
|
||||
await new Promise(function(r){setTimeout(r,400)});
|
||||
out.tabOpened=st.spec.main[0].tabs.length===1;
|
||||
out.tabType=st.spec.main[0].tabs[0].content.type;
|
||||
out.dialog=!!vis('.dsh-wt_customCard');
|
||||
out.textarea=!!vis('.dsh-wt_customInput');
|
||||
var hint=vis('.dsh-wt_customHint');
|
||||
out.hint=hint?String(hint.textContent).slice(0,26):null;
|
||||
var sel=vis('.dsh-wt_customSelect');
|
||||
out.selectValue=sel?sel.value:null;
|
||||
out.defaultIsCurrent=sel?sel.value==='t-custom':false;
|
||||
var send=vis('.dsh-wt_customSend');
|
||||
out.sendDisabled=send?send.disabled:null;
|
||||
st.close();
|
||||
}catch(err){ out.err=String(err) }
|
||||
return JSON.stringify(out);
|
||||
})()`);
|
||||
console.log('STEP20:', JSON.stringify(step20));
|
||||
|
||||
// 还原临时夹具
|
||||
try { fs.writeFileSync(TEMP_MD, '# ORIG\n', 'utf8') } catch {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user