refactor: unify refresh button at far left of web tab bar
This commit is contained in:
@@ -864,11 +864,10 @@ function makeDividerHandler(kind: 'left' | 'chat' | 'top' | 'pane' | 'topPane',
|
||||
}
|
||||
}
|
||||
|
||||
/** 浏览器内置窗:地址栏 + 刷新 + iframe(刷新 = 重新挂载 iframe,跨域也可靠) */
|
||||
function BrowserPane() {
|
||||
/** 浏览器内置窗:地址栏 + 前往;刷新统一在标签栏最左(重挂载 iframe,跨域也可靠) */
|
||||
function BrowserPane(props: { reloadKey: number }) {
|
||||
const [url, setUrl] = useState('https://example.com')
|
||||
const [src, setSrc] = useState('https://example.com')
|
||||
const [reloadKey, setReloadKey] = useState(0)
|
||||
const go = () => {
|
||||
const u = url.trim()
|
||||
setSrc(/^(\/|https?:\/\/)/i.test(u) ? u : 'about:blank')
|
||||
@@ -883,36 +882,22 @@ function BrowserPane() {
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') go() }}
|
||||
/>
|
||||
<button type="button" className="dsh-wt_browserGo" title={T('pane.refresh')} aria-label={T('pane.refresh')} onClick={() => setReloadKey((k) => k + 1)}>↻</button>
|
||||
<button type="button" className="dsh-wt_browserGo" onClick={go}>↗</button>
|
||||
</div>
|
||||
<iframe key={reloadKey} className="dsh-wt_paneFrame" src={src} title="browser" />
|
||||
<iframe key={props.reloadKey} className="dsh-wt_paneFrame" src={src} title="browser" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** iframe 内容标签(网页/站点产物):右上角常驻刷新按钮(重挂载整页刷新,跨域可靠) */
|
||||
function IframePane(props: { url: string; title?: string }) {
|
||||
const [reloadKey, setReloadKey] = useState(0)
|
||||
return (
|
||||
<div className="dsh-wt_frameWrap">
|
||||
<iframe key={reloadKey} className="dsh-wt_paneFrame" src={props.url} title={props.title ?? ''} />
|
||||
<button
|
||||
type="button"
|
||||
className="dsh-wt_frameRefresh"
|
||||
title={T('pane.refresh')}
|
||||
aria-label={T('pane.refresh')}
|
||||
onClick={() => setReloadKey((k) => k + 1)}
|
||||
>↻</button>
|
||||
</div>
|
||||
)
|
||||
/** iframe 内容标签(网页/站点产物):刷新统一在标签栏最左(重挂载整页刷新,跨域可靠) */
|
||||
function IframePane(props: { url: string; title?: string; reloadKey: number }) {
|
||||
return <iframe key={props.reloadKey} className="dsh-wt_paneFrame" src={props.url} title={props.title ?? ''} />
|
||||
}
|
||||
|
||||
/** 动画播放窗:iframe 壳 + 地址栏(站内自带项目/场景列表、播放、画幅切换、导出等全部控件) */
|
||||
function AnimPane() {
|
||||
function AnimPane(props: { reloadKey: number }) {
|
||||
const [url, setUrl] = useState('')
|
||||
const [src, setSrc] = useState('about:blank')
|
||||
const [reloadKey, setReloadKey] = useState(0)
|
||||
const go = () => {
|
||||
const u = url.trim()
|
||||
setSrc(/^(\/|https?:\/\/)/i.test(u) ? u : 'about:blank')
|
||||
@@ -927,10 +912,9 @@ function AnimPane() {
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') go() }}
|
||||
/>
|
||||
<button type="button" className="dsh-wt_browserGo" title={T('pane.refresh')} aria-label={T('pane.refresh')} onClick={() => setReloadKey((k) => k + 1)}>↻</button>
|
||||
<button type="button" className="dsh-wt_browserGo" onClick={go}>↗</button>
|
||||
</div>
|
||||
<iframe key={reloadKey} className="dsh-wt_paneFrame" src={src} title="anim" />
|
||||
<iframe key={props.reloadKey} className="dsh-wt_paneFrame" src={src} title="anim" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1548,16 +1532,16 @@ function CustomPane(props: { paneTitle?: string }) {
|
||||
}
|
||||
|
||||
/** 单个标签页的内容渲染 */
|
||||
function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number; paneTitle?: string }) {
|
||||
function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number; paneTitle?: string; reloadKey: number }) {
|
||||
const content = props.tab.content
|
||||
if (content.kind === 'iframe') {
|
||||
return <IframePane url={content.url} title={content.title ?? props.tab.title} />
|
||||
return <IframePane url={content.url} title={content.title ?? props.tab.title} reloadKey={props.reloadKey} />
|
||||
}
|
||||
if (content.kind === 'file') {
|
||||
return <FileViewer path={content.path} />
|
||||
}
|
||||
if (content.type === 'browser') return <BrowserPane />
|
||||
if (content.type === 'anim') return <AnimPane />
|
||||
if (content.type === 'browser') return <BrowserPane reloadKey={props.reloadKey} />
|
||||
if (content.type === 'anim') return <AnimPane reloadKey={props.reloadKey} />
|
||||
if (content.type === 'explorer') return <ExplorerPane row={props.row} index={props.index} />
|
||||
if (content.type === 'scm') return <GitPane />
|
||||
if (content.type === 'tasks') return <JobsPane />
|
||||
@@ -1571,14 +1555,26 @@ function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number; paneTit
|
||||
)
|
||||
}
|
||||
|
||||
/** 窗内容:标签页模型(无标签 = 6 选 1 选择器;标签可切换/关闭,关完回到选择器) */
|
||||
/** 需要标签栏刷新按钮的内容类型:网页类(iframe / 浏览器 / 动画)统一在标签最左放 ↻ */
|
||||
function refreshableTab(t: PaneTab): boolean {
|
||||
const c = t.content
|
||||
return c.kind === 'iframe' || (c.kind === 'builtin' && (c.type === 'browser' || c.type === 'anim'))
|
||||
}
|
||||
|
||||
/** 窗内容:标签页模型(无标签 = 6 选 1 选择器;标签可切换/关闭,关完回到选择器)
|
||||
* 网页类标签最左侧固定一个 ↻ 刷新按钮(标签名之前),点击重挂载该标签内容。 */
|
||||
function PaneBody(props: { pane: SplitPane; row: PaneRow; index: number }) {
|
||||
const { pane, row, index } = props
|
||||
const tabs = pane.tabs ?? []
|
||||
const active = Math.min(pane.active ?? 0, Math.max(0, tabs.length - 1))
|
||||
const [reloadKeys, setReloadKeys] = useState<Record<string, number>>({})
|
||||
if (tabs.length === 0) {
|
||||
return <PanePicker row={row} index={index} />
|
||||
}
|
||||
const refreshTab = (t: PaneTab) => {
|
||||
setReloadKeys((m) => ({ ...m, [t.id]: (m[t.id] ?? 0) + 1 }))
|
||||
splitStore.setActiveTab(row, index, t.id)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="dsh-wt_tabBar">
|
||||
@@ -1592,6 +1588,15 @@ function PaneBody(props: { pane: SplitPane; row: PaneRow; index: number }) {
|
||||
onDragEnd={() => { dragTab = null; setDropTarget(null) }}
|
||||
onClick={() => splitStore.setActiveTab(row, index, t.id)}
|
||||
>
|
||||
{refreshableTab(t) && (
|
||||
<button
|
||||
type="button"
|
||||
className="dsh-wt_tabRefresh"
|
||||
title={T('pane.refresh')}
|
||||
aria-label={T('pane.refresh')}
|
||||
onClick={(e) => { e.stopPropagation(); refreshTab(t) }}
|
||||
>↻</button>
|
||||
)}
|
||||
<span className="dsh-wt_tabTitle">{t.title}</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1602,7 +1607,7 @@ function PaneBody(props: { pane: SplitPane; row: PaneRow; index: number }) {
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<PaneTabBody tab={tabs[active]} row={row} index={index} paneTitle={pane.title} />
|
||||
<PaneTabBody tab={tabs[active]} row={row} index={index} paneTitle={pane.title} reloadKey={reloadKeys[tabs[active].id] ?? 0} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,9 +139,8 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_paneTitle{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,#9aa4b2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_paneFrame{flex:1;width:100%;border:0;background:#010409;min-height:0}',
|
||||
// iframe 内容标签:容器 + 右上角常驻刷新按钮
|
||||
'.dsh-wt_frameWrap{position:relative;flex:1;min-height:0;display:flex;flex-direction:column}',
|
||||
'.dsh-wt_frameRefresh{position:absolute;top:6px;right:6px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:6px;background:var(--dsw-alias-fill-l2,#171b22);color:var(--dsw-alias-label-primary,#e6e8eb);font-size:13px;line-height:1;cursor:pointer;opacity:.72;z-index:5}',
|
||||
'.dsh-wt_frameRefresh:hover{opacity:1;border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_tabRefresh{flex:none;width:14px;height:14px;padding:0;border:none;border-radius:3px;background:transparent;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:10px;line-height:1;cursor:pointer}',
|
||||
'.dsh-wt_tabRefresh:hover{color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.08))}',
|
||||
'.dsh-wt_splitDivider{background:var(--dsw-alias-border-l2,#3a4150);cursor:col-resize;touch-action:none;box-sizing:border-box}',
|
||||
'.dsh-wt_splitDividerH{cursor:row-resize}',
|
||||
'.dsh-wt_splitDivider:hover,.dsh-wt_splitDividerH:hover{background:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
|
||||
@@ -1089,24 +1089,19 @@
|
||||
(STEP10 断言按新行为重写)ERROR_COUNT: 0。
|
||||
- 备注:纯客户端改动,F5 即生效;重启不会自动出现该按钮,必须本次改动生效。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 补记(刷新按钮统一到标签栏最左)
|
||||
|
||||
- 用户定案:网页窗的刷新不要「地址栏 ↻ + 悬浮 ↻」两套格式——做成网页后地址栏会消失,
|
||||
悬浮按钮也找不准。统一方案:**每个网页类标签(iframe / 浏览器 / 动画)在标签栏最左侧、
|
||||
标签名之前固定一个 ↻**,点击重挂载该标签内容(并切到该标签);非网页标签不放。
|
||||
- 实现:PaneBody 增加 reloadKeys(按标签 id 计数)+ refreshableTab 判定,tab 首子元素
|
||||
渲染 .dsh-wt_tabRefresh;reloadKey 经 PaneTabBody 传入 IframePane/BrowserPane/AnimPane
|
||||
作为 iframe key。撤掉 BrowserPane/AnimPane 地址栏 ↻(只留 ↗)、撤掉 IframePane 的
|
||||
.dsh-wt_frameWrap/.dsh-wt_frameRefresh 悬浮按钮及样式(styles.ts 换为 .dsh-wt_tabRefresh)。
|
||||
- 验证(probe-batch14.cjs):iframe/浏览器/动画三类标签 ↻ 均在最左(title=刷新)且点击后
|
||||
iframe 重挂载 ✓;地址栏只剩 ↗(goCount=1)✓;frameWrap/frameRefresh 不再出现 ✓;
|
||||
tasks 非网页标签无 ↻ ✓;functional-diag 全 20 STEP ERROR_COUNT: 0。
|
||||
- 备注:纯客户端改动,F5 即生效。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// probe-batch14.cjs — 标签栏最左 ↻ 统一刷新(撤地址栏/悬浮两套旧刷新)
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const WebSocket = require('ws');
|
||||
const PORT = 9371;
|
||||
const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [
|
||||
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
|
||||
'--window-size=1440,900', '--force-device-scale-factor=1',
|
||||
'--remote-debugging-port=' + PORT,
|
||||
'--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-batch14',
|
||||
'about:blank',
|
||||
], { stdio: 'ignore' });
|
||||
const getJSON = (p) => new Promise((res, rej) => {
|
||||
http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(e)}}) }).on('error', rej);
|
||||
});
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
(async () => {
|
||||
let list = null;
|
||||
for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); }
|
||||
const target = list.find((t) => t.type === 'page') || list[0];
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
let id = 0; const pending = new Map();
|
||||
const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); });
|
||||
ws.on('message', (raw) => { const m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } });
|
||||
await new Promise((r) => ws.on('open', r));
|
||||
await send('Runtime.enable');
|
||||
await send('Page.enable');
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
|
||||
const evaluate = async (expr) => {
|
||||
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
|
||||
if (r.result && r.result.exceptionDetails) return { __error: (r.result.exceptionDetails.exception || {}).description || r.result.exceptionDetails.text };
|
||||
return r.result && r.result.result ? r.result.result.value : undefined;
|
||||
};
|
||||
let ready = null;
|
||||
for (let i = 0; i < 30; i++) { await sleep(1000); ready = await evaluate("!!window.__dshWorktable"); if (ready === true) break; }
|
||||
await sleep(1500);
|
||||
const out = await evaluate(`(async function(){
|
||||
var out = {};
|
||||
var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); };
|
||||
var vis = function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); };
|
||||
var st = window.__dshWorktable.splitStore;
|
||||
try {
|
||||
// A. iframe 网页标签:标签栏最左 ↻,无悬浮/无 wrap
|
||||
st.open({id:'t-frame',title:'f',top:null,main:[{id:'p1',title:'W',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
|
||||
await sleep(600);
|
||||
st.openTab('main', 0, { kind: 'iframe', url: 'https://example.com', title: 'web' });
|
||||
await sleep(900);
|
||||
var tab = vis('.dsh-wt_tab');
|
||||
var first = tab && tab.firstElementChild;
|
||||
out.iframe_tabRefreshFirst = !!first && first.className === 'dsh-wt_tabRefresh';
|
||||
out.iframe_refreshTitle = first ? first.getAttribute('title') : null;
|
||||
out.iframe_noWrap = !vis('.dsh-wt_frameWrap');
|
||||
out.iframe_noFloat = !vis('.dsh-wt_frameRefresh');
|
||||
var f1 = vis('.dsh-wt_paneFrame');
|
||||
if (first) { first.click(); }
|
||||
await sleep(700);
|
||||
var f2 = vis('.dsh-wt_paneFrame');
|
||||
out.iframe_remounted = !!f1 && !!f2 && f1 !== f2;
|
||||
st.close(); await sleep(300);
|
||||
// B. 浏览器内置窗:地址栏只剩 ↗(无 ↻),标签栏有 ↻ 且点击重挂载
|
||||
st.open({id:'t-br',title:'b',top:null,main:[{id:'p2',title:'B',min:200,content:{kind:'builtin',type:'browser'}}],chatWidth:{default:320,min:240,max:600}});
|
||||
await sleep(1200);
|
||||
var bar = vis('.dsh-wt_browserBar');
|
||||
out.browser_goCount = bar ? bar.querySelectorAll('.dsh-wt_browserGo').length : null;
|
||||
var btab = vis('.dsh-wt_tab');
|
||||
var bfirst = btab && btab.firstElementChild;
|
||||
out.browser_tabRefreshFirst = !!bfirst && bfirst.className === 'dsh-wt_tabRefresh';
|
||||
var g1 = vis('.dsh-wt_paneFrame');
|
||||
if (bfirst) { bfirst.click(); }
|
||||
await sleep(700);
|
||||
var g2 = vis('.dsh-wt_paneFrame');
|
||||
out.browser_remounted = !!g1 && !!g2 && g1 !== g2;
|
||||
st.close(); await sleep(300);
|
||||
// C. 动画窗:同样地址栏无 ↻、标签栏有 ↻
|
||||
st.open({id:'t-anim',title:'a',top:null,main:[{id:'p3',title:'A',min:200,content:{kind:'builtin',type:'anim'}}],chatWidth:{default:320,min:240,max:600}});
|
||||
await sleep(800);
|
||||
var abar = vis('.dsh-wt_browserBar');
|
||||
out.anim_goCount = abar ? abar.querySelectorAll('.dsh-wt_browserGo').length : null;
|
||||
var atab = vis('.dsh-wt_tab');
|
||||
var afirst = atab && atab.firstElementChild;
|
||||
out.anim_tabRefreshFirst = !!afirst && afirst.className === 'dsh-wt_tabRefresh';
|
||||
st.close(); await sleep(300);
|
||||
// D. 非网页标签(tasks):不放刷新按钮
|
||||
st.open({id:'t-tasks',title:'t',top:null,main:[{id:'p4',title:'T',min:200,content:{kind:'builtin',type:'tasks'}}],chatWidth:{default:320,min:240,max:600}});
|
||||
await sleep(800);
|
||||
var ttab = vis('.dsh-wt_tab');
|
||||
var tfirst = ttab && ttab.firstElementChild;
|
||||
out.tasks_noRefresh = !!(tfirst && tfirst.className !== 'dsh-wt_tabRefresh');
|
||||
out.tasks_firstClass = tfirst ? tfirst.className : null;
|
||||
st.close(); await sleep(200);
|
||||
} catch (e) { out.err = String(e && e.stack || e); }
|
||||
return out;
|
||||
})()`);
|
||||
console.log('RESULT ' + JSON.stringify(out));
|
||||
try { ws.close(); } catch {}
|
||||
proc.kill();
|
||||
})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} });
|
||||
@@ -0,0 +1,293 @@
|
||||
做第一个改动。我们给工作台的所有生成的项目也绑定工作区,什么意思呢?就是首先第一个改动是在他工作台添加项目的时候,你不仅要选择项目的名称,你还要选择项目的工作目录或者叫工作文件夹,强行逼着用户去建立一个对应的工作项目文件夹。然后已经建好的项目呢?也可以更改这个文件夹,你点击这个每个项目的绑定对话的两个点icon,你上面不是绑定对话吗?绑定对话的上方,再往上你再做一个类似的结构,是项目文件夹,这个文件夹也可以后面改,这个意思是说后续任何这个项目产生的文件,你要尽可能的都放在他选的这个文件夹里面,注意这件这件事情是你自己的一个代码上的修改,就是用户选择了这个文件夹之后,以后用户对这个项目做的任何内容,你尽可能的都要在他选择的文件夹里面去生成项目,而不要生成到你的C盘的你的默认位置,那样是不对的,这件事情就是为了让用户的文件夹足够的干净,让用户不要因为不知道你生成的文件放在哪里而去不敢生成。
|
||||
|
||||
第二个改动:我们假设用户在某一个项目及其绑定的对话中处理问题的时候他突然想切换到一个其他的对话。那如果他点击那个其他对话,目前的这个项目并不会自动退出,而只会把目前项目界面的那个对话替换成他选的那个对话。这里不太好,因为这件事情会让用户再手点一次他所在的那个项目,做一个反选,把项目关掉。这件事情是很麻烦的,因为当用户在这个项目的界面中选择其他对话的时候,他一定是想切换到其他的内容,而不是在继续深耕此此项目的内容,所以你没有必要再保留这个。所以我的意思就是当用户切换到其他对话,你就自动把他所在的这个项目界面给关掉。
|
||||
此外,如果用户如果用户在处理某一个项目及其对话的时候,用户做了一个反选的操作,就是他点击反选关掉了这个项目,或者他用叉号关掉了这个项目,这个时候你要自动跳跳,转到他上一个存在的对话,而不是目前这个对话。你懂这个操作吗?因为按目前的逻辑来说,假设我们假设用户在对话A中进行了一段操作,然后这个时候他切换到了项目B,项目B绑定对话B,那么而当他关掉项目B的时候,他目前界面上的对话就变成了跟项目B绑定的那个对话B,而不是他之前的对话A。所以你要在用户关掉这个项目的时候,把对话也切换成上一个,让对话跟项目的绑定关系更加紧密。
|
||||
|
||||
第三个改动,新建窗口的时候,首先你的自动生成的提示词里面,要带上窗口信息,就是要告诉对话,用户是在哪个窗口中发送的这段消息,这代表用户想把哪个窗口打造成他希望的内容,这一点你没有。然后,我试了几次,我发现对话在接到窗口发过去的提示词后,他的反应太慢,或者说处理时间太长,我把其中一个对话,从他接到窗口自动发送的信息,到他做出反应,所做的内容信息,你看看这其中有没有能简化的步骤,比如用户写好窗口作用后点击发送到A对话,这时候你给过去的信息是否可以带更多的注入信息?或者别的方法,你想想办法,我先给你看一下那个对话接到信号后的处理:上下文注入
|
||||
~/.dsh/AGENTS.md, AGENTS.md
|
||||
上下文注入
|
||||
@deepseek-ai/dsh-system-prompt
|
||||
上下文注入
|
||||
skill-catalog
|
||||
Think
|
||||
Let me understand the situation. The user has a "工作台" (worktable) plugin in DeepSeek Harness. They want to customize a window in project "2342" to be a calculator. This is a task where I should help produce content that can be put into the window.
|
||||
我来先做只读侦察,了解一下工作台插件是怎么组织窗口内容的(这决定了「计算器」以什么形式放进 2342 项目的窗口最合适)。
|
||||
|
||||
失败
|
||||
Glob
|
||||
Error: tool call timed out after 30000ms
|
||||
Glob
|
||||
**/*worktable*
|
||||
Think
|
||||
The workspace glob timed out (probably too big). Let me try a narrower search. Maybe list the workspace root directory first.
|
||||
工作区根目录搜索超时了,我先看一下目录结构,缩小范围。
|
||||
|
||||
Pwsh
|
||||
List workspace root directory
|
||||
Pwsh
|
||||
List .dsh home directory
|
||||
Think
|
||||
The workspace root is small. The worktable plugin must be elsewhere. Let me search:
|
||||
工作区根目录很小,插件不在这里。我去 DSH 本体检出目录和 .dsh 下找找 worktable 插件。
|
||||
|
||||
Pwsh
|
||||
Find worktable dirs in DSH checkout
|
||||
Pwsh
|
||||
Search .dsh for worktable files
|
||||
Think
|
||||
Found the plugin: `C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable`. Let me inspect its structure to understand how content windows work, what content types are supported, and where project "2342" state lives.
|
||||
找到了插件本体:C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable。我看看它的结构和窗口内容类型定义。
|
||||
|
||||
Pwsh
|
||||
List dsh-worktable plugin files
|
||||
Think
|
||||
Hmm, no output. Maybe the filter on node_modules is excluding everything (because the full path contains node_modules). Let me just list top-level and go down.
|
||||
过滤条件把路径都排掉了(路径本身含 node_modules)。换个方式列。
|
||||
|
||||
Pwsh
|
||||
List worktable plugin files by relative path
|
||||
Think
|
||||
The plugin has:
|
||||
插件结构清楚了,关键代码在 src/index.ts 和 src/client/*。我读一下主入口和客户端代码,重点看内容窗支持哪些类型、本地网页怎么加载。
|
||||
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\index.ts
|
||||
Think
|
||||
Now I understand the server side. The worktable serves local files via `/api/worktable/file?path=...` and local sites via `/api/worktable/site/<rootToken>/<rel>`. Also there's a `/api/worktable/workspaces` that reads `~/.dsh/storages/workspace.json` — this is the list of workspaces for custom window session grouping.
|
||||
服务端看明白了。继续读客户端两个文件,重点看「内容窗类型」「本地网页预览」和「自定义窗口会话分组」的实现。
|
||||
|
||||
Pwsh
|
||||
Check client source file sizes
|
||||
Think
|
||||
The client files are large (90KB and 72KB). Let me grep for relevant concepts: pane types, browser, preview, iframe, custom, session grouping, workspaces. Let me grep for key terms in index.tsx and split.tsx.
|
||||
文件较大,我用 grep 定位关键概念(窗类型、iframe/预览、自定义窗口、分组)。
|
||||
|
||||
Grep
|
||||
iframe|kind: 'browser'|kind: 'preview'|'custom'|workspace\.json|localStorage|windowId|sessionId
|
||||
Think
|
||||
Now I understand the pane content model:
|
||||
内容模型清楚了:custom 窗会绑定一个 DSH 会话(本对话就是它的产物)。我再看两处关键代码:CustomPane 的配置/展示逻辑,以及本地 html 如何在窗内打开;同时查一下「2342」项目在 workspace.json 里的信息。
|
||||
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\split.tsx
|
||||
Pwsh
|
||||
Read workspace.json workspaces list
|
||||
Think
|
||||
Now I understand:
|
||||
CustomPane 只是「建会话发需求」的对话框,本对话正是它的产物。要真正让窗口显示计算器,关键看窗内容如何渲染/切换。继续读几段:BrowserPane、FileViewer、窗类型选择器和持久化模型。
|
||||
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\split.tsx
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\split.tsx
|
||||
Think
|
||||
Now I have a full picture:
|
||||
内容窗机制完全清楚了:资源管理器里点击 .html 会以「目录级静态托管」方式开成 iframe 标签页。我再确认两件事:工作台里「项目」是怎么定义的(2342 对应什么目录),以及 Projects 目录现状。
|
||||
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\index.tsx
|
||||
Pwsh
|
||||
List Projects folder contents
|
||||
Think
|
||||
Interesting. `Projects` folder contains various projects (AiLearn, auto-robot, building-agent, dsh-animator, dsh-planreview, dsh-reminder, dsh-travelatlas, dsh-usage, dsh-worktable) but no "2342" folder.
|
||||
Projects 里没有 2342 文件夹。我搜一下会话记录,找到「2342」项目对应的会话和它的工作目录。
|
||||
|
||||
Grep
|
||||
2342
|
||||
Think
|
||||
No "2342" in session files. Let me check where project ids come from in the worktable. Maybe projects are the better-sidebar items or workspace groups. Let me search index.tsx for "getProjects" and "currentProjectId" implementation.
|
||||
会话记录里没有 2342。看看工作台「项目」列表的数据来源。
|
||||
|
||||
Grep
|
||||
getProjects|currentProjectId|PROJECTS_KEY|projects\.map|p\.id
|
||||
Think
|
||||
So projects are stored in localStorage under `dsh.worktable.projects.v1` — not accessible from here directly. Project "2342" is probably a project the user created in the worktable, with id "2342". The project's window (custom pane) created this session.
|
||||
「项目」列表存在浏览器 localStorage(dsh.worktable.projects.v1),服务端读不到。我看看插件的 package.json 和构建/加载方式,判断改插件新增「计算器」窗类型是否可行、代价多大。
|
||||
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\package.json
|
||||
Think
|
||||
So dsh-worktable:
|
||||
工作台插件的开发源应该就在工作区 Projects\dsh-worktable。确认一下它和 profile 里那个是否是同一份(junction 链接),以及 lib 构建产物情况。
|
||||
|
||||
失败
|
||||
Pwsh
|
||||
Check plugin source repo and lib build
|
||||
Think
|
||||
The junction: `profiles\web\node_modules\dsh-worktable` → `E:\AI_Workspace\DeepseekHarness\Projects\dsh-worktable\01_content`. And the source repo has `00_index`, `01_content`, `02_process`, `03_local`, `04_test` folders, plus AGENTS.md, README.md, LICENSE, .gitignore. The `lib` build doesn't exist in 01_content directly? Exit code 1 was from the last Get-ChildItem (lib not found). Let me look at 01_content structure.
|
||||
链接确认:profile 里的插件 junction 指向 Projects\dsh-worktable\01_content。看一下这个目录的结构和构建脚本。
|
||||
|
||||
Pwsh
|
||||
List 01_content plugin source tree
|
||||
Think
|
||||
Now everything is clear:
|
||||
插件源仓库和构建链路都清楚了。看一下项目自己的 AGENTS.md/README 和 build.mjs,了解构建与规范约定。
|
||||
|
||||
Read
|
||||
Projects\dsh-worktable\AGENTS.md
|
||||
Read
|
||||
Projects\dsh-worktable\01_content\build.mjs
|
||||
上下文注入
|
||||
Projects\dsh-worktable\AGENTS.md
|
||||
Think
|
||||
Now I have the full picture. Let me also quickly check the plugin's README.md for the pane types and how the "custom" window concept is described, plus check the PRD maybe. Actually, I have enough for planning. But a few more things worth checking for the plan:
|
||||
再确认最后一块拼图:工作台「项目」对象(id/name)到底怎么生成——这决定 2342 项目有没有对应目录、内容文件放哪最顺手。
|
||||
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\index.tsx
|
||||
Think
|
||||
OK so projects = registered alive projects (from better-sidebar slots? "aliveRegisteredIds") + custom layouts (presets). "2342" is probably the id of a registered project (maybe a better-sidebar project with id "2342") or a layout. Either way, it's a localStorage project; no folder mapping. The custom window of project 2342 spawned this session; this session's cwd is the main workspace `E:\AI_Workspace\DeepseekHarness`.
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\split.tsx
|
||||
Read
|
||||
C:\Users\SJL\.dsh\profiles\web\node_modules\dsh-worktable\src\client\split.tsx
|
||||
Think
|
||||
Recon complete. I now understand:
|
||||
侦察完毕,情况已经完全清楚了。先同步一下我掌握的关键事实,再问你几个问题定方案。
|
||||
|
||||
侦察结论(工作台插件 Projects\dsh-worktable,profile 通过 junction 加载 01_content):
|
||||
|
||||
内容窗的内容模型 = 标签页:内置窗(浏览器/动画/资源管理器/SCM/任务/终端/✨自定义) + iframe 网页 + 文件预览。未指派时显示选择器(当前 5 个磁贴)。
|
||||
「✨自定义」窗只是个对话框:负责建会话、把需求发给会话——本对话就是它在 2342 项目里的产物。它本身不渲染内容,所以「窗口变计算器」需要换一种内容形态。
|
||||
插件有完整源码和构建链:01_content/src/client/split.tsx + npm run build 出 lib/client.js;bundle 只在启动时组合,改插件必须重建并重启 dsh web。
|
||||
本地 HTML 可经 /api/worktable/site/<目录>/<文件> 目录级托管在窗口里完整渲染(资源管理器点 .html 即此路径)。
|
||||
2342 是 localStorage 里的工作台项目(无对应磁盘目录);本会话 cwd 是 E:\AI_Workspace\DeepseekHarness。
|
||||
基于此,需要你定三件事:
|
||||
|
||||
提问
|
||||
3/3 已回答
|
||||
|
||||
|
||||
自定义窗口的时候,你现在是新建对话以及发送到对话,默认是新建对话,把这两个换一个,你把默认变成【发送到对话】。两者换一个位置就行,【发动到对话】变成第一个,【新建对话】变成第二个。
|
||||
|
||||
|
||||
|
||||
第一个改动是 新建布局时候 在:【项目文件夹
|
||||
E:\AI_Workspace\DeepseekHarness\Projects
|
||||
选择位置…】这一行,你的项目文件夹和那个地址 最好左对齐,现在左边有一段空隙,往左移动一点,然后把地址栏拉长一点
|
||||
|
||||
第二个问题是,绑定对话 和 窗口的发送到对话 都不能选择【未分组对话】 列表里没有 未分组的对话
|
||||
|
||||
|
||||
"E:\AI_Workspace\MediaLab\ProductionLab\04_Project\FitnessWeb"在这个文件夹里你可以看到一个健身网站的项目,我希望你首先不要动这个项目,然后你将这个项目的文件夹结构及代码完整镜像到你的工作目录中"E:\AI_Workspace\DeepseekHarness\Projects\FitnessWeb",然后我希望在本窗口中预览该网页,以后你帮我搭建该网页的设计与信息
|
||||
|
||||
|
||||
[
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1818233854.0211,
|
||||
"hostOnly": false,
|
||||
"httpOnly": true,
|
||||
"name": "auth_token",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "33bdfe19d345dc777d23febadf696887828c98e4"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1800145440.806814,
|
||||
"hostOnly": false,
|
||||
"httpOnly": false,
|
||||
"name": "guest_id",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "v1%3A176593104162820880"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1818698386.035009,
|
||||
"hostOnly": false,
|
||||
"httpOnly": false,
|
||||
"name": "twid",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "u%3D2815174332"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1787164177.988143,
|
||||
"hostOnly": false,
|
||||
"httpOnly": true,
|
||||
"name": "__cf_bm",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "mKgX6Jll.GoZF1gaY9dYAzydSIRNhNErIPcglrYaI0g-1787162377.9095926-1.0.1.1-JODqx8m432mID.LyAI5xlAgfp0beyKp0uilJm2_5lE_hhtn6lEurkuPPfalNAqpOx9DSjMf7zJRF1eTnLvqkawXiXljbMvqbGgo3PJlyxSeiJzesKMDxM9NjHAtxHXLj"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1821257854.64504,
|
||||
"hostOnly": false,
|
||||
"httpOnly": false,
|
||||
"name": "ct0",
|
||||
"path": "/",
|
||||
"sameSite": "lax",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "0b19ce63dc884283bed67b770b4a5565af048776846463ac90f7859cd07d718260e33eafdcde3c69af8dc5beaef5d94c0a1254170e26441e0a702d67706840db6cc0469ee38cee4f0e60b3f3d074a7e8"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1821722386.034834,
|
||||
"hostOnly": false,
|
||||
"httpOnly": false,
|
||||
"name": "guest_id_ads",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "v1%3A176593104162820880"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1821722386.034945,
|
||||
"hostOnly": false,
|
||||
"httpOnly": false,
|
||||
"name": "guest_id_marketing",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "v1%3A176593104162820880"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1821257854.020862,
|
||||
"hostOnly": false,
|
||||
"httpOnly": true,
|
||||
"name": "kdt",
|
||||
"path": "/",
|
||||
"sameSite": null,
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "G0d44dZmTh88sIJGqK19XLfasLZmcdxTuD6zcKXo"
|
||||
},
|
||||
{
|
||||
"domain": ".x.com",
|
||||
"expirationDate": 1800491094.562787,
|
||||
"hostOnly": false,
|
||||
"httpOnly": false,
|
||||
"name": "personalization_id",
|
||||
"path": "/",
|
||||
"sameSite": "no_restriction",
|
||||
"secure": true,
|
||||
"session": false,
|
||||
"storeId": null,
|
||||
"value": "\"v1_BhEoSYaSGzUyazKZ2i/ueg==\""
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user