dsh-better-workspace 0.39.1 → 0.40.0
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 +3 -3
- package/README_EN.md +3 -3
- package/dsh.plugin.json +1 -1
- package/package.json +6 -3
- package/src/client.js +214 -137
- package/src/client.locales.js +105 -0
- package/src/index.js +8 -2
- package/src/title-source.js +65 -0
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
<table>
|
|
56
56
|
<tr>
|
|
57
57
|
<td align="center" width="58%"><img src="docs/screenshots/5-context-menu.png" alt="右键菜单"/></td>
|
|
58
|
-
<td valign="top"><b>右键菜单</b><br/>操作都收进右键:工作区行——重命名 /
|
|
58
|
+
<td valign="top"><b>右键菜单</b><br/>操作都收进右键:工作区行——重命名 / 在文件夹显示 / 删除工作区 / <b>筛选会话</b>(归档显示四态,v0.40.0 起) / 自定义外观 / <b>修改工作区默认预设</b>(v0.32.0 起,改写该工作区常驻新会话所用的 agent 预设,走官方同一通道;内置预设显示官方本地化名——标准模式 / PTC 模式 / 极简模式 / 创造模式,v0.33.0 起;工作区暂无待命新会话时,确认后会预备一个带所选预设的新会话作为该工作区默认);工作区分组行——重命名分组(同步更新组内所有工作区名称)/ 自定义外观;会话与会话分组行——重命名 / 分叉 / 归档 / 自定义外观;会话行另有<b>复制 Session ID</b>(v0.30.0 起,对齐 dsh 0.2.1-alpha.5 官方会话菜单第五项——写剪贴板走宿主原语,复制成功与否均有提示)。<br/><b>v0.38.0 起接入官方统一右键菜单</b>:会话/工作区行由宿主共享菜单渲染,官方条目与插件补充项(移动到分组 / 移出分组 / 自定义外观 / 修改默认预设,排在分隔线之后)同席,官方新增条目会自动出现;分组行仍用插件菜单。<br/><b>v0.40.0 起工作区右键新增「筛选会话」子菜单</b>:四个显示状态——跟随全局(默认)/ 隐藏已归档 / 全部对话(显示已归档)/ 仅显示已归档;选择只改该工作区,不写全局筛选,其他工作区照旧。</td>
|
|
59
59
|
</tr>
|
|
60
60
|
</table>
|
|
61
61
|
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行中带则移动进该分组(改写名称前缀);**工作区分组行同样可拖拽(v0.36.0 起,三带悬停)**——拖到分组行时行分三个落区:上下 **30% 边带**=与该组**排序**(拖完立即重排、刷新后保留),**中带**=整组移入(组成员逐个改写名称前缀,与「重命名分组」同一语义;拖到自身/自己的子孙组上会被拒绝);**组行与工作区行同处一条混合序列(v0.36.5 起)**:一个层级里的行顺序只有一份(组行与裸工作区行交错),工作区可以拖到分组行的边带插到该组前面/后面,分组也可以拖到工作区行前面/后面——不再有「组永远排在裸工作区之前」的限制;拖到**另一层**的边带是**放置**(v0.36.6):工作区离开原组(或进入目标层)并插到落点,「插到第一个」画哪落哪(跨磁盘层不放行——磁盘层是文件系统事实);**拖入黑框统一(v0.36.7)**:工作区行拖到分组行中带 = 改名前缀进组(黑框反馈);组行拖到**它所属的磁盘层工作区行 / 直接父级分组行**中带 = 整组「放入」该容器、排到该层末尾(标题与磁盘位置都不动,同一套黑框视觉)——排序走边带、放入走中带,两类操作视觉上彻底分开;**容器防环(v0.36.9)**:磁盘层的容器工作区不能被拖进它自己的名称组或子工作区——那会把容器改名成自己的子孙(如「工作/用户目录」甚至「<id>/用户目录」),现在整行拒绝、不留任何落点反馈,排序与放入父容器(v0.36.7)不受影响;**平级组嵌套与提升(v0.36.10)**:组行拖到平级组行的**中带**=变成它的**子组**(成员标题落到新组之下,`工作/平行帮` → `工作/Fis管理后台/平行帮`,天然不撞名——旧的并入语义会在双方都有同名子工作区时报 name-conflict、整树不动);组行拖到**另一层工作区行的边带**(同磁盘、不在自己子树内)=**整组提升**到那行所在层并锚定在落点旁(「把平行帮拖到和用户目录、工作平级」);已打开的排序槽不再锁死判定——指针推回行**中带**即可翻成放入黑框,嵌套与排序随时互切;**静止松手兜底**:Chromium 把指针静止约一秒后的松手吞成 dragend 时,按最后悬停的落点补提交(Esc 取消不受影响),不再「松手没反应」;**撑开让位(v0.36.4)**:进入排序边带立刻把目标行撑开一行(工作区行与分组行同一套视觉),判定按**行未撑开时的原始位置**算——指针停在槽空出的新区里保持当前方向不闪烁;槽已打开的行只按原始中线二选一(上半=插在前,下半=插在后,**不会翻成「移入」**),三带里的中带 INTO 只在首次进入时决定,随时可改主意;会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序(分组行 = 父容器下的持久化组序),拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层不放行(边带不接管光标、不画落点线),同磁盘层内按落点锚定。拖拽期间树形保持不变(v0.36.4):单链合并行不再临时展开、归组成功也不自动展开目标组——指针下的行不会突然换人,展开状态只在点击时改变。
|
|
73
73
|
- **不丢原生能力**:会话行(打开/重命名/分叉/归档;**双击标题**重命名,v0.34.0 起;**右键是唯一菜单入口**——行内「⋯」已退役,悬停只留快捷钮,工作区行同样只留新会话钮,v0.35.1 起)、**官方行样式逐项对齐**(v0.34.0 起:恒定 16px 状态槽——运行转圈不再挤动标题、悬停 800ms 弹官方 HoverCard(标题/路径/时间/状态,可复制)、超长标题悬停匀速滚动、活动链工作区文件夹图标着蓝、拖放指示用官方斜纹图案)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标;v0.23.0 起直接渲染官方 `sidebar.session.row.leading` 座位的占用者,与官方行同源)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
|
|
74
74
|
- **新增项自动配色 + 图标(v0.25.0,默认关闭)**:设置卡里一个开关,打开后**新创建的会话**自动分到一个内置色、**新工作区**再额外分到一个图标,懒得逐个设置的人直接开箱即用。颜色来自**定死的 13 个槽位**,每个槽位配**两色**——浅色主题用深色系、深色主题用亮色系——按「洗牌袋 + 近期回避窗口」抽取:一轮之内绝不重复,跨轮也不会立刻撞上刚出过的 3 个,连续新建几项肉眼可辨。**可读性是真达标**:每个色在**对应主题**背景下的对比度都 **≥4.5**,即 WCAG **正文级 AA**(对比度是脚本真算的,测试逐色断言;浅色主题按比纯白更暗的悬停面、深色主题按最亮的面这种保守取值)。行里存的是**槽位 id(`auto:3`)而不是色值**,所以**切换深浅主题时颜色自动换成同色系的另一套**,既不用重新设置,也不会失配。**只影响打开开关之后新建的项**:你手动设置过外观的行(存的是 hex)永不被覆盖,关闭开关也不回收已分配的样式,同一项不会被二次随机。图标从既有图标候选集里取、按宿主实际导出过滤;**图标只给工作区**——会话行的那个位置是官方状态灯与日程角标,没有图标位。
|
|
75
|
-
- **官方新语义与快捷键都接上(v0.23.0,对齐 dsh 0.1.7-rc.2)**:① 归档筛选是官方新版三态直选(**隐藏已归档** / **全部对话(显示已归档)** / **仅显示已归档**);选「仅显示已归档」时**没有归档会话的工作区整行隐藏**,空列表显示「暂无已归档会话」+「查看其他会话」一键回退;② 官方快捷键 **⌥⌘K 搜索 / ⌥⌘O 添加工作区 / ⌥⌘N 新会话**(桌面端为 ⌘K / ⌘O / ⌘N)在本插件的自绘侧栏里**真的生效**——搜索键打开并聚焦本插件的搜索框、添加键走同一个官方目录流(原生宿主弹系统选择器,browse 宿主直接弹出官方应用内目录浏览器),按钮文案还会带上宿主当前的键位;③
|
|
76
|
-
-
|
|
75
|
+
- **官方新语义与快捷键都接上(v0.23.0,对齐 dsh 0.1.7-rc.2)**:① 归档筛选是官方新版三态直选(**隐藏已归档** / **全部对话(显示已归档)** / **仅显示已归档**);选「仅显示已归档」时**没有归档会话的工作区整行隐藏**,空列表显示「暂无已归档会话」+「查看其他会话」一键回退;② 官方快捷键 **⌥⌘K 搜索 / ⌥⌘O 添加工作区 / ⌥⌘N 新会话**(桌面端为 ⌘K / ⌘O / ⌘N)在本插件的自绘侧栏里**真的生效**——搜索键打开并聚焦本插件的搜索框、添加键走同一个官方目录流(原生宿主弹系统选择器,browse 宿主直接弹出官方应用内目录浏览器),按钮文案还会带上宿主当前的键位;③ 首次使用的工作区显示为本地化的「默认工作区」,不再露出磁盘目录名。④ **v0.40.0 起工作区右键「筛选会话」可单独覆盖这三态**(跟随全局 / 隐藏已归档 / 全部对话 / 仅显示已归档):默认跟随全局,覆盖只影响该工作区,全局筛选和其他工作区都不动;「仅显示已归档」下没有归档会话的工作区若自己选了该态,仍保留空行,方便从同一个右键菜单改回。
|
|
76
|
+
- **自动标题永不拆层,用户分组才拆(v0.39.2,issue #19)**:宿主为每个标题记录了来源(`session/title` 的 `source.kind`)。本插件把来源经摘要投影 `betterWorkspace/titleSource` 带到侧栏:**`fallback` / `provider` 的自动标题整条平铺,标题里有多少 `/` 都不建组**(不再自动包 `“”`,也不等 20 秒改名);只有你**显式重命名、拖入分组或改写分组前缀**(来源变为 `user`)之后,`/` 才作为层级。这样新会话一生成就是一行,不会因为回显首条消息或路径被拆成伪目录。来源缺失时(旧冷缓存、投影未注册)仍保留显示层兜底:引用绝对路径或 `~` / `./` / `../` 路径的标题整条平铺;成对引号保护其中的 `/`;没有斜杠的引号段只是普通文字。旧会话不会丢,也不需要迁移数据。会话分组仍默认开启,不需要的话可在视图菜单关掉。
|
|
77
77
|
- **新会话页的工作区选择器 = 侧栏那棵树(v0.22.0)**:会话空态页「工作区」按钮的弹层原本是官方的一层平铺标题列表(连磁盘目录嵌套都没有);本插件接管该座位后,弹层绘制与侧栏**同一棵**树——磁盘目录嵌套、「/」名称分组三态、单链合并全部读同一个视图 store,所以侧栏的视图选项一改,**这边立刻跟着变**;当前工作区带勾选标记,分组行可开合(选择器内本地记忆,默认全展开),标题按侧栏的层级叶子显示。底部「添加工作区」仍由**官方目录拾取占用者**承载(原生选择器 / 官方应用内浏览器),不是仿制品。
|
|
78
78
|
|
|
79
79
|
<table>
|
package/README_EN.md
CHANGED
|
@@ -55,7 +55,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
55
55
|
<table>
|
|
56
56
|
<tr>
|
|
57
57
|
<td align="center" width="58%"><img src="docs/screenshots/5-context-menu.png" alt="Context menu"/></td>
|
|
58
|
-
<td valign="top"><b>Context menu everywhere</b><br/>Actions live in the right-click menu: workspace rows — rename /
|
|
58
|
+
<td valign="top"><b>Context menu everywhere</b><br/>Actions live in the right-click menu: workspace rows — rename / show in folder / delete workspace / <b>filter sessions</b> (archived visibility, since v0.40.0) / customize; folder rows — rename group (updates all descendant workspaces) / customize; session & session-group rows — rename / fork / archive / customize; session rows also get <b>Copy Session ID</b> (v0.30.0, mirroring dsh 0.2.1-alpha.5's fifth official menu entry — the clipboard write rides the host primitive, with a notice either way).<br/><b>Since v0.38.0 the shared host context menu owns session/workspace rows</b>: official entries and this plugin's extras (move to group / move out / customize / default preset, after a separator) share one popup, and future official entries appear automatically; group rows keep the plugin menu.<br/><b>Since v0.40.0 workspace rows add a “Filter sessions” submenu</b>: Follow global (default) / Hide archived / All conversations (show archived) / Archived only. The pick changes only that workspace, never the global filter, and every other workspace stays as it is.</td>
|
|
59
59
|
</tr>
|
|
60
60
|
</table>
|
|
61
61
|
|
|
@@ -72,8 +72,8 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
72
72
|
- **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 (rewrites the title prefix); drag session rows to reorder inside their group, onto a row of ANOTHER group to move into that group (onto a root-level row to leave the group), onto a session-group row to move into it, or onto their OWN workspace row / anywhere outside a group (empty space included) to leave it — the workspace row highlights and the tree shows a faint dashed hint while such a drag is in flight; a grouped session row also offers "Move out of group" in its context menu. Display order = host manual order, drag results are visible immediately. **The disk layer is a filesystem fact and never changes through dragging**: a cross-level drop only re-groups the name prefix (the workspace lands at the end of its own disk level), same-level drops take the anchor order. 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.
|
|
73
73
|
- **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; since v0.23.0 it renders the occupant of the shipped `sidebar.session.row.leading` seat, so it is the same mark the official row would show), ungrouped-session fallback, UI copy in 21 languages following the DSH language preference (Simplified and Traditional Chinese included).
|
|
74
74
|
- **Auto color + icon for new items (v0.25.0, off by default)**: one switch in the plugin settings card. Turn it on and every newly created session gets a built-in color while every new workspace additionally gets an icon — for anyone who would rather not style rows one by one. Colors come from **13 fixed slots**, each shipping **two colors** — a darker one for the light theme, a lighter one for the dark theme — drawn with a shuffle bag plus a recent-draw window: nothing repeats inside a cycle, a refill never starts with the last three colors, and items added in a row look clearly different. **The readability claim is real**: every entry keeps contrast **≥ 4.5** on its own theme's background, i.e. WCAG **normal-text AA** (computed by script and asserted per color in the test suite, measured against conservative surfaces: a hovered row darker than pure white in the light theme, the brightest surface in the dark theme). A row stores the **slot id (`auto:3`), not a hex**, so **switching between light and dark repaints it with the matching sibling color** — nothing to reconfigure and no mismatch. **Only items created after you turn it on are touched**: rows you styled by hand (stored as hex) are never overwritten, turning it off does not undo assignments, and an item is never randomized twice. Icons come from the existing icon candidate set, filtered by what the host actually exports — and **workspaces only**: the session row's cell belongs to the official status dot and schedule mark, so it has no icon slot.
|
|
75
|
-
- **Official semantics and shortcuts wired up (v0.23.0, aligned with dsh 0.1.7-rc.2)**: ① the archived filter is the shipped three-way pick (**Hide archived** / **All conversations (show archived)** / **Archived only**); under "Archived only" a workspace without archived sessions disappears entirely, and an empty list offers "No archived sessions yet" + **View other sessions** as the way back. ② The shipped shortcuts **⌥⌘K search / ⌥⌘O add workspace / ⌥⌘N new session** (⌘K / ⌘O / ⌘N on desktop) really work in this plugin's own sidebar — the search key opens and focuses this sidebar's filter field, the add key runs the very same official directory flow (native chooser on a native host, the in-app pointer on a browse host), and the buttons show the host's current keybindings. ③ A first-use workspace reads as the localized "Default workspace" instead of the on-disk directory name.
|
|
76
|
-
- **
|
|
75
|
+
- **Official semantics and shortcuts wired up (v0.23.0, aligned with dsh 0.1.7-rc.2)**: ① the archived filter is the shipped three-way pick (**Hide archived** / **All conversations (show archived)** / **Archived only**); under "Archived only" a workspace without archived sessions disappears entirely, and an empty list offers "No archived sessions yet" + **View other sessions** as the way back. ② The shipped shortcuts **⌥⌘K search / ⌥⌘O add workspace / ⌥⌘N new session** (⌘K / ⌘O / ⌘N on desktop) really work in this plugin's own sidebar — the search key opens and focuses this sidebar's filter field, the add key runs the very same official directory flow (native chooser on a native host, the in-app pointer on a browse host), and the buttons show the host's current keybindings. ③ A first-use workspace reads as the localized "Default workspace" instead of the on-disk directory name. ④ **Since v0.40.0 a workspace's right-click “Filter sessions” submenu can override those three states per workspace** (Follow global / Hide archived / All conversations / Archived only): the default follows the global filter, the override touches only that workspace (neither the global pick nor other workspaces move), and a workspace that chose “Archived only” while holding no archived sessions keeps its empty row so the same menu can switch it back.
|
|
76
|
+
- **Automatic titles never nest; only your explicit grouping does (v0.39.2, issue #19)**: the host records where each title came from (`source.kind` on `session/title`). This plugin carries that provenance to the sidebar through the `betterWorkspace/titleSource` summary projection: a **`fallback` / `provider` auto-title is always ONE row, no matter how many `/` it contains** (no synthetic `“”` wrap and no 20-second rename). A `/` becomes a hierarchy only after you **rename the session, drag it into a group, or rewrite its group prefix** — the source then becomes `user`. A fresh session therefore stays a single row from birth, even when its echoed first message or a path used to shred it into fake folders. When the projection is missing (old cold cache, registration unavailable) the display keeps its text fallback: titles quoting an absolute path or a `~` / `./` / `../` path stay flat; paired quotes protect a `/` inside them; a quoted span with no slash is ordinary text. No session data is migrated or lost. Session grouping stays on by default; turn it off in the view menu if you don't want it.
|
|
77
77
|
- **The New Session workspace picker IS the sidebar tree (v0.22.0)**: the empty-state "Workspace" chip used to open the shipped one-level list of flat titles (no disk nesting at all). This plugin shadows that seat (lowest priority renders) and draws the **same** tree the sidebar draws — disk nesting, the tri-state "/" name layer, chain compression — all from the one view store, so changing a view option on the sidebar moves this popover immediately. The current workspace carries a check, folder rows fold (picker-local, fully expanded by default), and the pinned "Add workspace" action still renders the **shipped directory-picking occupant** (native chooser / official in-app browser) instead of a copy.
|
|
78
78
|
|
|
79
79
|
## Install
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping; the New Session workspace picker shows the same tree.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树(同一份视图选项即时同步);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default), and the same tree in the New Session workspace picker; add-workspace rides the official directory flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"src/client.js",
|
|
43
43
|
"src/client.locales.js",
|
|
44
44
|
"src/client.styles.js",
|
|
45
|
+
"src/title-source.js",
|
|
45
46
|
"cordis.patch.yml",
|
|
46
47
|
"dsh.plugin.json",
|
|
47
48
|
"README.md",
|
|
@@ -66,7 +67,8 @@
|
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
68
69
|
"@deepseek-ai/dsh": ">=0.2.2-rc.1",
|
|
69
|
-
"@deepseek-ai/schemastery": "^3.19.0"
|
|
70
|
+
"@deepseek-ai/schemastery": "^3.19.0",
|
|
71
|
+
"zod": "^4.4.3"
|
|
70
72
|
},
|
|
71
73
|
"peerDependenciesMeta": {
|
|
72
74
|
"@deepseek-ai/dsh": {
|
|
@@ -79,6 +81,7 @@
|
|
|
79
81
|
},
|
|
80
82
|
"icon": "./icon.svg",
|
|
81
83
|
"devDependencies": {
|
|
82
|
-
"@deepseek-ai/schemastery": "^3.19.0"
|
|
84
|
+
"@deepseek-ai/schemastery": "^3.19.0",
|
|
85
|
+
"zod": "^4.4.3"
|
|
83
86
|
}
|
|
84
87
|
}
|
package/src/client.js
CHANGED
|
@@ -47,14 +47,28 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'))
|
|
48
48
|
return i === -1 ? s : s.slice(i + 1)
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
// Host-recorded title provenance (src/title-source.js). A fresh session's
|
|
52
|
+
// fallback/provider title never becomes a hierarchy; only a user rename or
|
|
53
|
+
// an explicit grouping action may form groups. Undefined means the host
|
|
54
|
+
// projection is not there (old cold cache), so the display falls back to
|
|
55
|
+
// the text heuristics below.
|
|
56
|
+
const TITLE_SOURCE_KEY = 'betterWorkspace/titleSource'
|
|
57
|
+
const titleSourceOf = (summary) => {
|
|
58
|
+
const values = summary && summary.projectionValues
|
|
59
|
+
return values && typeof values === 'object' ? values[TITLE_SOURCE_KEY] : undefined
|
|
60
|
+
}
|
|
61
|
+
const groupsAllowedFor = (summary) => {
|
|
62
|
+
const source = titleSourceOf(summary)
|
|
63
|
+
return source === undefined || source === 'user'
|
|
64
|
+
}
|
|
50
65
|
/**
|
|
51
66
|
* Plain-text splitting with the URL tail kept opaque: a bare "/" split
|
|
52
67
|
* shreds URLs ("https:" → empty → host → path...), so from the first
|
|
53
68
|
* "://" onward the tail is ONE leaf; text before the marker splits
|
|
54
69
|
* normally at the last "/" before it. This is the FALLBACK guard —
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* primary mechanism.
|
|
70
|
+
* whether a session title is allowed to form groups at all is decided by
|
|
71
|
+
* its host-recorded provenance (see TITLE_SOURCE_KEY), not by the text.
|
|
58
72
|
*/
|
|
59
73
|
const splitPlainSegs = (text) => {
|
|
60
74
|
const s = String(text || '')
|
|
@@ -79,12 +93,18 @@ window.__ModuleLoader__.load({
|
|
|
79
93
|
const titleQuotesPath = (text) => {
|
|
80
94
|
// A path inside a PAIRED quote span is already an opaque leaf (the
|
|
81
95
|
// quote parser below keeps it verbatim), so only the unquoted remainder
|
|
82
|
-
// can be mistaken for a name hierarchy.
|
|
96
|
+
// can be mistaken for a name hierarchy. "~/.dsh/..." and "./..." are
|
|
97
|
+
// paths even when they carry only one slash; drive-letter paths are
|
|
98
|
+
// paths by construction. A bare "https://" is not matched (it does not
|
|
99
|
+
// start with a slash).
|
|
83
100
|
const s = String(text || '').replace(/"[^"]*"|“[^”]*”/g, ' ')
|
|
84
|
-
const re = /(?:^|\s)(?:[A-Za-z]:)?\/\S
|
|
101
|
+
const re = /(?:^|\s)(?:(?:[A-Za-z]:)?\/\S+|(?:~|\.{1,2})\/\S+)/g
|
|
85
102
|
let m
|
|
86
103
|
while ((m = re.exec(s)) !== null) {
|
|
87
|
-
|
|
104
|
+
const token = m[0].trim()
|
|
105
|
+
if (/^[A-Za-z]:[\\/]/.test(token)) return true
|
|
106
|
+
if (/^(?:~|\.{1,2})\//.test(token)) return true
|
|
107
|
+
if ((token.match(/\//g) || []).length >= 2) return true
|
|
88
108
|
}
|
|
89
109
|
return false
|
|
90
110
|
}
|
|
@@ -100,8 +120,9 @@ window.__ModuleLoader__.load({
|
|
|
100
120
|
* Grouping is a pure projection, so previously shredded titles re-flow
|
|
101
121
|
* on reload.
|
|
102
122
|
*/
|
|
103
|
-
const splitTitleSegs = (title) => {
|
|
123
|
+
const splitTitleSegs = (title, allowGroups = true) => {
|
|
104
124
|
const s = String(title || '')
|
|
125
|
+
if (allowGroups === false) return s.trim() === '' ? [] : [s.trim()]
|
|
105
126
|
if (titleQuotesPath(s)) return [s.trim()]
|
|
106
127
|
if (!/["“]/.test(s)) return splitPlainSegs(s)
|
|
107
128
|
const out = []
|
|
@@ -117,11 +138,19 @@ window.__ModuleLoader__.load({
|
|
|
117
138
|
i += 1
|
|
118
139
|
continue
|
|
119
140
|
}
|
|
120
|
-
for (const seg of splitPlainSegs(plain)) out.push(seg)
|
|
121
|
-
plain = ''
|
|
122
141
|
const end = j + 1
|
|
123
142
|
const quoted = s.slice(i, end).trim()
|
|
124
|
-
|
|
143
|
+
// Quotes protect a slash INSIDE a leaf; they do not manufacture a
|
|
144
|
+
// group out of ordinary punctuation. "受阻" in a sentence stays
|
|
145
|
+
// part of the surrounding leaf (issue #19), so the pending plain
|
|
146
|
+
// text is only flushed when the quoted span itself carries a slash.
|
|
147
|
+
if (quoted !== '' && quoted.includes('/')) {
|
|
148
|
+
for (const seg of splitPlainSegs(plain)) out.push(seg)
|
|
149
|
+
plain = ''
|
|
150
|
+
out.push(quoted)
|
|
151
|
+
} else {
|
|
152
|
+
plain += quoted
|
|
153
|
+
}
|
|
125
154
|
i = end
|
|
126
155
|
continue
|
|
127
156
|
}
|
|
@@ -645,7 +674,7 @@ window.__ModuleLoader__.load({
|
|
|
645
674
|
}
|
|
646
675
|
for (const segs of explicitSegsOf(explicitSess, groupScope)) ensure(segs)
|
|
647
676
|
for (const row of rows || []) {
|
|
648
|
-
const segs = splitTitleSegs(row.title)
|
|
677
|
+
const segs = splitTitleSegs(row.title, row.allowGroups !== false)
|
|
649
678
|
const folderPath = segs.slice(0, -1).join('/')
|
|
650
679
|
const leaf = segs.length > 0 ? segs[segs.length - 1] : row.title
|
|
651
680
|
ensure(segs.slice(0, -1)).sessions.push({ ...row, leaf })
|
|
@@ -1080,7 +1109,7 @@ window.__ModuleLoader__.load({
|
|
|
1080
1109
|
// LITERAL, not a spread of DEFAULT_APPEARANCE: init() is eval-harness
|
|
1081
1110
|
// friendly (no cross-section reference), and the smoke suite asserts the
|
|
1082
1111
|
// two stay in lockstep field by field.
|
|
1083
|
-
init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true, sessionLimit: 0, appearance: { color: '', glow: 0, weight: 0, shadow: false, stroke: false, strokeWidth: 1, strokeColor: '#808080' } }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', workspaceGroupMode: 'disk', sessionTitleSlash: true, workspaceTitleSlash: false, ungroupedOpen: true, folders: { ws: {}, sess: {} }, folderOrder: {} }),
|
|
1112
|
+
init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true, sessionLimit: 0, appearance: { color: '', glow: 0, weight: 0, shadow: false, stroke: false, strokeWidth: 1, strokeColor: '#808080' } }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', workspaceArchivedFilters: {}, workspaceGroupMode: 'disk', sessionTitleSlash: true, workspaceTitleSlash: false, ungroupedOpen: true, folders: { ws: {}, sess: {} }, folderOrder: {} }),
|
|
1084
1113
|
// NOTE: hydration REPLACES the state with the persisted whole value —
|
|
1085
1114
|
// init defaults never merge. Every action must tolerate a missing key
|
|
1086
1115
|
// (states persisted by older plugin versions lack sessionGroups), and
|
|
@@ -1108,6 +1137,15 @@ window.__ModuleLoader__.load({
|
|
|
1108
1137
|
setGroupBy: (d, value) => { d.groupBy = value },
|
|
1109
1138
|
setOrderBy: (d, value) => { d.orderBy = value },
|
|
1110
1139
|
setArchivedFilter: (d, value) => { d.archivedFilter = value === 'show' || value === 'only' ? value : 'default' },
|
|
1140
|
+
// Per-workspace override of the archived visibility filter (v0.40.0).
|
|
1141
|
+
// Absent means "follow the global archivedFilter"; a stored value is
|
|
1142
|
+
// one of the same three literals, so sessionsOf needs no translation
|
|
1143
|
+
// layer. null deletes the override and restores the follow state.
|
|
1144
|
+
setWorkspaceArchivedFilter: (d, workspaceId, value) => {
|
|
1145
|
+
if (!d.workspaceArchivedFilters || typeof d.workspaceArchivedFilters !== 'object') d.workspaceArchivedFilters = {}
|
|
1146
|
+
if (value === 'default' || value === 'show' || value === 'only') d.workspaceArchivedFilters[workspaceId] = value
|
|
1147
|
+
else delete d.workspaceArchivedFilters[workspaceId]
|
|
1148
|
+
},
|
|
1111
1149
|
setSessionTitleSlash: (d, value) => { d.sessionTitleSlash = value !== false },
|
|
1112
1150
|
setWorkspaceTitleSlash: (d, value) => { d.workspaceTitleSlash = value !== false },
|
|
1113
1151
|
// v0.21.0 tri-state grouping (issue #7). The legacy boolean rides
|
|
@@ -3695,6 +3733,19 @@ window.__ModuleLoader__.load({
|
|
|
3695
3733
|
// Archived-row visibility (v0.20.0, official ArchivedFilter mirror);
|
|
3696
3734
|
// persisted snapshots from older plugin versions lack the key.
|
|
3697
3735
|
const archivedFilter = useStore ? (useStore(s => s.archivedFilter) || 'default') : 'default'
|
|
3736
|
+
// Per-workspace archived-visibility overrides (v0.40.0): hydrated
|
|
3737
|
+
// snapshots predate the key, so every read takes the empty-object
|
|
3738
|
+
// fallback and an unknown value means "follow the global filter".
|
|
3739
|
+
const workspaceArchivedFilters = useStore ? (useStore(s => s.workspaceArchivedFilters) || {}) : {}
|
|
3740
|
+
const workspaceArchivedFilterOf = (workspaceId) => {
|
|
3741
|
+
if (!workspaceArchivedFilters || typeof workspaceArchivedFilters !== 'object') return null
|
|
3742
|
+
const value = workspaceArchivedFilters[workspaceId]
|
|
3743
|
+
return value === 'default' || value === 'show' || value === 'only' ? value : null
|
|
3744
|
+
}
|
|
3745
|
+
const effectiveArchivedFilterOf = (workspace) => {
|
|
3746
|
+
const override = workspace ? workspaceArchivedFilterOf(workspace.workspaceId) : null
|
|
3747
|
+
return override || archivedFilter
|
|
3748
|
+
}
|
|
3698
3749
|
const archivedSet = React.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
|
|
3699
3750
|
const pinnedSet = React.useMemo(() => new Set(pinnedSessionIds), [pinnedSessionIds])
|
|
3700
3751
|
const subCounts = React.useMemo(
|
|
@@ -4009,42 +4060,13 @@ window.__ModuleLoader__.load({
|
|
|
4009
4060
|
// across a group row showed the thin insert line and never the empty
|
|
4010
4061
|
// slot — the user report "还是一条蓝线,没拨开让我插入". The edge bands
|
|
4011
4062
|
// are 30% wide and geometrically judged, so they can answer at once.
|
|
4012
|
-
//
|
|
4013
|
-
//
|
|
4014
|
-
//
|
|
4015
|
-
//
|
|
4016
|
-
//
|
|
4017
|
-
//
|
|
4018
|
-
// named deliberately, and is NEVER touched. (0.9.1 keyed "fresh" off
|
|
4019
|
-
// the first snapshot instead, so late-arriving OLD sessions were
|
|
4020
|
-
// misquoted; blank is the only trustworthy birth mark.)
|
|
4021
|
-
// - humanTouched: ids renamed through THIS component's user actions
|
|
4022
|
-
// (rename dialog, drag into a group, group prefix rewrite). A
|
|
4023
|
-
// user-written "/" is a deliberate grouping; a user-written quote
|
|
4024
|
-
// pair is the verbatim escape. The host pins user titles (a later
|
|
4025
|
-
// automatic name is superseded), so the mark is final.
|
|
4026
|
-
// - autoStable: id → { title, since } for freshly landed automatic
|
|
4027
|
-
// titles waiting out the stabilization window below.
|
|
4028
|
-
const blankSeenRef = React.useRef(null)
|
|
4029
|
-
const humanTouchedRef = React.useRef(null)
|
|
4030
|
-
const autoStableRef = React.useRef(null)
|
|
4031
|
-
const stableTimerRef = React.useRef(null)
|
|
4032
|
-
const [stableTick, setStableTick] = React.useState(0)
|
|
4033
|
-
// The wire cannot tell the deterministic fallback title (first user
|
|
4034
|
-
// message echo, "/"-prone) from the async LLM name that replaces it
|
|
4035
|
-
// seconds later — SessionSummary carries no source field. So a landed
|
|
4036
|
-
// "/"-bearing title is quoted only after it survives 20s unchanged;
|
|
4037
|
-
// any rename resets the window, and the common slash-free LLM name
|
|
4038
|
-
// simply releases the session untouched. Only the "new session
|
|
4039
|
-
// auto-title stayed slashy" case gets wrapped.
|
|
4040
|
-
const TITLE_STABLE_MS = 20000
|
|
4041
|
-
|
|
4042
|
-
// User-driven renames funnel through here: mark first so the
|
|
4043
|
-
// quote-on-land effect never second-guesses a deliberate title.
|
|
4063
|
+
// Title provenance is host-recorded: the summary projection above paints
|
|
4064
|
+
// source.kind ('fallback' | 'provider' | 'user') onto every list row, so
|
|
4065
|
+
// a user rename needs no local bookkeeping — the next snapshot marks it
|
|
4066
|
+
// 'user' and only then does a "/" title form groups. No synthetic user
|
|
4067
|
+
// title and no 20s stabilization window: rewriting the host title would
|
|
4068
|
+
// pin an automatic name before its LLM replacement lands.
|
|
4044
4069
|
const renameByUser = (sessionId, title) => {
|
|
4045
|
-
if (humanTouchedRef.current === null) humanTouchedRef.current = new Set()
|
|
4046
|
-
humanTouchedRef.current.add(String(sessionId))
|
|
4047
|
-
if (autoStableRef.current) autoStableRef.current.delete(String(sessionId))
|
|
4048
4070
|
// A manual rename is authoritative: forget any grafted display so
|
|
4049
4071
|
// the fresh wire title stands verbatim (no prefix re-applied).
|
|
4050
4072
|
if (titleCacheRef && typeof titleCacheRef.forgetDisplay === 'function') titleCacheRef.forgetDisplay(String(sessionId))
|
|
@@ -4096,66 +4118,6 @@ window.__ModuleLoader__.load({
|
|
|
4096
4118
|
return () => clearTimeout(timer)
|
|
4097
4119
|
}, [copyNotice ? copyNotice.seq : null])
|
|
4098
4120
|
|
|
4099
|
-
React.useEffect(() => {
|
|
4100
|
-
if (!list || !list.byId || typeof renameSession !== 'function') return
|
|
4101
|
-
if (blankSeenRef.current === null) blankSeenRef.current = new Set()
|
|
4102
|
-
if (humanTouchedRef.current === null) humanTouchedRef.current = new Set()
|
|
4103
|
-
if (autoStableRef.current === null) autoStableRef.current = new Map()
|
|
4104
|
-
const blankSeen = blankSeenRef.current
|
|
4105
|
-
const touched = humanTouchedRef.current
|
|
4106
|
-
const stable = autoStableRef.current
|
|
4107
|
-
const now = Date.now()
|
|
4108
|
-
const fixes = []
|
|
4109
|
-
let deadline = Infinity
|
|
4110
|
-
for (const id of Object.keys(list.byId)) {
|
|
4111
|
-
const summary = list.byId[id]
|
|
4112
|
-
if (!summary) continue
|
|
4113
|
-
if (summary.blank) { blankSeen.add(id); stable.delete(id); continue }
|
|
4114
|
-
if (!blankSeen.has(id) || touched.has(id)) { stable.delete(id); continue }
|
|
4115
|
-
const text = String(summary.title || '')
|
|
4116
|
-
if (text.trim() === '' || text.includes('“') || text.includes('"') || !text.includes('/')) {
|
|
4117
|
-
stable.delete(id) // quoted already, or the LLM name arrived slash-free: done
|
|
4118
|
-
continue
|
|
4119
|
-
}
|
|
4120
|
-
// v0.37.3: a path-shaped title is already ONE row at render time
|
|
4121
|
-
// (titleQuotesPath). Renaming it here would append a USER title and
|
|
4122
|
-
// supersede an asynchronous LLM name that has not landed yet, so the
|
|
4123
|
-
// fallback would stay pinned as a raw path. Leave it to the host.
|
|
4124
|
-
if (titleQuotesPath(text)) {
|
|
4125
|
-
stable.delete(id)
|
|
4126
|
-
continue
|
|
4127
|
-
}
|
|
4128
|
-
const prev = stable.get(id)
|
|
4129
|
-
if (prev && prev.title === text) {
|
|
4130
|
-
if (now - prev.since >= TITLE_STABLE_MS) {
|
|
4131
|
-
stable.delete(id)
|
|
4132
|
-
fixes.push([id, '“' + text + '”'])
|
|
4133
|
-
} else {
|
|
4134
|
-
deadline = Math.min(deadline, prev.since + TITLE_STABLE_MS)
|
|
4135
|
-
}
|
|
4136
|
-
continue
|
|
4137
|
-
}
|
|
4138
|
-
stable.set(id, { title: text, since: now }) // new landing or fallback→LLM rename: reset
|
|
4139
|
-
deadline = Math.min(deadline, now + TITLE_STABLE_MS)
|
|
4140
|
-
}
|
|
4141
|
-
for (const id of Array.from(stable.keys())) {
|
|
4142
|
-
if (!list.byId[id]) stable.delete(id) // session vanished (archived/deleted): drop the wait
|
|
4143
|
-
}
|
|
4144
|
-
if (stableTimerRef.current !== null) { clearTimeout(stableTimerRef.current); stableTimerRef.current = null }
|
|
4145
|
-
if (deadline !== Infinity) {
|
|
4146
|
-
stableTimerRef.current = setTimeout(() => {
|
|
4147
|
-
stableTimerRef.current = null
|
|
4148
|
-
setStableTick(t => t + 1) // re-evaluate through the effect, never rename from a stale closure
|
|
4149
|
-
}, Math.max(1, deadline - now))
|
|
4150
|
-
}
|
|
4151
|
-
if (fixes.length === 0) return
|
|
4152
|
-
Promise.resolve()
|
|
4153
|
-
.then(async () => { for (const [id, next] of fixes) await renameSession(id, next) })
|
|
4154
|
-
.catch(() => { /* display-layer fallback keeps the row flat; a later snapshot re-evaluates */ })
|
|
4155
|
-
}, [list, stableTick])
|
|
4156
|
-
React.useEffect(() => () => {
|
|
4157
|
-
if (stableTimerRef.current !== null) clearTimeout(stableTimerRef.current)
|
|
4158
|
-
}, [])
|
|
4159
4121
|
const normalizedQuery = query.trim().toLowerCase()
|
|
4160
4122
|
const now = Date.now()
|
|
4161
4123
|
|
|
@@ -4224,6 +4186,7 @@ window.__ModuleLoader__.load({
|
|
|
4224
4186
|
pending: visiblePendingKind(st && st.pendingInteraction && st.pendingInteraction.kind),
|
|
4225
4187
|
pinned: pinnedSet.has(id) && !archivedSet.has(id),
|
|
4226
4188
|
archived: archivedSet.has(id),
|
|
4189
|
+
allowGroups: groupsAllowedFor(summary),
|
|
4227
4190
|
})
|
|
4228
4191
|
}
|
|
4229
4192
|
ungrouped.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
@@ -4284,9 +4247,10 @@ window.__ModuleLoader__.load({
|
|
|
4284
4247
|
|
|
4285
4248
|
const sessionsOf = (workspace) => {
|
|
4286
4249
|
const rows = []
|
|
4250
|
+
const filter = effectiveArchivedFilterOf(workspace)
|
|
4287
4251
|
for (const id of workspace.sessionIds || []) {
|
|
4288
4252
|
const summary = list && list.byId ? list.byId[id] : undefined
|
|
4289
|
-
if (!sessionVisible(summary, currentId, archivedSet,
|
|
4253
|
+
if (!sessionVisible(summary, currentId, archivedSet, filter)) continue
|
|
4290
4254
|
const st = statusOf(id)
|
|
4291
4255
|
rows.push({
|
|
4292
4256
|
id,
|
|
@@ -4300,6 +4264,7 @@ window.__ModuleLoader__.load({
|
|
|
4300
4264
|
pending: visiblePendingKind(st && st.pendingInteraction && st.pendingInteraction.kind),
|
|
4301
4265
|
pinned: pinnedSet.has(id) && !archivedSet.has(id),
|
|
4302
4266
|
archived: archivedSet.has(id),
|
|
4267
|
+
allowGroups: groupsAllowedFor(summary),
|
|
4303
4268
|
})
|
|
4304
4269
|
}
|
|
4305
4270
|
// Browser-local reorder fallback: used only when the injected
|
|
@@ -4576,16 +4541,27 @@ window.__ModuleLoader__.load({
|
|
|
4576
4541
|
* by these groups, so a drop that only reorders the flat list would leave
|
|
4577
4542
|
* the row exactly where it was.
|
|
4578
4543
|
*/
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
return
|
|
4544
|
+
// A path-shaped leaf stays ONE opaque row (titleQuotesPath), so an
|
|
4545
|
+
// unwrapped prefix would never become a visible group. Quote it and the
|
|
4546
|
+
// prefix splits while the path stays verbatim inside the quotes. A leaf
|
|
4547
|
+
// from an auto-titled session is opaque for the same reason: its "\/" is
|
|
4548
|
+
// prose, not a group, and the drag itself is the explicit grouping act.
|
|
4549
|
+
const leafForGroup = (leaf, opaque) => {
|
|
4550
|
+
const s = String(leaf || '')
|
|
4551
|
+
if (!s.includes('/')) return s
|
|
4552
|
+
if (/^“[^”]*”$/.test(s) || /^"[^"]*"$/.test(s)) return s
|
|
4553
|
+
return (opaque || titleQuotesPath(s)) ? '“' + s + '”' : s
|
|
4554
|
+
}
|
|
4555
|
+
const titleMovedToGroup = (title, anchorTitle, opts = {}) => {
|
|
4556
|
+
const sourceOpaque = opts.sourceOpaque === true
|
|
4557
|
+
const anchorOpaque = opts.anchorOpaque === true
|
|
4558
|
+
const leaf = sourceOpaque ? String(title || '') : leafOf(title)
|
|
4559
|
+
const group = anchorOpaque ? '' : groupPrefixOf(anchorTitle)
|
|
4560
|
+
// Returning to the root is still a manual rename in the host, which
|
|
4561
|
+
// turns the title's source into 'user'. Quote an opaque leaf so its
|
|
4562
|
+
// prose "/" cannot start forming groups after that rename.
|
|
4563
|
+
if (!group) return leafForGroup(leaf, sourceOpaque)
|
|
4564
|
+
return group + '/' + leafForGroup(leaf, sourceOpaque)
|
|
4589
4565
|
}
|
|
4590
4566
|
/**
|
|
4591
4567
|
* Every existing group path, deduplicated (datalist source). `kind`
|
|
@@ -4607,7 +4583,12 @@ window.__ModuleLoader__.load({
|
|
|
4607
4583
|
}
|
|
4608
4584
|
for (const workspace of items || []) {
|
|
4609
4585
|
if (kind !== 'session') addPrefixes(workspace.title)
|
|
4610
|
-
if (kind !== 'workspace') for (const session of sessionsOf(workspace))
|
|
4586
|
+
if (kind !== 'workspace') for (const session of sessionsOf(workspace)) {
|
|
4587
|
+
// An auto-titled session's "/" is prose: it forms no group and must
|
|
4588
|
+
// not be suggested as one (issue #19).
|
|
4589
|
+
if (session.allowGroups === false) continue
|
|
4590
|
+
addPrefixes(session.title)
|
|
4591
|
+
}
|
|
4611
4592
|
}
|
|
4612
4593
|
return [...paths].sort()
|
|
4613
4594
|
}
|
|
@@ -4731,7 +4712,7 @@ window.__ModuleLoader__.load({
|
|
|
4731
4712
|
const id = text.slice(5)
|
|
4732
4713
|
for (const workspace of items || []) {
|
|
4733
4714
|
const row = sessionsOf(workspace).find((s) => s.id === id)
|
|
4734
|
-
if (row) return { kind: 'session', id: row.id, title: row.title, workspaceId: workspace.workspaceId }
|
|
4715
|
+
if (row) return { kind: 'session', id: row.id, title: row.title, workspaceId: workspace.workspaceId, allowGroups: row.allowGroups !== false }
|
|
4735
4716
|
}
|
|
4736
4717
|
return null
|
|
4737
4718
|
}
|
|
@@ -4747,13 +4728,15 @@ window.__ModuleLoader__.load({
|
|
|
4747
4728
|
* (folder / sgroup) rewrite the prefix of every member, exactly like
|
|
4748
4729
|
* the existing rename-group dialogs; single kinds just retitle the
|
|
4749
4730
|
* entry. All paths are user-driven renames, so sessions go through
|
|
4750
|
-
* renameByUser
|
|
4731
|
+
* renameByUser, so the next snapshot records source "user" and the new
|
|
4732
|
+
* prefix becomes a real group.
|
|
4751
4733
|
*/
|
|
4752
4734
|
const submitGroupMove = (target, rawGroup) => {
|
|
4753
4735
|
if (!target) { setDialog(null); return }
|
|
4754
4736
|
const group = normPath(String(rawGroup || ''))
|
|
4755
|
-
const leaf = leafOf(target.title)
|
|
4756
|
-
const
|
|
4737
|
+
const leaf = target.allowGroups === false ? String(target.title || '') : leafOf(target.title)
|
|
4738
|
+
const safe = target.kind === 'session' ? leafForGroup(leaf, target.allowGroups === false) : leaf
|
|
4739
|
+
const next = group ? group + '/' + safe : safe
|
|
4757
4740
|
if (next === '' || next === target.title) { setDialog(null); return }
|
|
4758
4741
|
if (target.kind === 'workspace') {
|
|
4759
4742
|
Promise.resolve()
|
|
@@ -4988,7 +4971,7 @@ window.__ModuleLoader__.load({
|
|
|
4988
4971
|
// then shows its plain "no drop" cursor, which tells the truth).
|
|
4989
4972
|
if (dragMatches('session')) {
|
|
4990
4973
|
if (!sessionSlash || drag.source.workspaceId !== workspace.workspaceId) return
|
|
4991
|
-
if (leafOf(drag.source.title) === drag.source.title) return
|
|
4974
|
+
if (drag.source.allowGroups === false || leafOf(drag.source.title) === drag.source.title) return
|
|
4992
4975
|
event.preventDefault()
|
|
4993
4976
|
event.stopPropagation()
|
|
4994
4977
|
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
@@ -5081,7 +5064,7 @@ window.__ModuleLoader__.load({
|
|
|
5081
5064
|
onDrop: (event) => {
|
|
5082
5065
|
if (dragMatches('session')) {
|
|
5083
5066
|
if (!sessionSlash || drag.source.workspaceId !== workspace.workspaceId) return
|
|
5084
|
-
if (leafOf(drag.source.title) === drag.source.title) return
|
|
5067
|
+
if (drag.source.allowGroups === false || leafOf(drag.source.title) === drag.source.title) return
|
|
5085
5068
|
event.preventDefault()
|
|
5086
5069
|
event.stopPropagation()
|
|
5087
5070
|
commitSessionMoveOut(workspace.workspaceId)
|
|
@@ -5791,7 +5774,7 @@ window.__ModuleLoader__.load({
|
|
|
5791
5774
|
event.dataTransfer.effectAllowed = 'move'
|
|
5792
5775
|
event.dataTransfer.setData('text/plain', session.id)
|
|
5793
5776
|
} catch { }
|
|
5794
|
-
setDrag({ kind: 'session', source: { sessionId: session.id, workspaceId, title: session.title, leaf: session.leaf || session.title }, over: null })
|
|
5777
|
+
setDrag({ kind: 'session', source: { sessionId: session.id, workspaceId, title: session.title, leaf: session.leaf || session.title, allowGroups: session.allowGroups !== false }, over: null })
|
|
5795
5778
|
},
|
|
5796
5779
|
onDragEnd: () => setDrag(null),
|
|
5797
5780
|
onDragOver: (event) => {
|
|
@@ -5849,7 +5832,7 @@ window.__ModuleLoader__.load({
|
|
|
5849
5832
|
// pure reorder; with the name layer off every group is '' — which is
|
|
5850
5833
|
// the same thing.
|
|
5851
5834
|
const target = flat.find(s => s.id === targetSessionId)
|
|
5852
|
-
const nextTitle = sessionSlash && target ? titleMovedToGroup(source.title, target.title) : null
|
|
5835
|
+
const nextTitle = sessionSlash && target ? titleMovedToGroup(source.title, target.title, { sourceOpaque: source.allowGroups === false, anchorOpaque: target.allowGroups === false }) : null
|
|
5853
5836
|
// Official pinned-block rule: a pinned row reorders only among pinned
|
|
5854
5837
|
// rows. A cross-block drop that would be a pure reorder is ignored;
|
|
5855
5838
|
// group moves keep working (the pin fronts the row in its new group).
|
|
@@ -5882,7 +5865,9 @@ window.__ModuleLoader__.load({
|
|
|
5882
5865
|
const source = drag.source
|
|
5883
5866
|
setDrag(null)
|
|
5884
5867
|
if (!source || source.workspaceId !== workspaceId) return
|
|
5885
|
-
|
|
5868
|
+
// An auto-titled row is already at the root: its "/" never formed a
|
|
5869
|
+
// group, so there is nothing to leave.
|
|
5870
|
+
const leaf = source.allowGroups === false ? source.title : leafOf(source.title)
|
|
5886
5871
|
if (leaf === source.title) return
|
|
5887
5872
|
Promise.resolve()
|
|
5888
5873
|
.then(() => renameByUser(source.sessionId, leaf))
|
|
@@ -5904,7 +5889,8 @@ window.__ModuleLoader__.load({
|
|
|
5904
5889
|
const commitSessionMoveInto = (workspaceId, groupPath) => {
|
|
5905
5890
|
const source = drag.source
|
|
5906
5891
|
setDrag(null)
|
|
5907
|
-
const
|
|
5892
|
+
const leaf = leafForGroup(source.leaf, source.allowGroups === false)
|
|
5893
|
+
const newTitle = groupPath !== '' ? groupPath + '/' + leaf : leaf
|
|
5908
5894
|
if (newTitle === source.title) return
|
|
5909
5895
|
Promise.resolve()
|
|
5910
5896
|
.then(() => renameByUser(source.sessionId, newTitle))
|
|
@@ -6128,8 +6114,17 @@ window.__ModuleLoader__.load({
|
|
|
6128
6114
|
* with, so this can never disagree with the session list below the row.
|
|
6129
6115
|
*/
|
|
6130
6116
|
const archivedOnly = archivedFilter === 'only'
|
|
6131
|
-
const workspaceVisible = (workspace) =>
|
|
6132
|
-
|
|
6117
|
+
const workspaceVisible = (workspace) => {
|
|
6118
|
+
if (!workspace) return false
|
|
6119
|
+
if (effectiveArchivedFilterOf(workspace) !== 'only') return true
|
|
6120
|
+
if (Array.isArray(workspace.sessionIds) && sessionsOf(workspace).length > 0) return true
|
|
6121
|
+
// Keep an EMPTY workspace that carries its own "only archived"
|
|
6122
|
+
// override: its row is the sole surface holding the menu that can
|
|
6123
|
+
// switch it back to "follow global". Hiding it would trap the user
|
|
6124
|
+
// in an empty view with no visible way out (the global empty-state
|
|
6125
|
+
// action edits the global filter, not this workspace's override).
|
|
6126
|
+
return workspaceArchivedFilterOf(workspace.workspaceId) === 'only'
|
|
6127
|
+
}
|
|
6133
6128
|
const countVisibleAt = (level) => {
|
|
6134
6129
|
let total = 0
|
|
6135
6130
|
for (const folder of level.folders) {
|
|
@@ -6392,7 +6387,7 @@ window.__ModuleLoader__.load({
|
|
|
6392
6387
|
const groupItem = { id: 'move-group', label: t('menu.moveToGroup'), icon: icon('IconFolderOpenOutline16', 16) }
|
|
6393
6388
|
const ungroupItem = { id: 'move-out', label: t('menu.moveOutGroup'), icon: icon('IconRightUpOutline16', 16) }
|
|
6394
6389
|
const sgroupItems = sessionSlash ? [groupItem] : []
|
|
6395
|
-
const sessionUngroupItems = sessionSlash && payload && groupPrefixOf(String(payload.title || '')) !== '' ? [ungroupItem] : []
|
|
6390
|
+
const sessionUngroupItems = sessionSlash && payload && payload.allowGroups !== false && groupPrefixOf(String(payload.title || '')) !== '' ? [ungroupItem] : []
|
|
6396
6391
|
const official = (key, fallback) => {
|
|
6397
6392
|
const value = typeof ot === 'function' ? ot(key) : null
|
|
6398
6393
|
return typeof value === 'string' && value !== '' && value !== key ? value : fallback
|
|
@@ -6435,6 +6430,10 @@ window.__ModuleLoader__.load({
|
|
|
6435
6430
|
// One group-LEAVING implementation for the local menu and the official
|
|
6436
6431
|
// contribution: retitle to the leaf and let the user-touch wrapper pin it.
|
|
6437
6432
|
const moveSessionOut = (payload) => {
|
|
6433
|
+
const summary = list && list.byId ? list.byId[payload.id] : undefined
|
|
6434
|
+
// Only a user-authored title has groups to leave; an automatic title's
|
|
6435
|
+
// "/" is prose (issue #19).
|
|
6436
|
+
if (payload.allowGroups === false || (summary && groupsAllowedFor(summary) === false)) return
|
|
6438
6437
|
const title = String(payload.title || '')
|
|
6439
6438
|
const leaf = leafOf(title)
|
|
6440
6439
|
if (leaf !== title) Promise.resolve().then(() => renameByUser(payload.id, leaf)).catch(fail)
|
|
@@ -6520,6 +6519,39 @@ window.__ModuleLoader__.load({
|
|
|
6520
6519
|
current: projection && typeof projection.agentPreset === 'string' ? projection.agentPreset : undefined,
|
|
6521
6520
|
})
|
|
6522
6521
|
}
|
|
6522
|
+
// Per-workspace archived visibility (v0.40.0). The four entries share
|
|
6523
|
+
// one id set across the primitives submenu, the flat no-Menu fallback
|
|
6524
|
+
// and the official shared menu, so dispatch is identical everywhere.
|
|
6525
|
+
// A leading check marks the stored value; `follow` is the absent key.
|
|
6526
|
+
const archiveFilterOptions = (workspaceId) => {
|
|
6527
|
+
const selected = workspaceArchivedFilterOf(workspaceId)
|
|
6528
|
+
const option = (value, id, label) => ({
|
|
6529
|
+
value,
|
|
6530
|
+
id,
|
|
6531
|
+
label: (selected === value ? '✓ ' : '') + label,
|
|
6532
|
+
})
|
|
6533
|
+
return [
|
|
6534
|
+
option(null, 'bw-archive-follow', t('menu.archiveFollow')),
|
|
6535
|
+
option('default', 'bw-archive-hide', t('menu.archiveHide')),
|
|
6536
|
+
option('show', 'bw-archive-show', t('menu.archiveShow')),
|
|
6537
|
+
option('only', 'bw-archive-only', t('menu.archiveOnly')),
|
|
6538
|
+
]
|
|
6539
|
+
}
|
|
6540
|
+
const archiveFilterEntries = (workspaceId) => {
|
|
6541
|
+
const options = archiveFilterOptions(workspaceId)
|
|
6542
|
+
// The plugin's no-primitives fallback menu is a flat button list, so
|
|
6543
|
+
// a submenu would be unreachable there. Flatten into the parent menu
|
|
6544
|
+
// instead; ids stay the same, dispatch stays shared.
|
|
6545
|
+
if (typeof ui.Menu !== 'function') {
|
|
6546
|
+
return options.map((option) => ({ id: option.id, label: option.label }))
|
|
6547
|
+
}
|
|
6548
|
+
return [{
|
|
6549
|
+
id: 'bw-archive-filter',
|
|
6550
|
+
label: t('menu.archiveFilter'),
|
|
6551
|
+
icon: icon('IconArchiveOutlineRegular', 16),
|
|
6552
|
+
submenu: options.map((option) => ({ id: option.id, label: option.label })),
|
|
6553
|
+
}]
|
|
6554
|
+
}
|
|
6523
6555
|
const menuEntries = () => {
|
|
6524
6556
|
if (ctx === null) return []
|
|
6525
6557
|
const customizeEntry = { id: 'customize', label: t('custom.title'), icon: icon('IconPersonalizationOutline16', 16) }
|
|
@@ -6553,6 +6585,7 @@ window.__ModuleLoader__.load({
|
|
|
6553
6585
|
{ id: 'delete', label: ot ? ot('delete.workspace') : t('menu.delete'), icon: icon('IconTrashOutline16', 16), danger: true },
|
|
6554
6586
|
{ type: 'separator', id: 'bw-sep' },
|
|
6555
6587
|
...wsGroupItems,
|
|
6588
|
+
...archiveFilterEntries(ctx.payload.workspaceId),
|
|
6556
6589
|
customizeEntry,
|
|
6557
6590
|
// v0.32.0: the workspace default preset. Rides the official
|
|
6558
6591
|
// agent-preset roster + blank-composition channels (the hero
|
|
@@ -6587,14 +6620,25 @@ window.__ModuleLoader__.load({
|
|
|
6587
6620
|
}
|
|
6588
6621
|
const { kind, payload } = current
|
|
6589
6622
|
setCtx(null)
|
|
6623
|
+
const archiveValue = id === 'bw-archive-follow' ? null
|
|
6624
|
+
: id === 'bw-archive-hide' ? 'default'
|
|
6625
|
+
: id === 'bw-archive-show' ? 'show'
|
|
6626
|
+
: id === 'bw-archive-only' ? 'only'
|
|
6627
|
+
: undefined
|
|
6628
|
+
if (kind === 'workspace' && archiveValue !== undefined) {
|
|
6629
|
+
if (actions && typeof actions.setWorkspaceArchivedFilter === 'function') {
|
|
6630
|
+
actions.setWorkspaceArchivedFilter(payload.workspaceId, archiveValue)
|
|
6631
|
+
}
|
|
6632
|
+
return
|
|
6633
|
+
}
|
|
6590
6634
|
if (id === 'move-group') {
|
|
6591
6635
|
// Lock the clicked row as the dialog target; the group name starts
|
|
6592
6636
|
// at its CURRENT prefix so "retype to move elsewhere" is one edit.
|
|
6593
6637
|
const target = kind === 'folder' ? { kind: 'folder', path: payload.path, title: payload.path }
|
|
6594
6638
|
: kind === 'workspace' ? { kind: 'workspace', id: payload.workspaceId, title: payload.title || payload.leaf }
|
|
6595
|
-
: kind === 'session' ? { kind: 'session', id: payload.id, title: payload.title }
|
|
6639
|
+
: kind === 'session' ? { kind: 'session', id: payload.id, title: payload.title, allowGroups: payload.allowGroups !== false }
|
|
6596
6640
|
: { kind: 'sgroup', path: payload.path, title: payload.path, workspaceId: payload.workspaceId }
|
|
6597
|
-
setDialog({ kind: 'group-move', target, initial: groupPrefixOf(target.title), groupPaths: collectGroupPaths(target.kind) })
|
|
6641
|
+
setDialog({ kind: 'group-move', target, initial: target.allowGroups === false ? '' : groupPrefixOf(target.title), groupPaths: collectGroupPaths(target.kind) })
|
|
6598
6642
|
}
|
|
6599
6643
|
else if (kind === 'folder' && id === 'rename-folder') setDialog({ kind: 'folder-rename', path: payload.path })
|
|
6600
6644
|
else if (kind === 'folder' && id === 'delete-folder') setDialog({ kind: 'folder-delete', path: payload.path, scope: payload.scope, name: payload.name })
|
|
@@ -6618,7 +6662,19 @@ window.__ModuleLoader__.load({
|
|
|
6618
6662
|
slashes: { session: sessionSlash, workspace: workspaceSlash },
|
|
6619
6663
|
hasPreset: () => !!presetFace,
|
|
6620
6664
|
groupPrefix: (title) => groupPrefixOf(title),
|
|
6665
|
+
// The official session target carries only displayTitle, so provenance
|
|
6666
|
+
// is looked up by id here — the same list snapshot the rows render from.
|
|
6667
|
+
allowGroups: (sessionId) => {
|
|
6668
|
+
const summary = list && list.byId ? list.byId[sessionId] : undefined
|
|
6669
|
+
return !summary || groupsAllowedFor(summary)
|
|
6670
|
+
},
|
|
6621
6671
|
label: (key) => t(key),
|
|
6672
|
+
archiveOptions: (workspaceId) => archiveFilterOptions(workspaceId),
|
|
6673
|
+
setArchiveFilter: (workspaceId, value) => {
|
|
6674
|
+
if (actions && typeof actions.setWorkspaceArchivedFilter === 'function') {
|
|
6675
|
+
actions.setWorkspaceArchivedFilter(workspaceId, value)
|
|
6676
|
+
}
|
|
6677
|
+
},
|
|
6622
6678
|
customize: (kind, payload) => {
|
|
6623
6679
|
const name = kind === 'workspace' ? (payload.title || payload.leaf) : payload.title
|
|
6624
6680
|
setCustomize({ kind, entryKey: keyOf(kind, payload), name })
|
|
@@ -6628,10 +6684,13 @@ window.__ModuleLoader__.load({
|
|
|
6628
6684
|
const target = kind === 'workspace'
|
|
6629
6685
|
? { kind: 'workspace', id: payload.workspaceId, title }
|
|
6630
6686
|
: { kind: 'session', id: payload.id, title }
|
|
6687
|
+
const summary = kind === 'session' && list && list.byId ? list.byId[payload.id] : undefined
|
|
6688
|
+
const allowGroups = !summary || groupsAllowedFor(summary)
|
|
6689
|
+
if (kind === 'session') target.allowGroups = allowGroups
|
|
6631
6690
|
setDialog({
|
|
6632
6691
|
kind: 'group-move',
|
|
6633
6692
|
target,
|
|
6634
|
-
initial: groupPrefixOf(target.title),
|
|
6693
|
+
initial: allowGroups ? groupPrefixOf(target.title) : '',
|
|
6635
6694
|
groupPaths: collectGroupPaths(kind),
|
|
6636
6695
|
})
|
|
6637
6696
|
},
|
|
@@ -6899,7 +6958,7 @@ window.__ModuleLoader__.load({
|
|
|
6899
6958
|
// gap), so a drop outside every group always means the same thing:
|
|
6900
6959
|
// leave the group. Row handlers stopPropagation, so this only fires
|
|
6901
6960
|
// where no row claimed the drop.
|
|
6902
|
-
const sessionCouldLeaveGroup = dragMatches('session') && sessionSlash && drag.source && leafOf(drag.source.title) !== drag.source.title
|
|
6961
|
+
const sessionCouldLeaveGroup = dragMatches('session') && sessionSlash && drag.source && drag.source.allowGroups !== false && leafOf(drag.source.title) !== drag.source.title
|
|
6903
6962
|
const rootDropEvents = {
|
|
6904
6963
|
onDragOver: (event) => {
|
|
6905
6964
|
if (!sessionCouldLeaveGroup) return
|
|
@@ -7273,7 +7332,8 @@ window.__ModuleLoader__.load({
|
|
|
7273
7332
|
run: () => bridge.moveGroup('session', { id: target.sessionId, title }),
|
|
7274
7333
|
})
|
|
7275
7334
|
}
|
|
7276
|
-
|
|
7335
|
+
const allowGroups = !bridge.allowGroups || bridge.allowGroups(target.sessionId)
|
|
7336
|
+
if (allowGroups && bridge.groupPrefix && bridge.groupPrefix(title) !== '') {
|
|
7277
7337
|
actions.push({
|
|
7278
7338
|
id: 'bw-move-out',
|
|
7279
7339
|
label: bridge.label('menu.moveOutGroup'),
|
|
@@ -7305,6 +7365,23 @@ window.__ModuleLoader__.load({
|
|
|
7305
7365
|
run: () => bridge.moveGroup('workspace', payload),
|
|
7306
7366
|
})
|
|
7307
7367
|
}
|
|
7368
|
+
// A submenu, not four top-level rows: the official shared menu
|
|
7369
|
+
// only sees this plugin's contribution, so the workspace verb
|
|
7370
|
+
// block stays short while the four archive modes are one level
|
|
7371
|
+
// deeper. The check mark lives in the label because a shared
|
|
7372
|
+
// contextMenus action has no selectedIds channel.
|
|
7373
|
+
const archive = bridge.archiveOptions && bridge.archiveOptions(target.workspaceId)
|
|
7374
|
+
if (archive && archive.length > 0) {
|
|
7375
|
+
actions.push({
|
|
7376
|
+
id: 'bw-archive-filter',
|
|
7377
|
+
label: bridge.label('menu.archiveFilter'),
|
|
7378
|
+
children: archive.map((option) => ({
|
|
7379
|
+
id: option.id,
|
|
7380
|
+
label: option.label,
|
|
7381
|
+
run: () => bridge.setArchiveFilter(target.workspaceId, option.value),
|
|
7382
|
+
})),
|
|
7383
|
+
})
|
|
7384
|
+
}
|
|
7308
7385
|
actions.push({
|
|
7309
7386
|
id: 'bw-customize',
|
|
7310
7387
|
label: bridge.label('custom.title'),
|
package/src/client.locales.js
CHANGED
|
@@ -64,6 +64,11 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
'menu.moveOutGroup': '移出分组',
|
|
65
65
|
'menu.sessionId': '复制 Session ID',
|
|
66
66
|
'menu.workspacePreset': '修改工作区默认预设…',
|
|
67
|
+
'menu.archiveFilter': '筛选会话',
|
|
68
|
+
'menu.archiveFollow': '跟随全局',
|
|
69
|
+
'menu.archiveHide': '隐藏已归档',
|
|
70
|
+
'menu.archiveShow': '全部对话(显示已归档)',
|
|
71
|
+
'menu.archiveOnly': '仅显示已归档',
|
|
67
72
|
'wsPreset.title': '修改工作区默认预设',
|
|
68
73
|
'wsPreset.current': '当前默认预设',
|
|
69
74
|
'wsPreset.currentUnknown': '当前预设未知',
|
|
@@ -227,6 +232,11 @@ window.__ModuleLoader__.load({
|
|
|
227
232
|
'menu.moveOutGroup': 'Move out of group',
|
|
228
233
|
'menu.sessionId': 'Copy Session ID',
|
|
229
234
|
'menu.workspacePreset': 'Workspace default preset…',
|
|
235
|
+
'menu.archiveFilter': 'Filter sessions',
|
|
236
|
+
'menu.archiveFollow': 'Follow global',
|
|
237
|
+
'menu.archiveHide': 'Hide archived',
|
|
238
|
+
'menu.archiveShow': 'All conversations (show archived)',
|
|
239
|
+
'menu.archiveOnly': 'Archived only',
|
|
230
240
|
'wsPreset.title': 'Change workspace default preset',
|
|
231
241
|
'wsPreset.current': 'Current default preset',
|
|
232
242
|
'wsPreset.currentUnknown': 'Current preset unknown',
|
|
@@ -398,6 +408,11 @@ window.__ModuleLoader__.load({
|
|
|
398
408
|
'menu.moveOutGroup': 'إخراج من المجموعة',
|
|
399
409
|
'menu.sessionId': 'نسخ معرّف الجلسة',
|
|
400
410
|
'menu.workspacePreset': 'الإعداد المسبق الافتراضي لمساحة العمل…',
|
|
411
|
+
'menu.archiveFilter': 'تصفية الجلسات',
|
|
412
|
+
'menu.archiveFollow': 'اتباع العام',
|
|
413
|
+
'menu.archiveHide': 'إخفاء المؤرشفة',
|
|
414
|
+
'menu.archiveShow': 'كل المحادثات (إظهار المؤرشفة)',
|
|
415
|
+
'menu.archiveOnly': 'المؤرشفة فقط',
|
|
401
416
|
'wsPreset.title': 'تغيير الإعداد المسبق الافتراضي لمساحة العمل',
|
|
402
417
|
'wsPreset.current': 'الإعداد المسبق الافتراضي الحالي',
|
|
403
418
|
'wsPreset.currentUnknown': 'الإعداد المسبق الحالي غير معروف',
|
|
@@ -561,6 +576,11 @@ window.__ModuleLoader__.load({
|
|
|
561
576
|
'menu.moveOutGroup': 'Aus der Gruppe entfernen',
|
|
562
577
|
'menu.sessionId': 'Session-ID kopieren',
|
|
563
578
|
'menu.workspacePreset': 'Workspace-Standardpreset…',
|
|
579
|
+
'menu.archiveFilter': 'Sitzungen filtern',
|
|
580
|
+
'menu.archiveFollow': 'Global folgen',
|
|
581
|
+
'menu.archiveHide': 'Archivierte ausblenden',
|
|
582
|
+
'menu.archiveShow': 'Alle Unterhaltungen (archivierte anzeigen)',
|
|
583
|
+
'menu.archiveOnly': 'Nur archivierte',
|
|
564
584
|
'wsPreset.title': 'Workspace-Standardpreset ändern',
|
|
565
585
|
'wsPreset.current': 'Aktueller Standardpreset',
|
|
566
586
|
'wsPreset.currentUnknown': 'Aktueller Preset unbekannt',
|
|
@@ -724,6 +744,11 @@ window.__ModuleLoader__.load({
|
|
|
724
744
|
'menu.moveOutGroup': 'Retirer du groupe',
|
|
725
745
|
'menu.sessionId': 'Copier l’ID de session',
|
|
726
746
|
'menu.workspacePreset': 'Préréglage par défaut du workspace…',
|
|
747
|
+
'menu.archiveFilter': 'Filtrer les sessions',
|
|
748
|
+
'menu.archiveFollow': 'Suivre le global',
|
|
749
|
+
'menu.archiveHide': 'Masquer les archivées',
|
|
750
|
+
'menu.archiveShow': 'Toutes les conversations (afficher les archivées)',
|
|
751
|
+
'menu.archiveOnly': 'Archivées uniquement',
|
|
727
752
|
'wsPreset.title': 'Modifier le préréglage par défaut du workspace',
|
|
728
753
|
'wsPreset.current': 'Préréglage par défaut actuel',
|
|
729
754
|
'wsPreset.currentUnknown': 'Préréglage actuel inconnu',
|
|
@@ -887,6 +912,11 @@ window.__ModuleLoader__.load({
|
|
|
887
912
|
'menu.moveOutGroup': 'समूह से बाहर निकालें',
|
|
888
913
|
'menu.sessionId': 'सेशन आईडी कॉपी करें',
|
|
889
914
|
'menu.workspacePreset': 'वर्कस्पेस डिफ़ॉल्ट प्रीसेट…',
|
|
915
|
+
'menu.archiveFilter': 'सत्र फ़िल्टर करें',
|
|
916
|
+
'menu.archiveFollow': 'ग्लोबल का पालन करें',
|
|
917
|
+
'menu.archiveHide': 'संग्रहीत छिपाएँ',
|
|
918
|
+
'menu.archiveShow': 'सभी बातचीत (संग्रहीत दिखाएँ)',
|
|
919
|
+
'menu.archiveOnly': 'केवल संग्रहीत',
|
|
890
920
|
'wsPreset.title': 'वर्कस्पेस डिफ़ॉल्ट प्रीसेट बदलें',
|
|
891
921
|
'wsPreset.current': 'वर्तमान डिफ़ॉल्ट प्रीसेट',
|
|
892
922
|
'wsPreset.currentUnknown': 'वर्तमान प्रीसेट अज्ञात',
|
|
@@ -1050,6 +1080,11 @@ window.__ModuleLoader__.load({
|
|
|
1050
1080
|
'menu.moveOutGroup': 'Keluarkan dari grup',
|
|
1051
1081
|
'menu.sessionId': 'Salin ID Sesi',
|
|
1052
1082
|
'menu.workspacePreset': 'Preset bawaan ruang kerja…',
|
|
1083
|
+
'menu.archiveFilter': 'Filter sesi',
|
|
1084
|
+
'menu.archiveFollow': 'Ikuti global',
|
|
1085
|
+
'menu.archiveHide': 'Sembunyikan arsip',
|
|
1086
|
+
'menu.archiveShow': 'Semua percakapan (tampilkan arsip)',
|
|
1087
|
+
'menu.archiveOnly': 'Hanya arsip',
|
|
1053
1088
|
'wsPreset.title': 'Ubah preset bawaan ruang kerja',
|
|
1054
1089
|
'wsPreset.current': 'Preset bawaan saat ini',
|
|
1055
1090
|
'wsPreset.currentUnknown': 'Preset saat ini tidak diketahui',
|
|
@@ -1213,6 +1248,11 @@ window.__ModuleLoader__.load({
|
|
|
1213
1248
|
'menu.moveOutGroup': 'Rimuovi dal gruppo',
|
|
1214
1249
|
'menu.sessionId': 'Copia ID sessione',
|
|
1215
1250
|
'menu.workspacePreset': 'Preset predefinito dello workspace…',
|
|
1251
|
+
'menu.archiveFilter': 'Filtra sessioni',
|
|
1252
|
+
'menu.archiveFollow': 'Segui globale',
|
|
1253
|
+
'menu.archiveHide': 'Nascondi archiviate',
|
|
1254
|
+
'menu.archiveShow': 'Tutte le conversazioni (mostra archiviate)',
|
|
1255
|
+
'menu.archiveOnly': 'Solo archiviate',
|
|
1216
1256
|
'wsPreset.title': 'Modifica il preset predefinito dello workspace',
|
|
1217
1257
|
'wsPreset.current': 'Preset predefinito attuale',
|
|
1218
1258
|
'wsPreset.currentUnknown': 'Preset attuale sconosciuto',
|
|
@@ -1376,6 +1416,11 @@ window.__ModuleLoader__.load({
|
|
|
1376
1416
|
'menu.moveOutGroup': 'グループから出す',
|
|
1377
1417
|
'menu.sessionId': 'セッション ID をコピー',
|
|
1378
1418
|
'menu.workspacePreset': 'ワークスペース既定プリセット…',
|
|
1419
|
+
'menu.archiveFilter': 'セッションを絞り込む',
|
|
1420
|
+
'menu.archiveFollow': 'グローバルに従う',
|
|
1421
|
+
'menu.archiveHide': 'アーカイブを非表示',
|
|
1422
|
+
'menu.archiveShow': 'すべての会話(アーカイブを表示)',
|
|
1423
|
+
'menu.archiveOnly': 'アーカイブのみ',
|
|
1379
1424
|
'wsPreset.title': 'ワークスペース既定プリセットを変更',
|
|
1380
1425
|
'wsPreset.current': '現在の既定プリセット',
|
|
1381
1426
|
'wsPreset.currentUnknown': '現在のプリセットは不明',
|
|
@@ -1539,6 +1584,11 @@ window.__ModuleLoader__.load({
|
|
|
1539
1584
|
'menu.moveOutGroup': '그룹에서 제거',
|
|
1540
1585
|
'menu.sessionId': '세션 ID 복사',
|
|
1541
1586
|
'menu.workspacePreset': '작업 공간 기본 프리셋…',
|
|
1587
|
+
'menu.archiveFilter': '세션 필터',
|
|
1588
|
+
'menu.archiveFollow': '전역 설정 따르기',
|
|
1589
|
+
'menu.archiveHide': '보관된 항목 숨기기',
|
|
1590
|
+
'menu.archiveShow': '모든 대화(보관된 항목 표시)',
|
|
1591
|
+
'menu.archiveOnly': '보관된 항목만',
|
|
1542
1592
|
'wsPreset.title': '작업 공간 기본 프리셋 변경',
|
|
1543
1593
|
'wsPreset.current': '현재 기본 프리셋',
|
|
1544
1594
|
'wsPreset.currentUnknown': '현재 프리셋을 알 수 없음',
|
|
@@ -1702,6 +1752,11 @@ window.__ModuleLoader__.load({
|
|
|
1702
1752
|
'menu.moveOutGroup': 'Uit groep halen',
|
|
1703
1753
|
'menu.sessionId': 'Sessie-ID kopiëren',
|
|
1704
1754
|
'menu.workspacePreset': 'Standaardpreset van workspace…',
|
|
1755
|
+
'menu.archiveFilter': 'Sessies filteren',
|
|
1756
|
+
'menu.archiveFollow': 'Globaal volgen',
|
|
1757
|
+
'menu.archiveHide': 'Gearchiveerde verbergen',
|
|
1758
|
+
'menu.archiveShow': 'Alle gesprekken (gearchiveerde tonen)',
|
|
1759
|
+
'menu.archiveOnly': 'Alleen gearchiveerd',
|
|
1705
1760
|
'wsPreset.title': 'Standaardpreset van workspace wijzigen',
|
|
1706
1761
|
'wsPreset.current': 'Huidige standaardpreset',
|
|
1707
1762
|
'wsPreset.currentUnknown': 'Huidige preset onbekend',
|
|
@@ -1865,6 +1920,11 @@ window.__ModuleLoader__.load({
|
|
|
1865
1920
|
'menu.moveOutGroup': 'Przenieś poza grupę',
|
|
1866
1921
|
'menu.sessionId': 'Kopiuj ID sesji',
|
|
1867
1922
|
'menu.workspacePreset': 'Domyślny preset obszaru roboczego…',
|
|
1923
|
+
'menu.archiveFilter': 'Filtruj sesje',
|
|
1924
|
+
'menu.archiveFollow': 'Zgodnie z globalnymi',
|
|
1925
|
+
'menu.archiveHide': 'Ukryj zarchiwizowane',
|
|
1926
|
+
'menu.archiveShow': 'Wszystkie rozmowy (pokaż zarchiwizowane)',
|
|
1927
|
+
'menu.archiveOnly': 'Tylko zarchiwizowane',
|
|
1868
1928
|
'wsPreset.title': 'Zmień domyślny preset obszaru roboczego',
|
|
1869
1929
|
'wsPreset.current': 'Aktualny domyślny preset',
|
|
1870
1930
|
'wsPreset.currentUnknown': 'Aktualny preset nieznany',
|
|
@@ -2028,6 +2088,11 @@ window.__ModuleLoader__.load({
|
|
|
2028
2088
|
'menu.moveOutGroup': 'Remover do grupo',
|
|
2029
2089
|
'menu.sessionId': 'Copiar ID da sessão',
|
|
2030
2090
|
'menu.workspacePreset': 'Preset padrão do workspace…',
|
|
2091
|
+
'menu.archiveFilter': 'Filtrar sessões',
|
|
2092
|
+
'menu.archiveFollow': 'Seguir o global',
|
|
2093
|
+
'menu.archiveHide': 'Ocultar arquivadas',
|
|
2094
|
+
'menu.archiveShow': 'Todas as conversas (mostrar arquivadas)',
|
|
2095
|
+
'menu.archiveOnly': 'Somente arquivadas',
|
|
2031
2096
|
'wsPreset.title': 'Alterar o preset padrão do workspace',
|
|
2032
2097
|
'wsPreset.current': 'Preset padrão atual',
|
|
2033
2098
|
'wsPreset.currentUnknown': 'Preset atual desconhecido',
|
|
@@ -2191,6 +2256,11 @@ window.__ModuleLoader__.load({
|
|
|
2191
2256
|
'menu.moveOutGroup': 'Убрать из группы',
|
|
2192
2257
|
'menu.sessionId': 'Копировать ID сессии',
|
|
2193
2258
|
'menu.workspacePreset': 'Пресет воркспейса по умолчанию…',
|
|
2259
|
+
'menu.archiveFilter': 'Фильтровать сеансы',
|
|
2260
|
+
'menu.archiveFollow': 'Как в общем',
|
|
2261
|
+
'menu.archiveHide': 'Скрыть архивные',
|
|
2262
|
+
'menu.archiveShow': 'Все разговоры (показать архивные)',
|
|
2263
|
+
'menu.archiveOnly': 'Только архивные',
|
|
2194
2264
|
'wsPreset.title': 'Изменить пресет воркспейса по умолчанию',
|
|
2195
2265
|
'wsPreset.current': 'Текущий пресет по умолчанию',
|
|
2196
2266
|
'wsPreset.currentUnknown': 'Текущий пресет неизвестен',
|
|
@@ -2354,6 +2424,11 @@ window.__ModuleLoader__.load({
|
|
|
2354
2424
|
'menu.moveOutGroup': 'Flytta ut ur gruppen',
|
|
2355
2425
|
'menu.sessionId': 'Kopiera sessions-ID',
|
|
2356
2426
|
'menu.workspacePreset': 'Standardförinställning för arbetsyta…',
|
|
2427
|
+
'menu.archiveFilter': 'Filtrera sessioner',
|
|
2428
|
+
'menu.archiveFollow': 'Följ globalt',
|
|
2429
|
+
'menu.archiveHide': 'Dölj arkiverade',
|
|
2430
|
+
'menu.archiveShow': 'Alla konversationer (visa arkiverade)',
|
|
2431
|
+
'menu.archiveOnly': 'Endast arkiverade',
|
|
2357
2432
|
'wsPreset.title': 'Ändra standardförinställning för arbetsyta',
|
|
2358
2433
|
'wsPreset.current': 'Aktuell standardförinställning',
|
|
2359
2434
|
'wsPreset.currentUnknown': 'Aktuell förinställning okänd',
|
|
@@ -2517,6 +2592,11 @@ window.__ModuleLoader__.load({
|
|
|
2517
2592
|
'menu.moveOutGroup': 'ย้ายออกจากกลุ่ม',
|
|
2518
2593
|
'menu.sessionId': 'คัดลอก ID เซสชัน',
|
|
2519
2594
|
'menu.workspacePreset': 'พรีเซ็ตเริ่มต้นของพื้นที่ทำงาน…',
|
|
2595
|
+
'menu.archiveFilter': 'กรองเซสชัน',
|
|
2596
|
+
'menu.archiveFollow': 'ตามการตั้งค่าทั่วไป',
|
|
2597
|
+
'menu.archiveHide': 'ซ่อนที่เก็บถาวร',
|
|
2598
|
+
'menu.archiveShow': 'บทสนทนาทั้งหมด (แสดงที่เก็บถาวร)',
|
|
2599
|
+
'menu.archiveOnly': 'เฉพาะที่เก็บถาวร',
|
|
2520
2600
|
'wsPreset.title': 'เปลี่ยนพรีเซ็ตเริ่มต้นของพื้นที่ทำงาน',
|
|
2521
2601
|
'wsPreset.current': 'พรีเซ็ตเริ่มต้นปัจจุบัน',
|
|
2522
2602
|
'wsPreset.currentUnknown': 'ไม่ทราบพรีเซ็ตปัจจุบัน',
|
|
@@ -2680,6 +2760,11 @@ window.__ModuleLoader__.load({
|
|
|
2680
2760
|
'menu.moveOutGroup': 'Gruptan çıkar',
|
|
2681
2761
|
'menu.sessionId': 'Oturum kimliğini kopyala',
|
|
2682
2762
|
'menu.workspacePreset': 'Çalışma alanı varsayılan ön ayarı…',
|
|
2763
|
+
'menu.archiveFilter': 'Oturumları filtrele',
|
|
2764
|
+
'menu.archiveFollow': 'Geneli takip et',
|
|
2765
|
+
'menu.archiveHide': 'Arşivlenenleri gizle',
|
|
2766
|
+
'menu.archiveShow': 'Tüm konuşmalar (arşivlenenleri göster)',
|
|
2767
|
+
'menu.archiveOnly': 'Yalnızca arşivlenenler',
|
|
2683
2768
|
'wsPreset.title': 'Çalışma alanı varsayılan ön ayarını değiştir',
|
|
2684
2769
|
'wsPreset.current': 'Geçerli varsayılan ön ayar',
|
|
2685
2770
|
'wsPreset.currentUnknown': 'Geçerli ön ayar bilinmiyor',
|
|
@@ -2843,6 +2928,11 @@ window.__ModuleLoader__.load({
|
|
|
2843
2928
|
'menu.moveOutGroup': 'Đưa ra khỏi nhóm',
|
|
2844
2929
|
'menu.sessionId': 'Sao chép ID phiên',
|
|
2845
2930
|
'menu.workspacePreset': 'Preset mặc định của không gian làm việc…',
|
|
2931
|
+
'menu.archiveFilter': 'Lọc phiên',
|
|
2932
|
+
'menu.archiveFollow': 'Theo toàn cục',
|
|
2933
|
+
'menu.archiveHide': 'Ẩn đã lưu trữ',
|
|
2934
|
+
'menu.archiveShow': 'Tất cả cuộc trò chuyện (hiện đã lưu trữ)',
|
|
2935
|
+
'menu.archiveOnly': 'Chỉ đã lưu trữ',
|
|
2846
2936
|
'wsPreset.title': 'Thay đổi preset mặc định của không gian làm việc',
|
|
2847
2937
|
'wsPreset.current': 'Preset mặc định hiện tại',
|
|
2848
2938
|
'wsPreset.currentUnknown': 'Không rõ preset hiện tại',
|
|
@@ -3006,6 +3096,11 @@ window.__ModuleLoader__.load({
|
|
|
3006
3096
|
'menu.moveOutGroup': '移出分組',
|
|
3007
3097
|
'menu.sessionId': '複製 Session ID',
|
|
3008
3098
|
'menu.workspacePreset': '修改工作區預設…',
|
|
3099
|
+
'menu.archiveFilter': '篩選會話',
|
|
3100
|
+
'menu.archiveFollow': '跟隨全域',
|
|
3101
|
+
'menu.archiveHide': '隱藏已封存',
|
|
3102
|
+
'menu.archiveShow': '全部對話(顯示已封存)',
|
|
3103
|
+
'menu.archiveOnly': '僅顯示已封存',
|
|
3009
3104
|
'wsPreset.title': '修改工作區預設',
|
|
3010
3105
|
'wsPreset.current': '目前預設',
|
|
3011
3106
|
'wsPreset.currentUnknown': '目前預設未知',
|
|
@@ -3169,6 +3264,11 @@ window.__ModuleLoader__.load({
|
|
|
3169
3264
|
'menu.moveOutGroup': '移出分組',
|
|
3170
3265
|
'menu.sessionId': '複製 Session ID',
|
|
3171
3266
|
'menu.workspacePreset': '修改工作區預設…',
|
|
3267
|
+
'menu.archiveFilter': '篩選會話',
|
|
3268
|
+
'menu.archiveFollow': '跟隨全域',
|
|
3269
|
+
'menu.archiveHide': '隱藏已封存',
|
|
3270
|
+
'menu.archiveShow': '全部對話(顯示已封存)',
|
|
3271
|
+
'menu.archiveOnly': '僅顯示已封存',
|
|
3172
3272
|
'wsPreset.title': '修改工作區預設',
|
|
3173
3273
|
'wsPreset.current': '目前預設',
|
|
3174
3274
|
'wsPreset.currentUnknown': '目前預設未知',
|
|
@@ -3332,6 +3432,11 @@ window.__ModuleLoader__.load({
|
|
|
3332
3432
|
'menu.moveOutGroup': '移出分組',
|
|
3333
3433
|
'menu.sessionId': '複製 Session ID',
|
|
3334
3434
|
'menu.workspacePreset': '修改工作區預設…',
|
|
3435
|
+
'menu.archiveFilter': '篩選工作階段',
|
|
3436
|
+
'menu.archiveFollow': '跟隨全域',
|
|
3437
|
+
'menu.archiveHide': '隱藏已封存',
|
|
3438
|
+
'menu.archiveShow': '全部對話(顯示已封存)',
|
|
3439
|
+
'menu.archiveOnly': '僅顯示已封存',
|
|
3335
3440
|
'wsPreset.title': '修改工作區預設',
|
|
3336
3441
|
'wsPreset.current': '目前預設',
|
|
3337
3442
|
'wsPreset.currentUnknown': '目前預設未知',
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* dsh client store). This host half exists so the cordis plugin row has a
|
|
7
7
|
* valid Node entry and so Config metadata is available to the Loader.
|
|
8
8
|
*/
|
|
9
|
+
import { registerTitleSource } from './title-source.js'
|
|
10
|
+
|
|
9
11
|
export const name = 'dsh-better-workspace'
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -48,9 +50,13 @@ export const Config = Schema === null
|
|
|
48
50
|
appearance: Schema.dict(Schema.any()).default({}).volatile(),
|
|
49
51
|
})
|
|
50
52
|
|
|
51
|
-
export
|
|
53
|
+
export const inject = ['sessionProjections']
|
|
54
|
+
|
|
55
|
+
export async function apply(ctx) {
|
|
52
56
|
const log = ctx && ctx.logger && typeof ctx.logger.info === 'function'
|
|
53
57
|
? (msg) => ctx.logger.info(msg)
|
|
54
58
|
: (msg) => console.log(msg)
|
|
55
|
-
|
|
59
|
+
const registered = await registerTitleSource(ctx, log)
|
|
60
|
+
log('[dsh-better-workspace] host half loaded; UI runs in the browser (client half)'
|
|
61
|
+
+ (registered ? '; session title provenance projection registered' : '; title provenance unavailable'))
|
|
56
62
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title provenance: the host knows WHY a session title exists — the
|
|
3
|
+
* `session/title` event carries `source.kind` (`fallback` | `provider` |
|
|
4
|
+
* `user`), and the official title projection deliberately exposes only the
|
|
5
|
+
* plain string. Without the source, the sidebar can only guess from the text
|
|
6
|
+
* whether a `/` was a deliberate grouping or part of an auto-generated
|
|
7
|
+
* sentence (issue #19). This optional summary projection carries the kind to
|
|
8
|
+
* the list rows so the client can trust intent instead of guessing it.
|
|
9
|
+
*
|
|
10
|
+
* It is a separate module so the fold can be unit-tested without booting the
|
|
11
|
+
* plugin row. Registration is best-effort: when zod is unavailable (a profile
|
|
12
|
+
* that did not hoist the host's dependency), the client keeps its display-layer
|
|
13
|
+
* fallback and the plugin still mounts.
|
|
14
|
+
*/
|
|
15
|
+
export const TITLE_SOURCE_KEY = 'betterWorkspace/titleSource'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the projection definition. `z` is zod's namespace, injected so the
|
|
19
|
+
* fold can be tested with a real schema without a top-level import.
|
|
20
|
+
* @param z - zod namespace (v4).
|
|
21
|
+
* @returns a summary projection definition for `sessionProjections.register`.
|
|
22
|
+
*/
|
|
23
|
+
export function titleSourceProjection(z) {
|
|
24
|
+
const schema = z.string().nullable()
|
|
25
|
+
return {
|
|
26
|
+
key: TITLE_SOURCE_KEY,
|
|
27
|
+
stateVersion: 1,
|
|
28
|
+
stateSchema: schema,
|
|
29
|
+
init: () => null,
|
|
30
|
+
apply: (state, event) => {
|
|
31
|
+
if (!event || event.type !== 'session/title') return state
|
|
32
|
+
const kind = event.data && event.data.source && event.data.source.kind
|
|
33
|
+
return typeof kind === 'string' && kind !== '' ? kind : state
|
|
34
|
+
},
|
|
35
|
+
// A summary unit by default: the list row needs this value. Eager
|
|
36
|
+
// providers must stay cheap; this fold only reads session/title events.
|
|
37
|
+
wire: { viewSchema: schema, view: (state) => state },
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register the projection on the plugin context. Never throws: a missing zod
|
|
43
|
+
* or a registration conflict degrades to the client's text fallback.
|
|
44
|
+
* @param ctx - the plugin host context carrying `sessionProjections`.
|
|
45
|
+
* @param log - diagnostic sink.
|
|
46
|
+
* @returns whether the projection was registered.
|
|
47
|
+
*/
|
|
48
|
+
export async function registerTitleSource(ctx, log = () => {}) {
|
|
49
|
+
let z
|
|
50
|
+
try {
|
|
51
|
+
({ z } = await import('zod'))
|
|
52
|
+
} catch (error) {
|
|
53
|
+
log('[dsh-better-workspace] zod is not resolvable; session title provenance falls back to display heuristics: '
|
|
54
|
+
+ (error && error.message || String(error)))
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await ctx.sessionProjections.register(titleSourceProjection(z))
|
|
59
|
+
return true
|
|
60
|
+
} catch (error) {
|
|
61
|
+
log('[dsh-better-workspace] title provenance projection was not registered; '
|
|
62
|
+
+ 'session titles fall back to display heuristics: ' + (error && error.message || String(error)))
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|