feat: 自制下拉列表——文件夹分组标题(📁+小字)与1px细分隔线,弃用原生select/optgroup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Terminal } from 'xterm'
|
||||
import 'xterm/css/xterm.css'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
@@ -1245,6 +1245,51 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 自制下拉列表(原生 select 无法美化):文件夹分组标题 + 1px 细分隔线 + 选项列表 */
|
||||
function SelectPop(props: {
|
||||
value: string | null
|
||||
groups: { title: string; items: { id: string; label: string; isCurrent?: boolean }[] }[]
|
||||
placeholder?: string
|
||||
onChange: (id: string) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const flat = props.groups.flatMap((g) => g.items)
|
||||
const selected = flat.find((i) => i.id === props.value)
|
||||
return (
|
||||
<div className="dsh-wt_select">
|
||||
<button type="button" className="dsh-wt_selectBtn" onClick={() => setOpen((v) => !v)}>
|
||||
<span className={'dsh-wt_selectVal' + (selected ? '' : ' dsh-wt_selectPh')}>{selected?.label ?? props.placeholder ?? ''}</span>
|
||||
<span className="dsh-wt_selectCaret" aria-hidden>▾</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="dsh-wt_selectList">
|
||||
{props.groups.map((g, gi) => (
|
||||
<Fragment key={g.title || 'g' + gi}>
|
||||
{g.title && (
|
||||
<>
|
||||
<div className="dsh-wt_selectDivider" />
|
||||
<div className="dsh-wt_selectGroup">📁 {g.title}</div>
|
||||
</>
|
||||
)}
|
||||
{g.items.map((it) => (
|
||||
<button
|
||||
key={it.id}
|
||||
type="button"
|
||||
className={'dsh-wt_selectItem' + (it.id === props.value ? ' dsh-wt_selectItemOn' : '')}
|
||||
onClick={() => { props.onChange(it.id); setOpen(false) }}
|
||||
>
|
||||
<span className="dsh-wt_selectItemTitle">{it.label}</span>
|
||||
{it.isCurrent && <span className="dsh-wt_selectCurrent">{T('custom.sessionCurrent')}</span>}
|
||||
</button>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 自定义窗口:居中对话框。两种模式:新建专属会话 / 发送到已有会话(默认当前会话)。 */
|
||||
function CustomPane() {
|
||||
const custom = splitEnv?.custom
|
||||
@@ -1320,37 +1365,25 @@ function CustomPane() {
|
||||
{mode === 'existing' && (
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.session')}</span>
|
||||
<select
|
||||
className="dsh-wt_customSelect"
|
||||
value={sessionId ?? ''}
|
||||
onChange={(e) => setSessionId(e.target.value)}
|
||||
>
|
||||
{sessionGroups.map((g) => g.title
|
||||
? (
|
||||
<optgroup key={g.title} label={g.title}>
|
||||
{g.sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)
|
||||
: g.sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
)),
|
||||
)}
|
||||
</select>
|
||||
<SelectPop
|
||||
value={sessionId}
|
||||
groups={sessionGroups.map((g) => ({
|
||||
title: g.title,
|
||||
items: g.sessions.map((s) => ({ id: s.id, label: s.title, isCurrent: s.isCurrent })),
|
||||
}))}
|
||||
placeholder={T('custom.session')}
|
||||
onChange={setSessionId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<SelectPop
|
||||
value={projectId}
|
||||
groups={[{ title: '', items: projects.map((p) => ({ id: p.id, label: p.name })) }]}
|
||||
placeholder={T('custom.project')}
|
||||
onChange={setProjectId}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -205,6 +205,21 @@ export const css = [
|
||||
'.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_customSelect option{background-color:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
// 自制下拉(文件夹分组 + 细分隔线)
|
||||
'.dsh-wt_select{position:relative;flex:1;min-width:0}',
|
||||
'.dsh-wt_selectBtn{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;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;line-height:18px;cursor:pointer}',
|
||||
'.dsh-wt_selectBtn:hover{border-color:var(--dsw-alias-border-l2,#3a4150)}',
|
||||
'.dsh-wt_selectVal{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_selectPh{color:var(--dsw-alias-label-tertiary,#6e7683)}',
|
||||
'.dsh-wt_selectCaret{flex:none;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:10px;line-height:1}',
|
||||
'.dsh-wt_selectList{margin-top:4px;max-height:180px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14);padding:4px;box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}',
|
||||
'.dsh-wt_selectItem{display:flex;align-items:center;gap:6px;width:100%;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:17px;cursor:pointer;text-align:left}',
|
||||
'.dsh-wt_selectItem:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_selectItemOn{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_selectItemTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_selectCurrent{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary,#6e7683)}',
|
||||
'.dsh-wt_selectDivider{height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:4px 2px}',
|
||||
'.dsh-wt_selectGroup{display:flex;align-items:center;gap:5px;padding:2px 8px 1px;font-size:10px;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,#6e7683);user-select: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}',
|
||||
|
||||
@@ -639,18 +639,14 @@
|
||||
原生视图内部是浏览器扩展页,插件注入不了按钮、拦不到鼠标,也驱动不了内部滚动——
|
||||
向用户如实说明后按指示回退);② 视图选项改名「设置」,点开直接内嵌「排序方式 + 管理项目
|
||||
展开列表」,不再二次点击;③ 管理项目里给现有项目加「变更视图」:换预设拓扑,
|
||||
## 补记(会话列表按用户面板分组 + 排除后台会话)
|
||||
## 补记(自制下拉列表:文件夹分组标题 + 细分隔线)
|
||||
|
||||
- 用户反馈:会话下拉里出现看不到的会话(如「你是一个只读调查员」等后台/子代理对话、乱码标题);
|
||||
应只显示用户面板里可见的对话,并按面板结构分组(Projects / DeepSeek Harness 两个工作区),
|
||||
带分隔符。
|
||||
- 处理:
|
||||
① 服务端新增只读路由 /api/worktable/workspaces:读宿主 ~/.dsh/storages/workspace.json
|
||||
(工作区 id→title+sessionIds + archived 列表);
|
||||
② 客户端 getSessions 改为异步:拉工作区分组 → 排除 archived 与子代理会话(subagentsByParent),
|
||||
标题取自活快照 byId,当前会话标记;路由不可用时回退平铺(排除子代理);
|
||||
③ CustomPane 会话下拉用 optgroup 按工作区分组(分隔符),默认选中当前会话;
|
||||
④ 清理临时探针导出。
|
||||
- 验证(functional-diag STEP20 扩展):回退态 sessionOptionCount=16、sessionHasCurrent=true;
|
||||
workspaces 路由 404 已加入待重启过滤;全 20 STEP ERROR_COUNT: 0。
|
||||
- 备注:含服务端新路由,**需重启一次 dsh web** 后分组生效;重启前为平铺回退。
|
||||
- 用户反馈:① 工作区文件夹名(Projects/DeepSeek Harness)在原生 optgroup 里看起来像对话项,
|
||||
需要明确区分是分组名;② 分隔符是白色大横线太丑,要很细的一条。
|
||||
- 处理:弃用原生 select/optgroup(无法美化),新写 SelectPop 自制下拉:
|
||||
组标题 = 📁 图标 + 小号暗色大写文字(不可点击、明显是文件夹名);组间 1px 细分隔线;
|
||||
选项可点选、当前会话带「(当前)」角标、选中项蓝色高亮;列表 max-height 内滚动。
|
||||
项目下拉同样换用(顺带根治原生 option 配色问题)。修复过程中两次测试脚本语法损坏已恢复。
|
||||
- 验证(STEP20 重写断言):selectBtnCount=2、listOpen、groupHeaders=2、dividers=2、
|
||||
itemCount=13(子代理/archived 已排除)、currentBadge=1;全 20 STEP ERROR_COUNT: 0。
|
||||
- 备注:纯客户端改动,F5 即生效。
|
||||
|
||||
+15
-12
@@ -651,23 +651,26 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
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 sel=vis('.dsh-wt_selectBtn');
|
||||
out.selectValue=sel?String(sel.textContent).trim():null;
|
||||
out.defaultIsCurrent=false;
|
||||
var send=vis('.dsh-wt_customSend');
|
||||
out.sendDisabled=send?send.disabled:null;
|
||||
// 切到「发送到会话」模式:出现会话下拉,默认当前会话
|
||||
// 切到「发送到会话」模式:出现自制会话下拉,点开看分组与当前标记
|
||||
var modeBtns=[].slice.call(document.querySelectorAll('.dsh-wt_customModeBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');});
|
||||
if(modeBtns[1]){ modeBtns[1].click(); }
|
||||
await new Promise(function(r){setTimeout(r,250)});
|
||||
var ssel=[].slice.call(document.querySelectorAll('.dsh-wt_customSelect')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');})[0];
|
||||
out.sessionSelect=!!ssel;
|
||||
out.sessionOptGroups=ssel?ssel.querySelectorAll('optgroup').length:0;
|
||||
out.sessionOptionCount=ssel?ssel.options.length:0;
|
||||
out.sessionHasCurrent=ssel?String(ssel.textContent).indexOf('\u5f53\u524d')>=0||String(ssel.textContent).indexOf('current')>=0:false;
|
||||
var sbtns=[].slice.call(document.querySelectorAll('.dsh-wt_selectBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');});
|
||||
out.selectBtnCount=sbtns.length;
|
||||
out.sessionSelect=sbtns.length>=2;
|
||||
if(sbtns[0]){ sbtns[0].click(); }
|
||||
await new Promise(function(r){setTimeout(r,250)});
|
||||
var list=[].slice.call(document.querySelectorAll('.dsh-wt_selectList')).find(function(el){return (getComputedStyle(el).visibility!=='hidden');});
|
||||
out.listOpen=!!list;
|
||||
out.groupHeaders=list?list.querySelectorAll('.dsh-wt_selectGroup').length:0;
|
||||
out.dividers=list?list.querySelectorAll('.dsh-wt_selectDivider').length:0;
|
||||
out.itemCount=list?list.querySelectorAll('.dsh-wt_selectItem').length:0;
|
||||
out.currentBadge=list?list.querySelectorAll('.dsh-wt_selectCurrent').length:0;
|
||||
st.close();
|
||||
}catch(err){ out.err=String(err) }
|
||||
return JSON.stringify(out);
|
||||
|
||||
Reference in New Issue
Block a user