Skip to content

Releases

v0.9.2 — 整个按钮都能点

Choose a tag to compare

@buhuikongpan buhuikongpan released this 11 Sep 07:11

之前只有图标那一小块能触发

原因:entry 的渲染结果被外壳限制在一个固定尺寸的图标位里,所以铺在它上面的透明点击层只盖住了
图标。点到右边的「临时会话」文字时,事件落在外壳自己的 button 上,触发的是 selectPanel
——正好是这个入口要劫持掉的行为。

改法:不在够不到的盒子上做文章,改在捕获阶段接管

从 entry 自己的节点用 closest('button') 找到外层 button,注册原生监听时带
capture: true。捕获监听在 React 的委托冒泡处理器之前执行,在那里 stopPropagation
就能让 selectPanel 永不触发——于是整行(图标、文字、内边距)都能开新会话。

透明点击层随之删除,entry 回到只画一个图标。

40/40 测试通过。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

v0.9.1 — 按钮移到「新会话」下面

Choose a tag to compare

@buhuikongpan buhuikongpan released this 11 Sep 07:03

把按钮从底部挪到「新会话」正下方

原来它待在侧边栏最底部(工作区树下面、设置旁边)——那是整条侧边栏里最难发现的位置。
现在它就在官方「新会话」按钮下面那一排,「开一个新东西」的按钮本来也该在那儿。

那一排是 sidebar.panellist(官方契约:"Global panel icons"),用它有两条约束要绕:

  • 外壳把每个 entry 塞进一个固定尺寸的图标位,所以 entry 只画图标(旁边的标签由外壳投影);
  • entry 外面套着外壳自己的 button,它的 onClick 是 selectPanel(id)。

所以 entry 画图标 + 铺一层覆盖点击区的透明层并 stopPropagation,自己把会话开起来。另外注册一个
渲染 null 的 main 占位——selectPanel 会校验 hasMainPanel(id),不注册会抛错;点击被劫持后
它永远不会渲染。

标签用 thunk 传递,跟随当前界面语言(槽契约支持 string | (() => string),每次投影重新求值)。

自动创建(本来就是这样,未改动)

插件加载时宿主会确保 $DSH_HOME/workspace/default 存在,并在工作区注册表里登记为
「临时会话」;已存在则复用那条记录(幂等)。所以装完插件重启,侧边栏里就有那个文件夹和按钮了。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

40/40 测试通过。

v0.9.0 — 回退:一个共享的临时工作区

Choose a tag to compare

@buhuikongpan buhuikongpan released this 11 Sep 06:45

回到"一个共享的临时工作区"

撤掉 0.6.0–0.8.0 的每会话一个子目录架构与侧边栏浮层。临时会话回到同一个共享工作区目录,
于是它们显示在官方工作区树的「临时会话」文件夹下面——这正是浮层给不了的东西:往侧边栏里塞
第二棵树不是插件能做的事(sidebar.workspaces 是 single 槽,官方组件也没导出,接管就等于把官方
整棵树的维护责任接过来)。

随之删除:浮层、sidebar.panellist 入口、main 占位、hover 信息卡、折叠分组、搜索、
重命名/分叉/归档动作、new-conversation 端点与 sessions.create 那条路径。
lib/ 与 test/ 从 v0.5.1 取回,再打了两个补丁。

两处刻意保留

  • 判定继续用路径前缀匹配:cwd 等于临时目录或在其之下都算临时会话。0.6–0.8 期间建的
    13 个会话的 cwd 是各自的子目录,如果改回严格相等,它们会突然被当成项目、吃上
    <project_context>。用前缀匹配新旧通吃,不用手动迁移。
  • 临时会话仍然什么都不注入。v0.5.1 会注入一段 <workspace_purpose>;那一段不回来——
    模型唯一能依据的就是它实际所在的目录。

同时保持:插件依旧不碰会话权限,这些会话和别的会话权限一致。

代价(明确接受)

共享目录意味着这些会话的产出文件混在同一个目录里,互相可见。约束只剩目录本身,
不再有"每会话一个目录"那种结构性隔离。想回到那种形态:git checkout v0.8.0。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

40/40 测试通过。

v0.8.0 — 浮层对齐官方树:折叠、hover 卡、分叉

Choose a tag to compare

@buhuikongpan buhuikongpan released this 11 Sep 06:23

让浮层更像"外壳的一部分"

几何值对齐官方 Rows.module.css

元素 值
会话行 高 32px、padding: 0 8px、圆角 8px、无 flex gap
标题 14px / 20px,margin: 0 6px 0 4px
时间 12px / 20px,label-tertiary
行内图标操作 16×16、圆角 4px
状态点 落在独立的 16×20 槽里(原来是裸点)
分组行 高 34px(对齐官方的项目行)
hover 背景 --dsw-alias-interactive-bg-hover

补上官方树有、之前缺的三件

  • 可折叠分组:带旋转 chevron 的组头(今天 / 昨天 / 更早,替代官方的"按工作区"分组)
  • hover 信息卡:标题 + 完整 cwd + 最后活动时间,跟随指针;pointer-events: none,所以它永远不会抢走它所描述的那个 hover
  • 分叉:官方行的第三个操作,走 uiWorkspace.forkSession。fork 继承同一个 cwd,所以仍是临时会话

仍然刻意不做(README 已写明原因)

  • 拖拽排序:官方走 workspaces.insertSessionBefore(workspaceId, …),临时会话没有 workspaceId
  • 工作区级操作(重命名/删除/添加):没有工作区可操作

宿主半边未动;33/33 测试通过。

顺带一个核实结论

侧边栏那个旧「临时会话」工作区下的会话都是空会话(每个日志只有 1 个事件、零条对话消息)。官方的
sessionVisible 同样会过滤掉它们(!session.blank || session.id === current),所以浮层不显示不是 bug,
是口径一致。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

v0.7.0 — 侧边栏浮层(不再占中间面板)

Choose a tag to compare

@buhuikongpan buhuikongpan released this 11 Sep 06:02

换座位:从"中间面板"改成"盖在工作区树上方的浮层"

0.6.1 的中间面板坐错了位置:它占掉对话区域,而侧边栏的工作区树照样把这些会话列在「未分组」里,
没有任何入口指向它们。这一版改用外壳专门为浮层预留的那张座位:

  • shell.overlay —— 官方契约原文:"the additive seat for a frame-wide surface of your own:
    a fresh id is added beside the shipped entries instead of replacing them"
    ,而且该层本身点击穿透。
    所以官方的工作区树和它的全部功能(搜索、分组、折叠、拖拽、右键菜单)原样保留,继续由官方维护,
    插件只拥有"临时会话列表"这一份。
  • 侧边栏入口仍然只渲染一个图标(那个槽的契约),但上面铺了一层透明的点击层并 stopPropagation,
    所以点它是开关浮层、不会触发官方的 selectPanel,中间区域完全不受影响。
  • main 仍注册一个占位:layout.selectPanel 会校验 hasMainPanel,不注册会抛错。

浮层里有什么

  • 标题 + 会话计数 + 收起键(✕ / 点遮罩 / Esc 都能收起)
  • 搜索:sessions.search(宿主内容搜索,250ms debounce + abort),结果再按 cwd 前缀过滤,只留临时会话
  • 按时间分组:今天 / 昨天 / 更早
  • 会话行:运行状态点 + 标题 + 相对时间;hover 显示「重命名 / 归档」,右键出同样的菜单,
    双击标题就地改名;归档走 uiWorkspace.archiveSession,与官方数据一致
  • 底部「新建临时会话」

两条刻意的边界

  • 是列表,不是树:临时会话之间没有层级关系(每个会话是一个独立目录当 cwd)。
  • 拖拽排序做不了:官方的路径需要 workspaceId,而临时会话不属于任何工作区。

宿主半边、路径约定与注入规则全程未变;33/33 测试通过。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

v0.6.1 — 侧边栏「临时会话」面板

Choose a tag to compare

@buhuikongpan buhuikongpan released this 10 Sep 14:28

临时会话有了自己的侧边栏入口

临时会话不属于任何工作区(它们的 cwd 是自己的子目录,而 attachSession 要求
cwd === workspace.path),所以官方工作区树只能把它们列在「未分组」下——那个分组名也
改不了(group.ungrouped 属于官方 ui-workspace 命名空间,locale 是单占位)。

这一版让插件自己提供入口:

  • 侧边栏新增「临时会话」面板入口(sidebar.panellist,图标 + 标签)。契约规定 list id
    直接寻址 main 插槽里同 key 的面板,所以它和 panel key 是同一个值。标签以 thunk 形式
    注册——契约每次投影重新求值,切换界面语言时标签跟着变,不需要重新注册。
  • 面板列出全部临时会话(main 插槽,用插件自有的 key,官方只占用 conversation,
    不会遮住官方 UI):按最近活动倒序,点一行就打开它并切回对话视图;面板里也有新建入口。
  • 过滤口径跟侧边栏一致:cwd 在临时根目录之下、非 subagent 子会话、非归档、非空白占位。
    归档集合是 registry-global 的,取自 workspaces 服务而不是 sessions 快照。
  • 底部的快捷新建按钮保留。

仍然存在的边界

官方工作区树里的「未分组」桶照样会列出同一批会话,插件移不走它们。唯一的隐藏手段是
归档,那会让会话从所有官方界面消失,等于劫持,本插件不做。所以两处会看到同一批会话——
插件能做的到此为止。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

宿主半边、路径约定与注入规则都没动,直接更新即可。33/33 测试通过。

v0.6.0 — 每个临时会话一个目录

Choose a tag to compare

@buhuikongpan buhuikongpan released this 10 Sep 14:04

换了架构:每个临时会话一个目录

临时会话不再共用一个目录。每个会话有自己的 cwd($DSH_HOME/workspace/default/<会话 id>/),
产出文件各写各的,互不干扰。

这就是 DSH 对 stray 会话本来的做法——每个会话带一个创建者指定的 cwd——插件只是把它
自动化了:先建目录,再以它为 cwd 创建会话。

用途判定因此变成一个纯路径问题:cwd 落在临时根目录之下(或等于它,兼容旧会话)就是临时
会话。没有状态文件、没有注册表查询、没有会话级开关。

破坏性变更

  • 删掉状态文件层:~/.dsh/storages/workspace-purpose.json 不再读写,可以手工删除。
  • 没有"临时工作区"了:不再创建工作区、不再登记。临时会话不属于任何工作区,因此落在
    侧边栏的「未分组」桶里。这个分组名改不了(group.ungrouped 属于官方 ui-workspace
    命名空间,而 locale 注册是单占位)。
  • 删掉 <workspace_purpose> 注入:临时会话不再注入任何提示词。隔离是结构性的。
  • 插件不再碰会话权限:临时会话保持你配置的沙箱模式和审批策略。把它钉在
    workspace-write 会破坏这些会话常被用来做的清理工作,也会覆盖 permission.defaultPreset。
  • 删掉 ensureConversationWorkspace、conversationWorkspaceId 指针和 workspaceRegistry 依赖。

项目侧不变

<project_context> 照旧只在项目模式(非临时会话)注入。

升级

dsh plugin --profile web update dsh-project-context
# 重启 dsh web

旧临时会话(cwd 是根目录本身)判定仍然认,只是历史里已有的 <workspace_purpose> 消息不会
被改写——插件不删别人的历史。

净减 228 行;33/33 测试通过。

v0.5.1 — 修复 locale 可选注入导致的插件加载失败

Choose a tag to compare

@buhuikongpan buhuikongpan released this 10 Sep 12:48

修复

  • 插件加载失败:lib/client.js 里直接读 ctx.locale,会让 cordis 的 ctx 代理抛
    cannot get property "locale" without inject —— 服务代理的 getter 在 typeof / ?.
    之前就抛错了,apply() 中断后整个插件加载失败(界面报 Failed to load plugins: dsh-project-context)。
    改为可选注入 ctx.inject(['locale'], (scope) => …):宿主提供 locale 服务就注册字典,
    没有就静默跳过,插件照常加载。

说明

  • 只影响客户端半边(lib/client.js),宿主半边无改动。
  • 受影响版本:v0.5.0。v0.4.x 及更早没有这段代码,不受影响。
  • 验证:用 cordis 同款「未注入服务属性即抛错」的 ctx 代理做 A/B 实测 —— locale 缺席时
    修前抛错、修后不抛且注册行为不变;npm test 38/38 通过。

完整变更:v0.5.0...v0.5.1

v0.5.0 — 工作区用途分层:项目 / 临时会话

Choose a tag to compare

@buhuikongpan buhuikongpan released this 10 Sep 12:34

工作区用途分层:项目 / 临时会话

从这一版起,每个本地工作区都带一个用途,判据只有一个:它是不是那个固定的临时会话工作区
($DSH_HOME/workspace/default)。其余一律是项目。

新增

  • 首次运行自动创建临时会话工作区:mkdir -p $DSH_HOME/workspace/default,只建目录、不预放任何文件;
    已在 registry 登记就复用(幂等),未登记才 create(dir, '临时会话')。
    失败(权限/只读/路径被占)不阻塞插件加载:写一条 host 日志、按钮进入禁用态并说明原因,下次自动重试。

  • 侧边栏底部「临时会话」按钮(sidebar.footer.action):点击在临时工作区里开一个新会话;
    当前已经是空白会话时不重复新建。

  • 会话头「临时会话」徽标(conversation.session.header.actions):不可点的 Pill,
    仅在该会话属于临时工作区时渲染。

  • 用途决定注入什么(agent/pre-step,独立 user 消息块,与技能目录同款机制):

    用途 注入块
    项目工作区 <project_context>:文件归此处、先建心智模型再动手、空目录即新项目
    临时会话 <workspace_purpose>:非项目、项目约定不适用、不要在此目录累积项目文件

    内容不变不重发,变化时原位替换;无 cwd 的系统会话不注入、不判定。

  • 只读端点 GET /__workspace-purpose/state → {ok, conversationWorkspaceId, path, ready}。
    用途完全由目录决定,客户端没有任何需要写回的状态。

  • 客户端判定口径:反查 workspaces 快照的 sessionIds → workspaceId(纯 uuid 比较,
    不碰 Windows 路径大小写/8.3 短名的坑);宿主侧另有 cwd realpath 兜底。

破坏性变更

  • 每会话的「项目」开关已移除——用途是工作区的属性,不是会话偶然的属性。
    UI 从 conversation.input.right 移到 sidebar.footer.action + conversation.session.header.actions。
  • 旧状态文件 $DSH_HOME/storages/project-context.json({modes:{...}})不再读写,可以手工删除。
    新状态文件是 $DSH_HOME/storages/workspace-purpose.json:
    { "version": 1, "conversationWorkspaceId": "<uuid|null>" }。
  • 会话历史里已有的 project-context 消息不会被改写(不删别人的历史),只是不再被当作开关依据。

修复(由本版新增的测试发现)

  • 失败的 ensure 曾把失败结果缓存到 await 之后,导致重试永久拿到旧的 null——
    现在只在成功时缓存,失败在 promise 真正结算时解禁。
  • 状态缓存与磁盘不一致时(文件被外部删除)会报告"已记录"的假成功——
    现在缓存对 mtime 敏感,且落盘失败时回滚到写入前的值。

测试

node --test:38/38,npm run check 通过。覆盖状态文件损坏容错、幂等/并发 ensure、
失败降级与重试、用途判定(registry membership 与 cwd 兜底)、历史幂等与可见性。

安装 / 更新

dsh plugin --profile web add "github:buhuikongpan/dsh-project-context"
# 或
dsh plugin --profile web update dsh-project-context

装完必须重启 dsh web 才生效。

已知边界

  • 按钮只能落在侧边栏底部:官方 sidebar.workspaces 是 single 槽且被 WorkspaceBrowser 占用,
    行/分区级插槽不存在(已强制枚举验证),贴进工作区列表只能整包替换官方组件,故不做。
  • 临时工作区会像普通工作区一样出现在列表里(新建工作区置顶),不做排序干预。
  • $DSH_HOME/AGENTS.md 仍会注入临时会话:它是 DSH 的用户级全局指令,不属于项目级作用域。

v0.4.2

Choose a tag to compare

@buhuikongpan buhuikongpan released this 03 Sep 11:56

修复:升级后的 DSH 移除了 Session.events,改用 snapshotEvents()/eventAt(seq)。此前去重逻辑读到 session.events 恒为 undefined,导致 project-context 消息在每个 agent 步骤(含每次工具调用后)都被重复注入。现在用 sessionEvents() 兼容新 API,内容未变时幂等不重发。补了真实 Session 形状的回归测试。