dsh-better-workspace 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README_EN.md +1 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +103 -44
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
61
61
|
- **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标);会话分组同样可收起;搜索时自动全部展开。
|
|
62
62
|
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组;会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
|
|
63
63
|
- **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、中英双语跟随界面语言。
|
|
64
|
-
- **URL/斜杠标题不拆层**:宿主自动生成的会话标题落地时若含 `/`(如复述了 URL),整串自动包上 `“”`
|
|
64
|
+
- **URL/斜杠标题不拆层**:宿主自动生成的会话标题落地时若含 `/`(如复述了 URL),整串自动包上 `“”` 原样显示,绝不解析成层级——只对本次启动后**新建的会话**生效,并等命名稳定(约 20 秒,让原生 AI 命名先落地)才处理;存量会话与你手动命名的 `/` 分组标题永不被改动。引号内(含手动添加)的 `/` 永不切分,孤立引号只当普通字符;无引号但含 URL 的旧标题也有平铺兜底。
|
|
65
65
|
- **对话空态页同样生效**:会话空态页的「添加工作区」菜单走的也是本插件的拾取交互,同样带所属分组弹窗。
|
|
66
66
|
|
|
67
67
|
## 安装
|
package/README_EN.md
CHANGED
|
@@ -59,7 +59,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
59
59
|
- **Folder-style collapse**: clicking a workspace row collapses/expands all of its sessions (the row keeps a session counter when collapsed); session groups collapse too; search force-expands everything.
|
|
60
60
|
- **Native drag restored**: drag a workspace row above/below another to reorder (writes back the host registry order) or onto a folder row to move it into that group; drag session rows to reorder within the same workspace or onto a session group to move. Display order = host manual order, drag results are visible immediately. While dragging a workspace, merged single-chain rows (e.g. work/A) temporarily re-expand into folder rows — every level of the path becomes a drop target, and chains merge back when the drag ends.
|
|
61
61
|
- **Native capabilities kept**: open/rename/fork/archive sessions, per-workspace new-session (the + button always expands the workspace first so the new session is immediately visible), current-session highlight, official status dots (blue running ring / amber pending / green done), **status breathing lights** (dots hidden by collapse bubble outward: workspace/folder rows breathe on their icon in the status color — amber pending / green done, custom-glow labels breathe along; running breathes blue only on the session row itself and never relays; off-able in settings), **active-schedule badge** (alarm clock icon for sessions with active Schedule records, matching the official rows), ungrouped-session fallback, zh/en localization.
|
|
62
|
-
- **Slash-bearing titles never nest from URLs**: when a host-generated session title lands containing `/` (e.g. echoing a pasted URL), it is auto-wrapped in `“”` and shown verbatim —
|
|
62
|
+
- **Slash-bearing titles never nest from URLs**: when a host-generated session title lands containing `/` (e.g. echoing a pasted URL), it is auto-wrapped in `“”` and shown verbatim — only for sessions **created after this launch**, and only once the name stays stable for ~20s (letting the native AI naming land first); pre-existing sessions and your manually named `/` groupings are never touched. Quotes (manual ones too) never split on `/`, and a lone quote is just an ordinary character; unquoted URL tails still fall back flat.
|
|
63
63
|
- **Conversation hero too**: the empty-state “Add workspace” menu uses the same picking interaction with the same group popup.
|
|
64
64
|
|
|
65
65
|
## Install
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "侧边栏工作区层级树:名称中的 / 即虚拟分组(web/前端 · web/后端),添加工作区的目录流附所属分组弹窗,重命名即时重排层级,可新建空分组。Hierarchy tree for the DSH sidebar workspace list.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-workspace",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "DSH web plugin: a hierarchical workspace tree for the sidebar. Workspace titles containing \"/\" group into virtual folders (web/前端 · web/后端); the add-workspace directory flow gains a parent-group popup; renames re-derive the tree; a new-folder button creates empty levels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -237,12 +237,15 @@ window.__ModuleLoader__.load({
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* Title → hierarchy segments, QUOTE-AWARE:
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
240
|
+
* Title → hierarchy segments, QUOTE-AWARE: a PAIRED “…” (or "…") span is
|
|
241
|
+
* verbatim — slashes inside quotes never split, the quotes stay part of
|
|
242
|
+
* the leaf. Text outside paired spans splits through splitPlainSegs
|
|
243
|
+
* (URL-tail opaque). A LONE quote character — an opener with no matching
|
|
244
|
+
* closer, or a closer without an opener — is an ORDINARY character and
|
|
245
|
+
* splits normally around it (0.9.1 swallowed the tail after an
|
|
246
|
+
* unterminated opener; the user wants lone quotes as plain text).
|
|
247
|
+
* Grouping is a pure projection, so previously shredded titles re-flow
|
|
248
|
+
* on reload.
|
|
246
249
|
*/
|
|
247
250
|
const splitTitleSegs = (title) => {
|
|
248
251
|
const s = String(title || '')
|
|
@@ -253,11 +256,16 @@ window.__ModuleLoader__.load({
|
|
|
253
256
|
while (i < s.length) {
|
|
254
257
|
const ch = s[i]
|
|
255
258
|
if (ch === '"' || ch === '“') {
|
|
256
|
-
for (const seg of splitPlainSegs(plain)) out.push(seg)
|
|
257
|
-
plain = ''
|
|
258
259
|
const close = ch === '“' ? '”' : '"'
|
|
259
260
|
const j = s.indexOf(close, i + 1)
|
|
260
|
-
|
|
261
|
+
if (j === -1) { // lone opener: ordinary character, keep scanning
|
|
262
|
+
plain += ch
|
|
263
|
+
i += 1
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
for (const seg of splitPlainSegs(plain)) out.push(seg)
|
|
267
|
+
plain = ''
|
|
268
|
+
const end = j + 1
|
|
261
269
|
const quoted = s.slice(i, end).trim()
|
|
262
270
|
if (quoted !== '') out.push(quoted)
|
|
263
271
|
i = end
|
|
@@ -1314,44 +1322,97 @@ window.__ModuleLoader__.load({
|
|
|
1314
1322
|
// source element, and Chromium cancels the whole gesture. dragEnd clears
|
|
1315
1323
|
// the timer so a same-tick cancel never leaves a ghost drag behind.
|
|
1316
1324
|
const wsDragArmTimer = React.useRef(null)
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1325
|
+
// Quote-on-land eligibility sets (mount-scoped):
|
|
1326
|
+
// - blankSeen: ids observed BLANK in any snapshot. A blank row is a
|
|
1327
|
+
// freshly created New Session, so ONLY these may ever receive an
|
|
1328
|
+
// automatic quote. A titled row missing from blankSeen — rows that
|
|
1329
|
+
// stream into the store after mount, fork children (born titled),
|
|
1330
|
+
// rows hidden by load-time filtering — predates this mount or was
|
|
1331
|
+
// named deliberately, and is NEVER touched. (0.9.1 keyed "fresh" off
|
|
1332
|
+
// the first snapshot instead, so late-arriving OLD sessions were
|
|
1333
|
+
// misquoted; blank is the only trustworthy birth mark.)
|
|
1334
|
+
// - humanTouched: ids renamed through THIS component's user actions
|
|
1335
|
+
// (rename dialog, drag into a group, group prefix rewrite). A
|
|
1336
|
+
// user-written "/" is a deliberate grouping; a user-written quote
|
|
1337
|
+
// pair is the verbatim escape. The host pins user titles (a later
|
|
1338
|
+
// automatic name is superseded), so the mark is final.
|
|
1339
|
+
// - autoStable: id → { title, since } for freshly landed automatic
|
|
1340
|
+
// titles waiting out the stabilization window below.
|
|
1341
|
+
const blankSeenRef = React.useRef(null)
|
|
1342
|
+
const humanTouchedRef = React.useRef(null)
|
|
1343
|
+
const autoStableRef = React.useRef(null)
|
|
1344
|
+
const stableTimerRef = React.useRef(null)
|
|
1345
|
+
const [stableTick, setStableTick] = React.useState(0)
|
|
1346
|
+
// The wire cannot tell the deterministic fallback title (first user
|
|
1347
|
+
// message echo, "/"-prone) from the async LLM name that replaces it
|
|
1348
|
+
// seconds later — SessionSummary carries no source field. So a landed
|
|
1349
|
+
// "/"-bearing title is quoted only after it survives 20s unchanged;
|
|
1350
|
+
// any rename resets the window, and the common slash-free LLM name
|
|
1351
|
+
// simply releases the session untouched. Only the "new session
|
|
1352
|
+
// auto-title stayed slashy" case gets wrapped.
|
|
1353
|
+
const TITLE_STABLE_MS = 20000
|
|
1354
|
+
|
|
1355
|
+
// User-driven renames funnel through here: mark first so the
|
|
1356
|
+
// quote-on-land effect never second-guesses a deliberate title.
|
|
1357
|
+
const renameByUser = (sessionId, title) => {
|
|
1358
|
+
if (humanTouchedRef.current === null) humanTouchedRef.current = new Set()
|
|
1359
|
+
humanTouchedRef.current.add(String(sessionId))
|
|
1360
|
+
if (autoStableRef.current) autoStableRef.current.delete(String(sessionId))
|
|
1361
|
+
return renameSession(sessionId, title)
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1331
1364
|
React.useEffect(() => {
|
|
1332
1365
|
if (!list || !list.byId || typeof renameSession !== 'function') return
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (
|
|
1366
|
+
if (blankSeenRef.current === null) blankSeenRef.current = new Set()
|
|
1367
|
+
if (humanTouchedRef.current === null) humanTouchedRef.current = new Set()
|
|
1368
|
+
if (autoStableRef.current === null) autoStableRef.current = new Map()
|
|
1369
|
+
const blankSeen = blankSeenRef.current
|
|
1370
|
+
const touched = humanTouchedRef.current
|
|
1371
|
+
const stable = autoStableRef.current
|
|
1372
|
+
const now = Date.now()
|
|
1336
1373
|
const fixes = []
|
|
1374
|
+
let deadline = Infinity
|
|
1337
1375
|
for (const id of Object.keys(list.byId)) {
|
|
1338
1376
|
const summary = list.byId[id]
|
|
1339
|
-
if (!summary
|
|
1340
|
-
|
|
1341
|
-
if (!
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1377
|
+
if (!summary) continue
|
|
1378
|
+
if (summary.blank) { blankSeen.add(id); stable.delete(id); continue }
|
|
1379
|
+
if (!blankSeen.has(id) || touched.has(id)) { stable.delete(id); continue }
|
|
1380
|
+
const text = String(summary.title || '')
|
|
1381
|
+
if (text.trim() === '' || text.includes('“') || text.includes('"') || !text.includes('/')) {
|
|
1382
|
+
stable.delete(id) // quoted already, or the LLM name arrived slash-free: done
|
|
1383
|
+
continue
|
|
1384
|
+
}
|
|
1385
|
+
const prev = stable.get(id)
|
|
1386
|
+
if (prev && prev.title === text) {
|
|
1387
|
+
if (now - prev.since >= TITLE_STABLE_MS) {
|
|
1388
|
+
stable.delete(id)
|
|
1389
|
+
fixes.push([id, '“' + text + '”'])
|
|
1390
|
+
} else {
|
|
1391
|
+
deadline = Math.min(deadline, prev.since + TITLE_STABLE_MS)
|
|
1392
|
+
}
|
|
1393
|
+
continue
|
|
1394
|
+
}
|
|
1395
|
+
stable.set(id, { title: text, since: now }) // new landing or fallback→LLM rename: reset
|
|
1396
|
+
deadline = Math.min(deadline, now + TITLE_STABLE_MS)
|
|
1397
|
+
}
|
|
1398
|
+
for (const id of Array.from(stable.keys())) {
|
|
1399
|
+
if (!list.byId[id]) stable.delete(id) // session vanished (archived/deleted): drop the wait
|
|
1400
|
+
}
|
|
1401
|
+
if (stableTimerRef.current !== null) { clearTimeout(stableTimerRef.current); stableTimerRef.current = null }
|
|
1402
|
+
if (deadline !== Infinity) {
|
|
1403
|
+
stableTimerRef.current = setTimeout(() => {
|
|
1404
|
+
stableTimerRef.current = null
|
|
1405
|
+
setStableTick(t => t + 1) // re-evaluate through the effect, never rename from a stale closure
|
|
1406
|
+
}, Math.max(1, deadline - now))
|
|
1349
1407
|
}
|
|
1350
1408
|
if (fixes.length === 0) return
|
|
1351
1409
|
Promise.resolve()
|
|
1352
1410
|
.then(async () => { for (const [id, next] of fixes) await renameSession(id, next) })
|
|
1353
|
-
.catch(() => { /*
|
|
1354
|
-
}, [list])
|
|
1411
|
+
.catch(() => { /* display-layer fallback keeps the row flat; a later snapshot re-evaluates */ })
|
|
1412
|
+
}, [list, stableTick])
|
|
1413
|
+
React.useEffect(() => () => {
|
|
1414
|
+
if (stableTimerRef.current !== null) clearTimeout(stableTimerRef.current)
|
|
1415
|
+
}, [])
|
|
1355
1416
|
const normalizedQuery = query.trim().toLowerCase()
|
|
1356
1417
|
const now = Date.now()
|
|
1357
1418
|
|
|
@@ -1730,7 +1791,7 @@ window.__ModuleLoader__.load({
|
|
|
1730
1791
|
const newTitle = groupPath !== '' ? groupPath + '/' + source.leaf : source.leaf
|
|
1731
1792
|
if (newTitle === source.title) return
|
|
1732
1793
|
Promise.resolve()
|
|
1733
|
-
.then(() =>
|
|
1794
|
+
.then(() => renameByUser(source.sessionId, newTitle))
|
|
1734
1795
|
.catch(fail)
|
|
1735
1796
|
}
|
|
1736
1797
|
|
|
@@ -1756,7 +1817,7 @@ window.__ModuleLoader__.load({
|
|
|
1756
1817
|
const title = String(nextTitle || '').trim()
|
|
1757
1818
|
if (title === '' || title === session.title) { setDialog(null); return }
|
|
1758
1819
|
Promise.resolve()
|
|
1759
|
-
.then(() =>
|
|
1820
|
+
.then(() => renameByUser(session.id, title))
|
|
1760
1821
|
.then(() => setDialog(null))
|
|
1761
1822
|
.catch(fail)
|
|
1762
1823
|
}
|
|
@@ -1778,7 +1839,7 @@ window.__ModuleLoader__.load({
|
|
|
1778
1839
|
for (const row of affected) {
|
|
1779
1840
|
if (row.blank) continue
|
|
1780
1841
|
const nextTitle = nextPath + row.title.slice(target.path.length)
|
|
1781
|
-
await
|
|
1842
|
+
await renameByUser(row.id, nextTitle)
|
|
1782
1843
|
}
|
|
1783
1844
|
})
|
|
1784
1845
|
.then(() => setDialog(null))
|
|
@@ -2233,7 +2294,6 @@ window.__ModuleLoader__.load({
|
|
|
2233
2294
|
const sessions = ctx.get('sessions')
|
|
2234
2295
|
const workspaces = ctx.get('workspaces')
|
|
2235
2296
|
const uiWorkspace = ctx.get('uiWorkspace')
|
|
2236
|
-
const connection = ctx.get('connection')
|
|
2237
2297
|
if (!sessions || !workspaces || !uiWorkspace) {
|
|
2238
2298
|
console.error('[dsh-better-workspace] required services missing', {
|
|
2239
2299
|
sessions: !!sessions, workspaces: !!workspaces, uiWorkspace: !!uiWorkspace,
|
|
@@ -2293,7 +2353,6 @@ window.__ModuleLoader__.load({
|
|
|
2293
2353
|
pickDirectory: () => uiWorkspace.pickDirectory(),
|
|
2294
2354
|
hooks: {
|
|
2295
2355
|
directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow'),
|
|
2296
|
-
connectionGeneration: connection ? connection.generation : undefined,
|
|
2297
2356
|
},
|
|
2298
2357
|
})
|
|
2299
2358
|
const flowInjected = (hole) => () => ({
|
|
@@ -2365,7 +2424,7 @@ window.__ModuleLoader__.load({
|
|
|
2365
2424
|
|
|
2366
2425
|
return {
|
|
2367
2426
|
name: 'dsh-better-workspace',
|
|
2368
|
-
inject: ['slots', 'sessions', 'workspaces', 'locale', '
|
|
2427
|
+
inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace'],
|
|
2369
2428
|
apply,
|
|
2370
2429
|
}
|
|
2371
2430
|
},
|