fix(v0.3.4): adapt worktable to DSH 0.2 Web
This commit is contained in:
@@ -8,15 +8,21 @@
|
||||
- **分栏工作区引擎(自研)**:声明式布局预设(左栏/顶行/主行 + 右侧对话窗),窗格可拖拽分割、标签页模型;内置 资源管理器 / 终端 / 浏览器 / 动画 / 自定义窗口。
|
||||
- **控制室**:默认自带项目(固定首位、不可删除),项目卡片网格实时监控所有项目的状态(工作中/待你决定/已完成),零轮询零 Token。
|
||||
- **自动挂载握手**:项目内 agent 完成窗口任务后写 widget-result.json,产物自动挂进对应窗口。
|
||||
- **平台**:Windows 是当前完整验证平台;macOS 为实验性支持(核心文件路径代码已做跨平台适配,尚未真机端到端验证)。
|
||||
- **平台**:Windows 是本轮验证平台,范围见下方兼容说明;macOS 为实验性支持(核心文件路径代码已做跨平台适配,尚未真机端到端验证)。
|
||||
|
||||
## 技术底座
|
||||
|
||||
- Cordis 插件协议(客户端 bundle + 服务端路由 /api/worktable/*、终端 WebSocket)。
|
||||
- 界面经 slot 座位协议注入侧边栏与 shell.overlay;对话窗复用宿主会话服务(sessions.open)。
|
||||
- 界面经 slot 座位协议注入侧边栏与 shell.overlay;对话窗复用宿主会话服务(新版 uiWorkspace.openSession,旧版 sessions.open)。
|
||||
- 状态监控为宿主会话运行时快照的事件订阅镜像;视图/项目/绑定状态存 localStorage。
|
||||
- 客户端:TypeScript + React(host externals)+ 原生 CSS;服务端:Node。
|
||||
|
||||
## v0.3.4 兼容与验证范围
|
||||
|
||||
- DSH 网页端兼容声明仅列 **0.1.1-rc.2 / 0.1.2-rc.1 / 0.2.0-rc.2**,不是对任意新旧版本的保证。
|
||||
- 本轮在 Windows + 0.2.0-rc.2 验证冷启动、工作台/控制室、新建并绑定对话、短文本发送与模型回复、项目关闭回切和重新打开;旧版沿用此前页面验收并补接口回退与标注引用坐标回归,未重跑整套旧版 GUI。
|
||||
- 实际审批、子代理、自动挂载全链路尚未完成业务验收;官方桌面端未适配,macOS 未真机验证。升级后请完整重启 DSH 并刷新页面,仍加载失败需保留完整错误。
|
||||
|
||||
## 权限与外部服务(如实披露)
|
||||
|
||||
- **文件访问**:服务端读写工作区与用户指定的目录——文件窗目录列表、MD 编辑保存、项目文件夹 mkdir、站点静态托管、widget 产物读取。除用户当次操作外,插件在加载时、项目状态变化时、绑定会话完成时会自动读取已配置项目的数据(如挂载产物 widget-result.json、项目文件夹与绑定信息),并读取宿主提供的工作区数据(workspaces 路由)。读写路径均来自用户配置或宿主数据;失败返回错误或降级。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"entry": {
|
||||
"name": "dsh-worktable",
|
||||
"inject": []
|
||||
|
||||
+358
-218
@@ -8896,7 +8896,29 @@ function basenameOf(p) {
|
||||
var import_react = require("react");
|
||||
|
||||
// src/client/changelog.ts
|
||||
var CHANGELOG_V030 = `\u66F4\u65B0\u516C\u544A \xB7 v0.3.3
|
||||
var CHANGELOG_V030 = `\u66F4\u65B0\u516C\u544A \xB7 v0.3.4
|
||||
|
||||
\u{1F6E0}\uFE0F \u672C\u6B21\u66F4\u65B0\uFF1A\u9002\u914D DSH 0.2 \u7F51\u9875\u7AEF
|
||||
|
||||
\u3010\u9879\u76EE\u4E0E\u4F1A\u8BDD\u8054\u52A8\u3011\u63A5\u5165\u65B0\u7248 DSH \u7684\u4F1A\u8BDD\u8BC6\u522B\u4E0E\u8DF3\u8F6C\u63A5\u53E3\uFF0C\u9002\u914D\u6253\u5F00\u7ED1\u5B9A\u9879\u76EE\u3001\u5173\u95ED\u9879\u76EE\u540E\u56DE\u5230\u539F\u4F1A\u8BDD\u3001\u63A7\u5236\u5BA4\u5361\u7247\u8DF3\u8F6C\u7B49\u5165\u53E3\uFF0C\u4E0D\u66FF\u6362\u5BBF\u4E3B\u5B98\u65B9\u7EC4\u4EF6\u3002
|
||||
|
||||
\u3010\u65B0\u5EFA\u5BF9\u8BDD\u3011\u4FEE\u590D\u65B0\u7248 DSH \u4E2D\u53EA\u8BBE\u7F6E\u9879\u76EE\u8DEF\u5F84\u521B\u5EFA\u4F1A\u8BDD\uFF0C\u53EF\u80FD\u51FA\u73B0\u300C\u5BF9\u8BDD\u80FD\u6253\u5F00\uFF0C\u5374\u65E0\u6CD5\u8F93\u5165\u300D\u7684\u95EE\u9898\u3002\u65B0\u5EFA\u65F6\u5148\u901A\u8FC7\u5BBF\u4E3B\u767B\u8BB0\u6216\u590D\u7528\u5DE5\u4F5C\u533A\uFF0C\u518D\u521B\u5EFA\u4F1A\u8BDD\uFF1B\u9884\u8BBE\u4E0E\u6A21\u578B\u9009\u62E9\u4E5F\u63A5\u5165\u65B0\u7248\u63A5\u53E3\u3002\u6B64\u6539\u52A8\u4E0D\u81EA\u52A8\u8FC1\u79FB\u5DF2\u6709\u4F1A\u8BDD\u3002
|
||||
|
||||
\u3010\u6807\u6CE8\u8F93\u5165\u3011\u9002\u914D\u65B0\u7248\u804A\u5929\u8F93\u5165\u6846\uFF0C\u901A\u8FC7\u5BBF\u4E3B\u63A5\u53E3\u8FFD\u52A0\u6807\u6CE8\uFF0C\u4FDD\u7559\u5DF2\u6709\u8349\u7A3F\u4E0E\u6587\u4EF6\u5F15\u7528\uFF0C\u4E0D\u81EA\u52A8\u53D1\u9001\uFF1B\u65E0\u6CD5\u5199\u5165\u65F6\u4ECD\u53EF\u590D\u5236\u5230\u526A\u8D34\u677F\u3002\u4FEE\u590D\u65E7\u7248\u5DF2\u6709\u6587\u4EF6\u5F15\u7528\u65F6\u8FFD\u52A0\u4F4D\u7F6E\u8BA1\u7B97\u9519\u8BEF\u7684\u95EE\u9898\uFF1B\u65E7\u7248\u8F93\u5165\u6846\u4FDD\u7559\u517C\u5BB9\u5206\u652F\uFF0C\u4E0D\u518D\u8BEF\u9009\u5DE5\u4F5C\u53F0\u81EA\u5DF1\u7684\u6807\u6CE8\u6846\u6216\u6587\u4EF6\u7F16\u8F91\u6846\u3002
|
||||
|
||||
\u3010\u63A7\u5236\u5BA4\u72B6\u6001\u4E0E\u8BA1\u65F6\u3011\u9002\u914D\u65B0\u7248\u4F1A\u8BDD\u72B6\u6001\u3001\u5B50\u4EE3\u7406\u5217\u8868\u53CA\u6700\u8FD1\u6D88\u606F\u8BFB\u53D6\u3002\u652F\u6301\u8BC6\u522B\u65B0\u7684\u5F85\u786E\u8BA4\u4E8B\u9879\uFF1A\u786E\u8BA4\u8FC7\u4E0A\u4E00\u6761\u540E\uFF0C\u4E0B\u4E00\u6761\u4ECD\u4F1A\u91CD\u65B0\u4EAE\u8D77\u63D0\u9192\uFF1B\u8BA1\u65F6\u8BFB\u53D6\u5BBF\u4E3B\u5DF2\u6709\u7684\u4EFB\u52A1\u6216\u4F1A\u8BDD\u8D77\u70B9\uFF0C\u53D6\u4E0D\u5230\u6709\u6548\u8D77\u70B9\u65F6\u4E0D\u663E\u793A\uFF0C\u4E0D\u7F16\u9020\u8FD0\u884C\u65F6\u957F\u3002\u4EFB\u52A1\u505C\u6B62\u4E0D\u7B49\u4E8E\u4EFB\u52A1\u6210\u529F\u3002
|
||||
|
||||
\u3010\u53D1\u9001\u4E0E\u6D88\u606F\u9884\u89C8\u3011\u9002\u914D\u65B0\u7248\u4F1A\u8BDD\u7684\u4E34\u65F6\u6301\u6709\u4E0E\u91CA\u653E\u673A\u5236\u3002\u53D1\u9001\u8BF7\u6C42\u5931\u8D25\u6216\u88AB\u62D2\u7EDD\u540E\uFF0C\u4E0D\u518D\u6362\u53E6\u4E00\u6761\u901A\u9053\u91CD\u590D\u53D1\u9001\uFF1B\u63A7\u5236\u5BA4\u9884\u89C8\u4ECE\u5BBF\u4E3B\u5DF2\u52A0\u8F7D\u7684\u6D88\u606F\u7A97\u53E3\u63D0\u53D6\u666E\u901A\u6587\u672C\uFF0C\u4E0D\u4EE3\u8868\u8BFB\u53D6\u4E86\u5B8C\u6574\u5386\u53F2\u3002
|
||||
|
||||
\u3010\u6570\u636E\u4E0E\u5347\u7EA7\u3011\u672C\u6B21\u672A\u66F4\u540D\u6216\u8FC1\u79FB\u5DE5\u4F5C\u53F0\u7684\u9879\u76EE\u3001\u5E03\u5C40\u4E0E\u5A92\u4F53\u5B58\u50A8\uFF0C\u4E0D\u9700\u8981\u4E3A\u4E86\u672C\u6B21\u66F4\u65B0\u4E3B\u52A8\u6E05\u7A7A\u6D4F\u89C8\u5668\u6570\u636E\u3002\u5347\u7EA7\u524D\u8BF7\u5907\u4EFD\u914D\u7F6E\u53CA\u91CD\u8981\u6570\u636E\uFF0C\u786E\u8BA4\u5176\u4ED6\u63D2\u4EF6\u4E5F\u652F\u6301\u76EE\u6807 DSH \u7248\u672C\uFF1B\u5B89\u88C5\u540E\u5B8C\u6574\u9000\u51FA\u5E76\u91CD\u542F DSH\uFF0C\u518D\u5237\u65B0\u9875\u9762\u3002\u82E5\u4ECD\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u4FDD\u7559\u5B8C\u6574\u9519\u8BEF\u4FE1\u606F\u53CD\u9988\uFF0C\u4E0D\u8981\u628A\u5B83\u5F53\u4F5C\u6B63\u5E38\u73B0\u8C61\u3002
|
||||
|
||||
\u3010\u9A8C\u8BC1\u8303\u56F4\u3011\u672C\u8F6E\u5728 Windows + DSH 0.2.0-rc.2 \u7F51\u9875\u7AEF\u9A8C\u8BC1\u4E86\u51B7\u542F\u52A8\u3001\u5DE5\u4F5C\u53F0/\u63A7\u5236\u5BA4\u9875\u9762\u3001\u901A\u8FC7\u5DE5\u4F5C\u53F0\u65B0\u5EFA\u5E76\u7ED1\u5B9A\u5BF9\u8BDD\u3001\u53D1\u9001\u77ED\u6587\u672C\u5E76\u6536\u5230\u6A21\u578B\u56DE\u590D\u3001\u5173\u95ED\u9879\u76EE\u540E\u56DE\u5207\u4E0E\u91CD\u65B0\u6253\u5F00\uFF1B\u540C\u65F6\u8865\u5145\u65E7\u7248\u670D\u52A1\u56DE\u9000\u4E0E\u5F15\u7528\u5750\u6807\u7B49\u5B9A\u5411\u56DE\u5F52\u3002\u517C\u5BB9\u58F0\u660E\u4EC5\u5217 0.1.1-rc.2\u30010.1.2-rc.1\u30010.2.0-rc.2\uFF1B\u524D\u4E24\u4E2A\u7248\u672C\u6CBF\u7528\u6B64\u524D\u9875\u9762\u9A8C\u6536\u57FA\u7840\u5E76\u505A\u4EE3\u7801\u56DE\u5F52\uFF0C\u672C\u8F6E\u672A\u91CD\u8DD1\u6574\u5957\u65E7\u7248 GUI\uFF0C\u4E0D\u627F\u8BFA\u5176\u4ED6\u7248\u672C\u5168\u90E8\u517C\u5BB9\u3002
|
||||
|
||||
\u3010\u5C1A\u672A\u95ED\u73AF\u3011\u5B9E\u9645\u5BA1\u6279/\u5B50\u4EE3\u7406\u6D41\u7A0B\u53CA\u81EA\u52A8\u6302\u8F7D\u5168\u94FE\u8DEF\u5C1A\u672A\u5B8C\u6210\u4E1A\u52A1\u9A8C\u6536\u3002\u5B98\u65B9\u684C\u9762\u7AEF\u5C1A\u672A\u9002\u914D\uFF0CmacOS \u5C1A\u672A\u771F\u673A\u9A8C\u8BC1\uFF1B\u672C\u6B21\u4E5F\u4E0D\u4EE3\u8868\u6240\u6709\u7528\u6237\u7684 DSH \u542F\u52A8\u5931\u8D25\u5DF2\u89E3\u51B3\u3002\u516C\u5F00\u53D1\u5E03\u4FE1\u606F\u4EE5 GitHub Release \u4E3A\u51C6\u3002
|
||||
|
||||
\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
||||
|
||||
\u6B64\u524D\u7248\u672C\u8BF4\u660E\uFF08v0.3.3\uFF09
|
||||
|
||||
\u{1F6E0}\uFE0F \u672C\u7248\u672C\u66F4\u65B0
|
||||
|
||||
@@ -8948,7 +8970,7 @@ var CHANGELOG_V030 = `\u66F4\u65B0\u516C\u544A \xB7 v0.3.3
|
||||
\u30103.1 \u6587\u5B57\u4E0E\u7EC6\u8282\u6253\u78E8\u3011\u5168\u5C40\u5B57\u4F53\u4E0E\u5B57\u53F7\u7EDF\u4E00\u4F18\u5316\u3001\u4E0B\u62C9\u9762\u677F\u73BB\u7483\u5316\u4E0E\u5BF9\u9F50\u3001\u83DC\u5355\u70B9\u9009\u540E\u4FDD\u6301\u6253\u5F00\u4FBF\u4E8E\u8FDE\u7EED\u9884\u89C8\u3001\u6309\u94AE\u63CF\u8FB9\u4E0E\u60AC\u505C\u53CD\u9988\u7B49\u4EA4\u4E92\u7EC6\u8282\uFF1B\u540C\u65F6\u4FEE\u590D\u4E86\u591A\u9879\u4F53\u9A8C\u95EE\u9898\uFF08\u7167\u7247\u4E0A\u4F20\u6E05\u6670\u5EA6\u3001\u80CC\u666F\u7F51\u683C\u7EBF\u5728\u7167\u7247\u6A21\u5F0F\u4E0B\u4E0D\u751F\u6548\u3001\u6D45\u8272\u4E3B\u9898\u4E0B\u5DE5\u4F5C\u72B6\u6001\u5149\u6548\u4E0D\u53EF\u89C1\u7B49\uFF09\u3002`;
|
||||
|
||||
// src/client/updateCheck.ts
|
||||
var LOCAL_VERSION = false ? "dev" : "0.3.3";
|
||||
var LOCAL_VERSION = false ? "dev" : "0.3.4";
|
||||
var UPDATE_REPO = "Aisland-SJL/dsh-worktable";
|
||||
var K_UPDATE_CHECK = "dsh.worktable.updateCheck.v1";
|
||||
var K_LAST_CHECK = "dsh.worktable.lastUpdateCheck.v1";
|
||||
@@ -17098,24 +17120,7 @@ async function copyTextSafe(text2) {
|
||||
}
|
||||
}
|
||||
function fillHostInput(text2) {
|
||||
try {
|
||||
const ta = document.querySelector("textarea[data-phase]") ?? Array.from(document.querySelectorAll("textarea")).pop();
|
||||
if (!ta) return false;
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
||||
const next = ta.value && ta.value.trim() ? ta.value + "\n\n" + text2 : text2;
|
||||
if (setter) setter.call(ta, next);
|
||||
else ta.value = next;
|
||||
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
try {
|
||||
ta.focus();
|
||||
ta.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
ta.setSelectionRange(ta.value.length, ta.value.length);
|
||||
} catch {
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return splitEnv?.fillHostInput(text2) ?? false;
|
||||
}
|
||||
function confirmAnnot() {
|
||||
const s = annotState;
|
||||
@@ -19940,6 +19945,255 @@ try {
|
||||
} catch {
|
||||
}
|
||||
|
||||
// src/client/sessionCompat.ts
|
||||
function currentSessionOf(snapshot) {
|
||||
if (typeof snapshot?.current === "string") return snapshot.current;
|
||||
const rows = Object.values(snapshot?.byId ?? {});
|
||||
return rows.find((row) => (row?.retainedBy?.mainView ?? 0) > 0)?.id ?? "";
|
||||
}
|
||||
function openHostSession(ctx, sessions, id) {
|
||||
const ui = typeof ctx?.get === "function" ? ctx.get("uiWorkspace") : ctx?.uiWorkspace;
|
||||
if (typeof ui?.openSession === "function") return ui.openSession(id);
|
||||
if (typeof sessions?.open === "function") return sessions.open(id);
|
||||
throw new Error("Host session navigation unavailable");
|
||||
}
|
||||
async function sendHostSession(bridge, id, text2) {
|
||||
const sessions = bridge?.sessions;
|
||||
const check = (result) => {
|
||||
if (result?.kind === "error" || result?.ok === false) {
|
||||
throw new Error(result.text || result.error?.message || result.error?.code || "Session submission rejected");
|
||||
}
|
||||
};
|
||||
const send = async (session) => {
|
||||
if (session && typeof bridge?.conversation?.sendSession === "function") {
|
||||
check(await bridge.conversation.sendSession(session, text2, [], "queue"));
|
||||
return;
|
||||
}
|
||||
if (session && typeof session.prompt === "function") {
|
||||
check(await session.prompt([{ type: "text", text: text2 }], "queue"));
|
||||
return;
|
||||
}
|
||||
const conv = sessions?.scope?.(id)?.get?.("conversation");
|
||||
if (typeof conv?.send === "function") {
|
||||
check(await conv.send(text2));
|
||||
return;
|
||||
}
|
||||
throw new Error("no send path: session face unavailable");
|
||||
};
|
||||
if (typeof sessions?.using === "function") {
|
||||
await sessions.using(id, { source: "dshWorktable" }, async (ref) => {
|
||||
const binding = await ref.ready;
|
||||
await send(binding.session);
|
||||
});
|
||||
return;
|
||||
}
|
||||
let face = null;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
face = sessions?.binding?.(id)?.session;
|
||||
if (face) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
await send(face);
|
||||
}
|
||||
function createSessionSnapshotAdapter() {
|
||||
const previousRunning = /* @__PURE__ */ new Map();
|
||||
const stopped = /* @__PURE__ */ new Set();
|
||||
return (snapshot, statusSource) => {
|
||||
if (!snapshot || typeof snapshot.current === "string") return snapshot;
|
||||
const statuses = statusSource?.getSnapshot?.();
|
||||
const byId = {};
|
||||
const subagentsByParent = { ...snapshot.subagentsByParent };
|
||||
for (const [id, row] of Object.entries(snapshot.byId ?? {})) {
|
||||
const status = statuses?.get?.(id);
|
||||
const running = status?.running ?? row.running;
|
||||
if (running === true) stopped.delete(id);
|
||||
else if (running === false && previousRunning.get(id) === true) stopped.add(id);
|
||||
if (typeof running === "boolean") previousRunning.set(id, running);
|
||||
byId[id] = {
|
||||
...row,
|
||||
running,
|
||||
pendingInteraction: status ? status.pendingInteraction : row.pendingInteraction,
|
||||
// completionUnread clears when opening the main view. Keep a witnessed run-stop
|
||||
// until the next run so an active project's widget-result handshake still runs.
|
||||
completed: running !== true && (stopped.has(id) || status?.completionUnread === true || row.completed === true)
|
||||
};
|
||||
const children = snapshot.projectionsBySession?.[id]?.values?.subagentCatalog ?? row.projectionValues?.subagentCatalog;
|
||||
if (Array.isArray(children)) subagentsByParent[id] = children;
|
||||
}
|
||||
if (snapshot.phase === "ready") {
|
||||
for (const id of previousRunning.keys()) if (!byId[id]) {
|
||||
previousRunning.delete(id);
|
||||
stopped.delete(id);
|
||||
}
|
||||
}
|
||||
return { ...snapshot, byId, subagentsByParent };
|
||||
};
|
||||
}
|
||||
function childSessionIdsOf(snapshot, sid) {
|
||||
const children = /* @__PURE__ */ new Set();
|
||||
for (const [id, row] of Object.entries(snapshot?.byId ?? {})) {
|
||||
if (id !== sid && row?.parentId === sid) children.add(id);
|
||||
}
|
||||
const catalog = snapshot?.subagentsByParent?.[sid];
|
||||
const rows = Array.isArray(catalog) ? catalog : catalog?.entries ?? catalog?.items ?? [];
|
||||
if (Array.isArray(rows)) for (const row of rows) {
|
||||
const id = row?.sessionId ?? row?.id;
|
||||
if (typeof id === "string" && id !== sid) children.add(id);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
function pendingAckOf(snapshot, sid, legacyFace) {
|
||||
const identities = [];
|
||||
let identified = false;
|
||||
const add = (id, pending) => {
|
||||
if (pending == null) return;
|
||||
const key = typeof pending?.key === "string" ? pending.key : "";
|
||||
if (key) identified = true;
|
||||
identities.push(JSON.stringify([id, String(pending?.kind ?? ""), key]));
|
||||
};
|
||||
const ids = [sid, ...childSessionIdsOf(snapshot, sid)];
|
||||
for (const id of ids) add(id, snapshot?.byId?.[id]?.pendingInteraction);
|
||||
if (snapshot?.byId?.[sid]?.pendingInteraction == null && Array.isArray(legacyFace?.pending)) {
|
||||
for (const pending of legacyFace.pending) add(sid, pending);
|
||||
}
|
||||
if (!identities.length) return null;
|
||||
return identified ? "need:" + JSON.stringify([...new Set(identities)].sort()) : "need";
|
||||
}
|
||||
function sessionRuntimeMs(snapshot, sid, sessions, uiConversation, now = Date.now()) {
|
||||
if (snapshot?.byId?.[sid]?.running !== true) return null;
|
||||
const validStart = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
||||
let start = null;
|
||||
for (const job of snapshot?.jobsBySession?.[sid] ?? []) {
|
||||
if (job?.status === "running" && validStart(job.startedAt) && (start == null || job.startedAt < start)) start = job.startedAt;
|
||||
}
|
||||
if (start != null) return Math.max(0, now - start);
|
||||
let timings;
|
||||
try {
|
||||
timings = uiConversation?.binding?.(sid)?.target?.("chat")?.getSnapshot?.()?.legacy?.turnTimings;
|
||||
} catch {
|
||||
}
|
||||
if (timings == null) {
|
||||
try {
|
||||
timings = sessions?.binding?.(sid)?.session?.getSnapshot?.()?.turnTimings;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
const turns = typeof timings?.values === "function" ? Array.from(timings.values()) : Object.values(timings ?? {});
|
||||
for (const turn of turns) {
|
||||
if (turn?.endTime == null && validStart(turn?.startTime) && (start == null || turn.startTime > start)) start = turn.startTime;
|
||||
}
|
||||
return start == null ? null : Math.max(0, now - start);
|
||||
}
|
||||
|
||||
// src/client/hostInput.ts
|
||||
function appendHostInput(bridge, text2, doc = document) {
|
||||
try {
|
||||
const resolver = bridge?.conversation?.input;
|
||||
if (typeof resolver?.for === "function") {
|
||||
const snapshot = bridge.sessions?.list?.getSnapshot?.();
|
||||
const id = currentSessionOf(snapshot);
|
||||
const scope = id ? bridge.sessions?.scope?.(id) : null;
|
||||
if (!scope) return false;
|
||||
const input = resolver.for(scope);
|
||||
const state = input?.state?.getSnapshot?.();
|
||||
if (!state || typeof state.draft !== "string" || !Number.isInteger(state.draftRev) || !["plain", "claimed"].includes(state.phase) || !Array.isArray(state.occurrences)) return false;
|
||||
const compactReferences = typeof snapshot?.current !== "string";
|
||||
let end = state.draft.length;
|
||||
let previousEnd = 0;
|
||||
for (const item of state.occurrences) {
|
||||
if (!Number.isInteger(item.offset) || !Number.isInteger(item.length) || item.length < 0 || item.offset < previousEnd || item.offset + item.length > state.draft.length) return false;
|
||||
previousEnd = item.offset + item.length;
|
||||
if (compactReferences) end += 1 - item.length;
|
||||
}
|
||||
const span = { start: end, end, draftRev: state.draftRev };
|
||||
const addition = (state.draft.trim() ? "\n\n" : "") + text2;
|
||||
const inserted = typeof input.actions?.insertText === "function" ? input.actions.insertText(addition, span) === true : scope.bail?.("slash/input-insert-text", { text: addition, span }) === true;
|
||||
if (inserted) {
|
||||
try {
|
||||
input.focus?.();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
const ta = doc.querySelector("textarea[data-phase]");
|
||||
if (!ta || ta.disabled || ta.readOnly || ta.closest(".dsh-wt_annotUi")) return false;
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
||||
const next = ta.value && ta.value.trim() ? ta.value + "\n\n" + text2 : text2;
|
||||
if (setter) setter.call(ta, next);
|
||||
else ta.value = next;
|
||||
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
try {
|
||||
ta.focus();
|
||||
ta.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
ta.setSelectionRange(ta.value.length, ta.value.length);
|
||||
} catch {
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// src/client/sessionDetails.ts
|
||||
function cleanPreviewText(raw) {
|
||||
return String(raw ?? "").replace(/```[a-zA-Z0-9_+-]*[\s\S]*?```/g, " ").replace(/```[a-zA-Z0-9_+-]*[\s\S]*$/g, " ").replace(/`[^`\n]{1,200}`/g, " ").replace(/```/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
function previewFromEvents(entries) {
|
||||
if (!Array.isArray(entries)) return "";
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const ev = entries[i]?.event;
|
||||
if (ev?.type !== "user/message" && ev?.type !== "assistant/message") continue;
|
||||
const data = ev.type === "assistant/message" ? ev.data?.message ?? ev.data : ev.data;
|
||||
const blocks = data?.content ?? data?.blocks;
|
||||
if (!Array.isArray(blocks)) continue;
|
||||
const raw = blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
||||
const text2 = cleanPreviewText(raw);
|
||||
if (text2.length >= 8) return text2.slice(0, 220);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
async function readSessionPreview(sessions, id) {
|
||||
if (typeof sessions?.using === "function") {
|
||||
return sessions.using(id, { source: "dshWorktablePreview" }, async (ref) => {
|
||||
const binding = await ref.ready;
|
||||
return previewFromEvents(binding.eventSource?.getSnapshot?.()?.entries);
|
||||
});
|
||||
}
|
||||
const face = sessions?.binding?.(id)?.session;
|
||||
if (typeof face?.history !== "function") return "";
|
||||
const result = await face.history({ maxMessages: 6 });
|
||||
return previewFromEvents(result?.result?.value?.events);
|
||||
}
|
||||
function presetApiOf(ctx, legacy) {
|
||||
const api = typeof ctx?.get === "function" ? ctx.get("remote.agentPresets") : ctx?.remote?.agentPresets;
|
||||
if (typeof api?.list !== "function" || typeof api?.select !== "function") return legacy;
|
||||
return {
|
||||
list: async () => ({ result: await api.list() }),
|
||||
select: async ({ sessionId, agentPreset }) => ({ result: await api.select(sessionId, agentPreset) })
|
||||
};
|
||||
}
|
||||
function modelApiOf(ctx, sessions, legacy) {
|
||||
if (typeof sessions?.using !== "function") return legacy;
|
||||
const resolver = typeof ctx?.get === "function" ? ctx.get("modelDirectories") : ctx?.modelDirectories;
|
||||
if (typeof resolver?.directoryFor !== "function") return legacy;
|
||||
const withDirectory = (id, operation) => sessions.using(id, { source: "dshWorktableModel" }, async (ref) => {
|
||||
await ref.ready;
|
||||
return operation(resolver.directoryFor(id));
|
||||
});
|
||||
return {
|
||||
models: async ({ sessionId }) => withDirectory(sessionId, async (directory) => ({ result: { ok: true, value: await directory.load() } })),
|
||||
selectModel: async ({ sessionId, ...selection }) => withDirectory(sessionId, async (directory) => ({ result: await directory.select(selection) }))
|
||||
};
|
||||
}
|
||||
async function createHostSession(sessions, workspaces, options = {}) {
|
||||
if (options.workspaceId || typeof sessions?.using !== "function") return sessions.create(options);
|
||||
const workspace = options.cwd ? await workspaces?.create?.({ path: options.cwd }) : await workspaces?.initializeDefault?.();
|
||||
if (typeof workspace?.workspaceId !== "string" || !workspace.workspaceId) throw new Error("workspace unavailable for new session");
|
||||
const { cwd: _cwd, ...rest } = options;
|
||||
return sessions.create({ ...rest, workspaceId: workspace.workspaceId });
|
||||
}
|
||||
|
||||
// src/client/photoStore.ts
|
||||
var DB_NAME = "dsh-worktable";
|
||||
var STORE = "photoRecords";
|
||||
@@ -20104,7 +20358,7 @@ var WAVE_BG_B64 = "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQUFBAYFBQUHBgYHCQ8KCQgIC
|
||||
|
||||
// src/client/index.tsx
|
||||
var import_jsx_runtime2 = require("react/jsx-runtime");
|
||||
var LOCAL_VERSION2 = false ? "dev" : "0.3.3";
|
||||
var LOCAL_VERSION2 = false ? "dev" : "0.3.4";
|
||||
var UPDATE_REPO2 = "Aisland-SJL/dsh-worktable";
|
||||
var UPGRADE_CMD2 = 'dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"';
|
||||
var UPGRADE_AI2 = "\u5E2E\u6211\u5347\u7EA7 dsh-worktable\uFF1A\u6267\u884C " + UPGRADE_CMD2 + "\uFF0C\u5B8C\u6210\u540E\u63D0\u9192\u6211\u91CD\u542F dsh web \u5E76\u5237\u65B0\u9875\u9762";
|
||||
@@ -20391,7 +20645,7 @@ var sessionBridge = null;
|
||||
var clientCtx = null;
|
||||
var hostApi = null;
|
||||
async function ensureSessionPreset(sessionId) {
|
||||
const api = hostApi?.agentPresets;
|
||||
const api = presetApiOf(clientCtx, hostApi?.agentPresets);
|
||||
if (!api || typeof api.list !== "function" || typeof api.select !== "function") return;
|
||||
try {
|
||||
const listRes = await api.list({});
|
||||
@@ -20447,7 +20701,7 @@ function pickInheritedModel(groups, baseModel) {
|
||||
return { provider: flat[0].provider, model: flat[0].model };
|
||||
}
|
||||
async function ensureSessionModel(sessionId) {
|
||||
const api = hostApi?.sessions;
|
||||
const api = modelApiOf(clientCtx, sessionBridge?.sessions, hostApi?.sessions);
|
||||
if (!api || typeof api.models !== "function" || typeof api.selectModel !== "function") return;
|
||||
try {
|
||||
const mRes = await api.models({ sessionId });
|
||||
@@ -20457,7 +20711,7 @@ async function ensureSessionModel(sessionId) {
|
||||
let target = null;
|
||||
let effort;
|
||||
try {
|
||||
const cur = sessionBridge?.list?.getSnapshot?.()?.current;
|
||||
const cur = currentSessionOf(sessionBridge?.list?.getSnapshot?.());
|
||||
if (cur && cur !== sessionId) {
|
||||
const cRes = await api.models({ sessionId: cur });
|
||||
if (cRes?.result?.ok) {
|
||||
@@ -20551,51 +20805,10 @@ function listWorkspaces() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function cleanPreviewText(raw) {
|
||||
let s = String(raw ?? "");
|
||||
s = s.replace(/```[a-zA-Z0-9_+-]*[\s\S]*?```/g, " ");
|
||||
s = s.replace(/```[a-zA-Z0-9_+-]*[\s\S]*$/g, " ");
|
||||
s = s.replace(/`[^`\n]{1,200}`/g, " ");
|
||||
s = s.replace(/```/g, " ");
|
||||
s = s.replace(/\s+/g, " ").trim();
|
||||
return s;
|
||||
}
|
||||
var previewCache = /* @__PURE__ */ new Map();
|
||||
var previewFetching = /* @__PURE__ */ new Set();
|
||||
var previewSweepBusy = false;
|
||||
var previewTimer = null;
|
||||
async function coldPreviewOf(face) {
|
||||
if (!face || typeof face.history !== "function") return "";
|
||||
const r1 = await face.history({ maxMessages: 6 });
|
||||
const evs = r1?.result?.value?.events;
|
||||
if (!Array.isArray(evs)) return "";
|
||||
const textOf = (blocks) => {
|
||||
let fallback = "";
|
||||
if (!Array.isArray(blocks)) return "";
|
||||
for (const b of blocks) {
|
||||
const s = typeof b?.text === "string" ? b.text.trim() : "";
|
||||
if (!s) continue;
|
||||
if (b?.type === "text") return s;
|
||||
if (!fallback) fallback = s;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
for (let i = evs.length - 1; i >= 0; i--) {
|
||||
const ev = evs[i]?.event;
|
||||
if (!ev) continue;
|
||||
const d = ev.data ?? {};
|
||||
let raw = "";
|
||||
if (ev.type === "user/message") raw = textOf(d.content ?? d.blocks);
|
||||
else if (ev.type === "assistant/message") {
|
||||
const m = d.message ?? d;
|
||||
raw = textOf(m.content ?? m.blocks);
|
||||
}
|
||||
if (!raw) continue;
|
||||
const clean = cleanPreviewText(raw);
|
||||
if (clean.length >= 8) return clean.slice(0, 220);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
async function sweepPreviews() {
|
||||
if (previewSweepBusy) return;
|
||||
previewSweepBusy = true;
|
||||
@@ -20605,12 +20818,9 @@ async function sweepPreviews() {
|
||||
if (previewFetching.has(sid)) continue;
|
||||
previewFetching.add(sid);
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session;
|
||||
const txt = await coldPreviewOf(face);
|
||||
if (txt) {
|
||||
previewCache.set(sid, txt);
|
||||
notifyConsole();
|
||||
}
|
||||
const txt = await readSessionPreview(sessionBridge?.sessions, sid);
|
||||
previewCache.set(sid, txt);
|
||||
notifyConsole();
|
||||
} catch {
|
||||
} finally {
|
||||
previewFetching.delete(sid);
|
||||
@@ -20621,7 +20831,7 @@ async function sweepPreviews() {
|
||||
}
|
||||
}
|
||||
function schedulePreviewSweep() {
|
||||
if (previewTimer != null) return;
|
||||
if (previewSweepBusy || previewTimer != null) return;
|
||||
previewTimer = window.setTimeout(() => {
|
||||
previewTimer = null;
|
||||
sweepPreviews();
|
||||
@@ -20629,7 +20839,9 @@ function schedulePreviewSweep() {
|
||||
}
|
||||
function lastTextOf(sid) {
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
const binding = sessionBridge?.sessions?.binding?.(sid);
|
||||
if (binding?.eventSource?.getSnapshot) return previewFromEvents(binding.eventSource.getSnapshot().entries);
|
||||
const face = binding?.session?.getSnapshot?.();
|
||||
const nodes = face?.nodes ?? [];
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const n = nodes[i];
|
||||
@@ -20656,7 +20868,7 @@ async function fetchSessionGroups() {
|
||||
try {
|
||||
const snap = sessionBridge?.list?.getSnapshot?.();
|
||||
const byId = snap?.byId ?? {};
|
||||
const current = snap?.current ?? "";
|
||||
const current = currentSessionOf(snap);
|
||||
const subKids = /* @__PURE__ */ new Set();
|
||||
try {
|
||||
const map2 = snap?.subagentsByParent ?? {};
|
||||
@@ -20723,41 +20935,7 @@ function hideBindTip() {
|
||||
async function promptIntoSession(sessionId, text2) {
|
||||
const b = sessionBridge;
|
||||
if (!b) throw new Error("bridge unavailable");
|
||||
const sessions = b.sessions;
|
||||
let session = null;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
session = sessions?.binding?.(sessionId)?.session ?? null;
|
||||
} catch {
|
||||
session = null;
|
||||
}
|
||||
if (session) break;
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
if (session) {
|
||||
if (typeof b.conversation?.sendSession === "function") {
|
||||
try {
|
||||
await b.conversation.sendSession(session, text2, [], "queue");
|
||||
return;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (typeof session.prompt === "function") {
|
||||
const result = await session.prompt([{ type: "text", text: text2 }], "queue");
|
||||
if (result && result.ok) return;
|
||||
if (result && !result.ok) throw new Error("session.prompt: " + (result.error?.code ?? "rejected") + (result.error?.message ? ": " + result.error.message : ""));
|
||||
}
|
||||
}
|
||||
try {
|
||||
const scoped = sessions?.scope?.(sessionId);
|
||||
const conv = scoped?.get?.("conversation");
|
||||
if (conv && typeof conv.send === "function") {
|
||||
await conv.send(text2);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
throw new Error("no send path: session face unavailable");
|
||||
await sendHostSession(b, sessionId, text2);
|
||||
}
|
||||
var KNOWLEDGE_PACK = [
|
||||
"\u3010\u63D2\u4EF6\u77E5\u8BC6\u5305\xB7\u8BF7\u76F4\u63A5\u91C7\u7528\uFF0C\u4E0D\u8981\u91CD\u65B0\u4FA6\u5BDF\u63D2\u4EF6\u6E90\u7801\u3011",
|
||||
@@ -20828,12 +21006,12 @@ async function createCustomSession(projectId, projectName, requirement, group, w
|
||||
let createOpts = {};
|
||||
if (workspaceId) createOpts = { workspaceId };
|
||||
else if (folder) createOpts = { cwd: folder };
|
||||
const sessionId = await b.sessions.create(createOpts);
|
||||
const sessionId = await createHostSession(b.sessions, b.workspaces, createOpts);
|
||||
await ensureSessionPreset(sessionId);
|
||||
await ensureSessionModel(sessionId);
|
||||
markPluginSessionOpen(sessionId);
|
||||
try {
|
||||
await b.sessions.open?.(sessionId);
|
||||
await openHostSession(clientCtx, b.sessions, sessionId);
|
||||
} catch {
|
||||
}
|
||||
await promptIntoSession(sessionId, text2);
|
||||
@@ -20845,7 +21023,7 @@ async function sendCustomToSession(sessionId, projectId, projectName, requiremen
|
||||
const text2 = buildWindowTaskText(projectId, projectName, windowLabel, requirement, folder, "send");
|
||||
markPluginSessionOpen(sessionId);
|
||||
try {
|
||||
await b.sessions.open?.(sessionId);
|
||||
await openHostSession(clientCtx, b.sessions, sessionId);
|
||||
} catch {
|
||||
}
|
||||
await promptIntoSession(sessionId, text2);
|
||||
@@ -20955,10 +21133,21 @@ function sessionNotifyState(entry) {
|
||||
if (entry.completed === true) return "done";
|
||||
return null;
|
||||
}
|
||||
var sessionStatusSource = null;
|
||||
var sessionConversationSource = null;
|
||||
var adaptSessionSnapshot = createSessionSnapshotAdapter();
|
||||
function pendingAckForSession(sid) {
|
||||
let face;
|
||||
try {
|
||||
face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
} catch {
|
||||
}
|
||||
return pendingAckOf(sessionsSnapshotStore.snapshot, sid, face);
|
||||
}
|
||||
function syncSessionScope(list2) {
|
||||
try {
|
||||
const snap = list2.getSnapshot();
|
||||
const current = snap?.current ?? "";
|
||||
const snap = adaptSessionSnapshot(list2.getSnapshot(), sessionStatusSource);
|
||||
const current = currentSessionOf(snap);
|
||||
const entry = snap?.byId?.[current] ?? snap?.items?.find((it) => it.sessionId === current) ?? null ?? null;
|
||||
const cat = snap?.subagentsByParent?.[current];
|
||||
let subagents = [];
|
||||
@@ -21190,32 +21379,13 @@ function WorktableSection(props) {
|
||||
const pr = projectsRef.current;
|
||||
const snap = sessionsSnapshotStore.snapshot;
|
||||
const byId = snap?.byId ?? {};
|
||||
const jobsMap = snap?.jobsBySession ?? {};
|
||||
const now = Date.now();
|
||||
const kidsSetOf = (sid) => {
|
||||
const set = /* @__PURE__ */ new Set();
|
||||
for (const [cid, ce] of Object.entries(byId)) if (ce?.parentId === sid) set.add(cid);
|
||||
const v = (snap?.subagentsByParent ?? {})[sid];
|
||||
const arr = Array.isArray(v) ? v : v?.entries ?? v?.items ?? [];
|
||||
if (Array.isArray(arr)) arr.forEach((c) => {
|
||||
const cid = c?.sessionId ?? c?.id;
|
||||
if (typeof cid === "string") set.add(cid);
|
||||
});
|
||||
return set;
|
||||
};
|
||||
const kidsSetOf = (sid) => childSessionIdsOf(snap, sid);
|
||||
const statusOf = (sid) => {
|
||||
if (!sid) return "idle";
|
||||
const e = byId[sid];
|
||||
if (!e) return "idle";
|
||||
if (sessionNotifyState(e) === "need") return "need";
|
||||
for (const cid of kidsSetOf(sid)) {
|
||||
if (byId[cid] && sessionNotifyState(byId[cid]) === "need") return "need";
|
||||
}
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
if (Array.isArray(face?.pending) && face.pending.length > 0) return "need";
|
||||
} catch {
|
||||
}
|
||||
if (pendingAckForSession(sid) != null) return "need";
|
||||
if (e.completed === true) return "done";
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
@@ -21227,36 +21397,13 @@ function WorktableSection(props) {
|
||||
};
|
||||
const runtimeOf = (sid) => {
|
||||
if (!sid) return null;
|
||||
const e = byId[sid];
|
||||
if (!e || e.running !== true) return null;
|
||||
let start = null;
|
||||
const jobs = jobsMap[sid] ?? [];
|
||||
for (const j of jobs) {
|
||||
if (j?.status === "running" && typeof j.startedAt === "number" && (start == null || j.startedAt < start)) start = j.startedAt;
|
||||
}
|
||||
if (start != null) return Math.max(0, now - start);
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
const timings = face?.turnTimings;
|
||||
if (timings instanceof Map) {
|
||||
for (const t2 of Array.from(timings.values()).reverse()) {
|
||||
if (t2 && typeof t2.startTime === "number" && t2.endTime == null) return Math.max(0, now - t2.startTime);
|
||||
}
|
||||
} else if (timings && typeof timings === "object") {
|
||||
for (const k of Object.keys(timings).reverse()) {
|
||||
const t2 = timings[k];
|
||||
if (t2 && typeof t2.startTime === "number" && t2.endTime == null) return Math.max(0, now - t2.startTime);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return null;
|
||||
return sessionRuntimeMs(snap, sid, sessionBridge?.sessions, sessionConversationSource, now);
|
||||
};
|
||||
const ackMap = loadNotifyAck();
|
||||
const make = (id, name, icon, self2) => {
|
||||
const sid = pr.projects.bindings[id];
|
||||
const status = statusOf(sid);
|
||||
const glow = !!sid && (status === "done" && ackMap[sid] !== "done" || status === "need" && ackMap[sid] !== "need");
|
||||
const glow = !!sid && (status === "done" && ackMap[sid] !== "done" || status === "need" && ackMap[sid] !== pendingAckForSession(sid));
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
@@ -21297,6 +21444,7 @@ function WorktableSection(props) {
|
||||
};
|
||||
(0, import_react2.useEffect)(() => {
|
||||
const env = {
|
||||
fillHostInput: (text2) => appendHostInput(sessionBridge, text2),
|
||||
getScope: () => {
|
||||
const s = sessionScopeStore.snapshot;
|
||||
return s ? { sessionId: s.sessionId, cwd: s.cwd } : null;
|
||||
@@ -21439,7 +21587,7 @@ function WorktableSection(props) {
|
||||
const bound = pr.bindings[id];
|
||||
if (bound) {
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(bound);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, bound);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -21450,7 +21598,7 @@ function WorktableSection(props) {
|
||||
if (!sid) return;
|
||||
markPluginSessionOpen(sid);
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(sid);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, sid);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -21511,7 +21659,7 @@ function WorktableSection(props) {
|
||||
const prev = projectAttachRef.sessionId;
|
||||
if (prev) {
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(prev);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, prev);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -21670,7 +21818,7 @@ function WorktableSection(props) {
|
||||
if (splitStore.active && splitStore.spec?.id === spec.id) {
|
||||
let prev = null;
|
||||
try {
|
||||
prev = sessionBridge?.list?.getSnapshot?.()?.current ?? null;
|
||||
prev = currentSessionOf(sessionBridge?.list?.getSnapshot?.()) || null;
|
||||
} catch {
|
||||
}
|
||||
projectAttachRef.sessionId = prev;
|
||||
@@ -21678,7 +21826,7 @@ function WorktableSection(props) {
|
||||
const bound = projectsRef.current.projects.bindings[spec.id];
|
||||
if (bound) {
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(bound);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, bound);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -21713,7 +21861,7 @@ function WorktableSection(props) {
|
||||
if (splitStore.active && splitStore.spec?.id === CONSOLE_ID) {
|
||||
let prev = null;
|
||||
try {
|
||||
prev = sessionBridge?.list?.getSnapshot?.()?.current ?? null;
|
||||
prev = currentSessionOf(sessionBridge?.list?.getSnapshot?.()) || null;
|
||||
} catch {
|
||||
}
|
||||
projectAttachRef.sessionId = prev;
|
||||
@@ -21721,7 +21869,7 @@ function WorktableSection(props) {
|
||||
projectAttachRef.attached = bound ?? prev;
|
||||
if (bound) {
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(bound);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, bound);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -21792,7 +21940,7 @@ function WorktableSection(props) {
|
||||
let createOpts = {};
|
||||
if (workspaceId) createOpts = { workspaceId };
|
||||
else if (folder) createOpts = { cwd: folder };
|
||||
const sessionId = await b.sessions.create(createOpts);
|
||||
const sessionId = await createHostSession(b.sessions, b.workspaces, createOpts);
|
||||
await ensureSessionPreset(sessionId);
|
||||
await ensureSessionModel(sessionId);
|
||||
markPluginSessionOpen(sessionId);
|
||||
@@ -22060,20 +22208,7 @@ function WorktableSection(props) {
|
||||
};
|
||||
}, [projects.folders, applyWidgetManifest]);
|
||||
const collectKids = (0, import_react2.useCallback)((sid) => {
|
||||
const kids = /* @__PURE__ */ new Set();
|
||||
const snap = sessionsSnapshotStore.snapshot;
|
||||
const byId = snap?.byId ?? {};
|
||||
for (const [cid, ce] of Object.entries(byId)) {
|
||||
if (ce?.parentId === sid) kids.add(cid);
|
||||
}
|
||||
const subMap = snap?.subagentsByParent ?? {};
|
||||
const v = subMap[sid];
|
||||
const arr = Array.isArray(v) ? v : v?.entries ?? v?.items ?? [];
|
||||
if (Array.isArray(arr)) arr.forEach((c) => {
|
||||
const cid = c?.sessionId ?? c?.id;
|
||||
if (typeof cid === "string") kids.add(cid);
|
||||
});
|
||||
return kids;
|
||||
return childSessionIdsOf(sessionsSnapshotStore.snapshot, sid);
|
||||
}, []);
|
||||
const bindNotifyMap = (0, import_react2.useMemo)(() => {
|
||||
const map2 = {};
|
||||
@@ -22083,29 +22218,16 @@ function WorktableSection(props) {
|
||||
for (const [pid, sid] of Object.entries(projects.bindings)) {
|
||||
const e = byId[sid];
|
||||
if (!e) continue;
|
||||
let needNow = sessionNotifyState(e) === "need";
|
||||
if (!needNow) {
|
||||
for (const cid of collectKids(sid)) {
|
||||
const ce = byId[cid];
|
||||
if (ce && sessionNotifyState(ce) === "need") {
|
||||
needNow = true;
|
||||
break;
|
||||
}
|
||||
const pendingAck = pendingAckForSession(sid);
|
||||
if (pendingAck !== seen[sid]) {
|
||||
if (seen[sid] !== void 0) {
|
||||
clearNotifyAck(sid);
|
||||
delete ack[sid];
|
||||
}
|
||||
seen[sid] = pendingAck;
|
||||
}
|
||||
if (!needNow) {
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
if (Array.isArray(face?.pending) && face.pending.length > 0) needNow = true;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (needNow !== seen[sid]) {
|
||||
if (seen[sid] !== void 0) clearNotifyAck(sid);
|
||||
seen[sid] = needNow;
|
||||
}
|
||||
if (needNow) {
|
||||
if (ack[sid] !== "need") map2[pid] = "need";
|
||||
if (pendingAck != null) {
|
||||
if (ack[sid] !== pendingAck) map2[pid] = "need";
|
||||
continue;
|
||||
}
|
||||
if (sessionNotifyState(e) === "done" && ack[sid] !== "done") {
|
||||
@@ -22120,22 +22242,20 @@ function WorktableSection(props) {
|
||||
const sid = projectsRef.current.projects.bindings[projectId];
|
||||
if (!sid) return;
|
||||
const byId = sessionsSnapshotStore.snapshot?.byId ?? {};
|
||||
let needNow = sessionNotifyState(byId[sid]) === "need";
|
||||
const pendingAck = pendingAckForSession(sid);
|
||||
for (const cid of collectKids(sid)) {
|
||||
if (sessionNotifyState(byId[cid]) === "need") {
|
||||
needNow = true;
|
||||
saveNotifyAck(cid, "need");
|
||||
const childAck = pendingAckForSession(cid);
|
||||
if (childAck != null) {
|
||||
saveNotifyAck(cid, childAck);
|
||||
notifyStateSeenRef.current[cid] = childAck;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!needNow) {
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
if (Array.isArray(face?.pending) && face.pending.length > 0) needNow = true;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (needNow) saveNotifyAck(sid, "need");
|
||||
else {
|
||||
if (pendingAck != null) {
|
||||
saveNotifyAck(sid, pendingAck);
|
||||
notifyStateSeenRef.current[sid] = pendingAck;
|
||||
} else {
|
||||
const st = sessionNotifyState(byId[sid]);
|
||||
if (st === "done") saveNotifyAck(sid, "done");
|
||||
}
|
||||
@@ -22479,7 +22599,7 @@ function WorktableSection(props) {
|
||||
const bound = projectsRef.current.projects.bindings[pid];
|
||||
if (bound) {
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(bound);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, bound);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -22579,7 +22699,7 @@ function WorktableSection(props) {
|
||||
const bound = pr.bindings[id];
|
||||
if (bound) {
|
||||
try {
|
||||
sessionBridge?.sessions?.open?.(bound);
|
||||
openHostSession(clientCtx, sessionBridge?.sessions, bound);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -23393,7 +23513,7 @@ function apply(ctx) {
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
window.__dshOpenSession = (id) => ctx.sessions?.open?.(id);
|
||||
window.__dshOpenSession = (id) => openHostSession(ctx, ctx.sessions, id);
|
||||
window.__dshSessions = ctx.sessions;
|
||||
window.__dshPromptIntoSession = (id, text2) => promptIntoSession(id, text2);
|
||||
window.__dshWorkspaces = ctx.workspaces;
|
||||
@@ -23454,9 +23574,29 @@ function apply(ctx) {
|
||||
ctx.effect(() => disposeSubscribe, "dsh-worktable: project registry watch");
|
||||
const sessionsList = ctx.sessions?.list;
|
||||
if (sessionsList && typeof sessionsList.getSnapshot === "function") {
|
||||
adaptSessionSnapshot = createSessionSnapshotAdapter();
|
||||
syncSessionScope(sessionsList);
|
||||
const disposeScope = sessionsList.subscribe(() => syncSessionScope(sessionsList));
|
||||
ctx.effect(() => disposeScope, "dsh-worktable: session scope watch");
|
||||
ctx.inject(["uiSession"], (statusCtx) => {
|
||||
const source = statusCtx.uiSession?.sessionStatus;
|
||||
if (!source?.getSnapshot || !source?.subscribe) return;
|
||||
sessionStatusSource = source;
|
||||
syncSessionScope(sessionsList);
|
||||
const disposeStatus = source.subscribe(() => syncSessionScope(sessionsList));
|
||||
statusCtx.effect(() => () => {
|
||||
disposeStatus();
|
||||
if (sessionStatusSource === source) sessionStatusSource = null;
|
||||
}, "dsh-worktable: session status watch");
|
||||
});
|
||||
ctx.inject(["uiConversation"], (conversationCtx) => {
|
||||
const source = conversationCtx.uiConversation;
|
||||
if (typeof source?.binding !== "function") return;
|
||||
sessionConversationSource = source;
|
||||
conversationCtx.effect(() => () => {
|
||||
if (sessionConversationSource === source) sessionConversationSource = null;
|
||||
}, "dsh-worktable: conversation timer source");
|
||||
});
|
||||
}
|
||||
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
||||
name: "shell.overlay",
|
||||
|
||||
@@ -115,7 +115,7 @@ function resolveDshHomeSafe() {
|
||||
cachedDshHome = baseDshHome();
|
||||
return cachedDshHome;
|
||||
}
|
||||
var PLUGIN_VERSION = false ? "dev" : "0.3.3";
|
||||
var PLUGIN_VERSION = false ? "dev" : "0.3.4";
|
||||
var name = "dsh-worktable";
|
||||
var inject = ["webServer", "sessions"];
|
||||
var HEALTH_PATH = "/api/worktable/health";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"description": "工作台 Worktable:DeepSeek Harness 侧边栏的 agent 级项目容器(应用抽屉)",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Aisland-SJL/dsh-worktable.git",
|
||||
@@ -39,10 +39,11 @@
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"dsh": "0.1.1-rc.2 || 0.1.2-rc.1",
|
||||
"dsh": "0.1.1-rc.2 || 0.1.2-rc.1 || 0.2.0-rc.2",
|
||||
"dshReleases": {
|
||||
"0.1.1-rc.2": "compatible",
|
||||
"0.1.2-rc.1": "compatible"
|
||||
"0.1.2-rc.1": "compatible",
|
||||
"0.2.0-rc.2": "compatible"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -51,6 +52,9 @@
|
||||
"check": "node --check lib/index.js && node --check lib/client.js",
|
||||
"pack": "node release-prep.mjs",
|
||||
"test:gate": "node client-factory-gate.test.mjs",
|
||||
"test:input": "node ../04_test/host-input.test.mjs",
|
||||
"test:session": "node ../04_test/session-compat.test.mjs",
|
||||
"test:details": "node ../04_test/session-details.test.mjs",
|
||||
"verify:remote": "node verify-remote.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -82,7 +82,12 @@ if (patchOk && (patchEntries[0].id !== IDENTITY || patchEntries[0].name !== IDEN
|
||||
if (!patchOk) process.exit(1)
|
||||
console.log('[release-prep] identity check passed (' + IDENTITY + ', cordis.patch.yml strict)')
|
||||
|
||||
// ---------- 2. 构建 + 语法检查 ----------
|
||||
// ---------- 2. 会话/输入兼容回归 + 构建 + 语法检查 ----------
|
||||
const compatTests = ['host-input.test.mjs', 'session-compat.test.mjs', 'session-details.test.mjs']
|
||||
.map((file) => join(HERE, '..', '04_test', file))
|
||||
const compatRun = spawnSync(process.execPath, ['--test', ...compatTests], { cwd: HERE, stdio: 'inherit' })
|
||||
if (compatRun.status !== 0) { console.error('[release-prep] FAIL: session/input compatibility tests'); process.exit(1) }
|
||||
console.log('[release-prep] session/input compatibility tests passed (source contracts)')
|
||||
// 构建必带 cwd: HERE(脚本自身位置=01_content),否则从仓库根调用会输出到根 lib/ 造成旧包
|
||||
const build = spawnSync(process.execPath, [join(HERE, 'build.mjs')], { stdio: 'inherit', cwd: HERE })
|
||||
if (build.status !== 0) { console.error('[release-prep] FAIL: build failed'); process.exit(1) }
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
/** 更新公告正文(v0.3.3:DSH 0.1.2 兼容 + 数据路径修复;呈现时按纯文本保留换行,不做 md 渲染) */
|
||||
export const CHANGELOG_VERSION = 'v0.3.3'
|
||||
export const CHANGELOG_V030 = `更新公告 · v0.3.3
|
||||
/** 更新公告正文(v0.3.4:DSH 0.2 网页端适配;呈现时按纯文本保留换行,不做 md 渲染) */
|
||||
export const CHANGELOG_VERSION = 'v0.3.4'
|
||||
export const CHANGELOG_V030 = `更新公告 · v0.3.4
|
||||
|
||||
🛠️ 本次更新:适配 DSH 0.2 网页端
|
||||
|
||||
【项目与会话联动】接入新版 DSH 的会话识别与跳转接口,适配打开绑定项目、关闭项目后回到原会话、控制室卡片跳转等入口,不替换宿主官方组件。
|
||||
|
||||
【新建对话】修复新版 DSH 中只设置项目路径创建会话,可能出现「对话能打开,却无法输入」的问题。新建时先通过宿主登记或复用工作区,再创建会话;预设与模型选择也接入新版接口。此改动不自动迁移已有会话。
|
||||
|
||||
【标注输入】适配新版聊天输入框,通过宿主接口追加标注,保留已有草稿与文件引用,不自动发送;无法写入时仍可复制到剪贴板。修复旧版已有文件引用时追加位置计算错误的问题;旧版输入框保留兼容分支,不再误选工作台自己的标注框或文件编辑框。
|
||||
|
||||
【控制室状态与计时】适配新版会话状态、子代理列表及最近消息读取。支持识别新的待确认事项:确认过上一条后,下一条仍会重新亮起提醒;计时读取宿主已有的任务或会话起点,取不到有效起点时不显示,不编造运行时长。任务停止不等于任务成功。
|
||||
|
||||
【发送与消息预览】适配新版会话的临时持有与释放机制。发送请求失败或被拒绝后,不再换另一条通道重复发送;控制室预览从宿主已加载的消息窗口提取普通文本,不代表读取了完整历史。
|
||||
|
||||
【数据与升级】本次未更名或迁移工作台的项目、布局与媒体存储,不需要为了本次更新主动清空浏览器数据。升级前请备份配置及重要数据,确认其他插件也支持目标 DSH 版本;安装后完整退出并重启 DSH,再刷新页面。若仍加载失败,请保留完整错误信息反馈,不要把它当作正常现象。
|
||||
|
||||
【验证范围】本轮在 Windows + DSH 0.2.0-rc.2 网页端验证了冷启动、工作台/控制室页面、通过工作台新建并绑定对话、发送短文本并收到模型回复、关闭项目后回切与重新打开;同时补充旧版服务回退与引用坐标等定向回归。兼容声明仅列 0.1.1-rc.2、0.1.2-rc.1、0.2.0-rc.2;前两个版本沿用此前页面验收基础并做代码回归,本轮未重跑整套旧版 GUI,不承诺其他版本全部兼容。
|
||||
|
||||
【尚未闭环】实际审批/子代理流程及自动挂载全链路尚未完成业务验收。官方桌面端尚未适配,macOS 尚未真机验证;本次也不代表所有用户的 DSH 启动失败已解决。公开发布信息以 GitHub Release 为准。
|
||||
|
||||
───────
|
||||
|
||||
此前版本说明(v0.3.3)
|
||||
|
||||
🛠️ 本版本更新
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { currentSessionOf } from './sessionCompat'
|
||||
|
||||
/** Append annotations without sending or replacing existing reference chips. */
|
||||
export function appendHostInput(bridge: any, text: string, doc: Document = document): boolean {
|
||||
try {
|
||||
const resolver = bridge?.conversation?.input
|
||||
if (typeof resolver?.for === 'function') {
|
||||
const snapshot = bridge.sessions?.list?.getSnapshot?.()
|
||||
const id = currentSessionOf(snapshot)
|
||||
const scope = id ? bridge.sessions?.scope?.(id) : null
|
||||
if (!scope) return false
|
||||
const input = resolver.for(scope)
|
||||
const state = input?.state?.getSnapshot?.()
|
||||
if (!state || typeof state.draft !== 'string' || !Number.isInteger(state.draftRev)
|
||||
|| !['plain', 'claimed'].includes(state.phase) || !Array.isArray(state.occurrences)) return false
|
||||
// DSH 0.1's current-based list uses display-text coordinates. DSH 0.2's
|
||||
// mainView list uses one U+FFFC unit per chip in insertion coordinates.
|
||||
const compactReferences = typeof snapshot?.current !== 'string'
|
||||
let end = state.draft.length
|
||||
let previousEnd = 0
|
||||
for (const item of state.occurrences) {
|
||||
if (!Number.isInteger(item.offset) || !Number.isInteger(item.length)
|
||||
|| item.length < 0 || item.offset < previousEnd
|
||||
|| item.offset + item.length > state.draft.length) return false
|
||||
previousEnd = item.offset + item.length
|
||||
if (compactReferences) end += 1 - item.length
|
||||
}
|
||||
const span = { start: end, end, draftRev: state.draftRev }
|
||||
const addition = (state.draft.trim() ? '\n\n' : '') + text
|
||||
// Public action face provides revision/phase guards and an undo boundary.
|
||||
// The scoped event is the public compatibility path when actions are absent.
|
||||
const inserted = typeof input.actions?.insertText === 'function'
|
||||
? input.actions.insertText(addition, span) === true
|
||||
: scope.bail?.('slash/input-insert-text', { text: addition, span }) === true
|
||||
if (inserted) { try { input.focus?.() } catch {} }
|
||||
return inserted
|
||||
}
|
||||
// Legacy DSH 0.1 textarea. Never select an arbitrary textarea: it may be
|
||||
// the annotation editor itself or an open Worktable file editor.
|
||||
const ta = doc.querySelector<HTMLTextAreaElement>('textarea[data-phase]')
|
||||
if (!ta || ta.disabled || ta.readOnly || ta.closest('.dsh-wt_annotUi')) return false
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set
|
||||
const next = ta.value && ta.value.trim() ? ta.value + '\n\n' + text : text
|
||||
if (setter) setter.call(ta, next)
|
||||
else ta.value = next
|
||||
ta.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
try {
|
||||
ta.focus()
|
||||
ta.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
ta.setSelectionRange(ta.value.length, ta.value.length)
|
||||
} catch {}
|
||||
return true
|
||||
} catch { return false }
|
||||
}
|
||||
+87
-188
@@ -3,6 +3,9 @@ import { css } from './styles'
|
||||
import { NS, zh, en, type WorktableKey } from './locales'
|
||||
import { isAbs, joinPath, parentPathOf, basenameOf } from './pathutil'
|
||||
import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, type SplitPane, type ConsoleCardData } from './split'
|
||||
import { appendHostInput } from './hostInput'
|
||||
import { currentSessionOf, openHostSession, sendHostSession, createSessionSnapshotAdapter, childSessionIdsOf, pendingAckOf, sessionRuntimeMs } from './sessionCompat'
|
||||
import { cleanPreviewText, previewFromEvents, readSessionPreview, presetApiOf, modelApiOf, createHostSession } from './sessionDetails'
|
||||
import { photoStore, kindOf } from './photoStore'
|
||||
import { DEFAULT_BG_SVG, b64ToBlob } from './defaultBg'
|
||||
import { WAVE_BG_B64 } from './waveBg'
|
||||
@@ -421,7 +424,7 @@ let hostApi: { agentPresets?: any; sessions?: any } | null = null
|
||||
/** 新会话显式应用「部署默认预设」:宿主新会话座位同款逻辑(api.agentPresets.select,仅对 blank 会话生效)。
|
||||
* 修复:用户删掉默认模型后,新建会话继承到失效预设,prompt 报 model-unavailable 导致窗口建不出来。 */
|
||||
async function ensureSessionPreset(sessionId: string): Promise<void> {
|
||||
const api = hostApi?.agentPresets
|
||||
const api = presetApiOf(clientCtx, hostApi?.agentPresets)
|
||||
if (!api || typeof api.list !== 'function' || typeof api.select !== 'function') return
|
||||
try {
|
||||
const listRes = await api.list({})
|
||||
@@ -475,7 +478,7 @@ function pickInheritedModel(groups: any[], baseModel: string | undefined): { pro
|
||||
* 不擅自选模型——① 优先继承用户当前会话正在用的模型;② 兜底按失效选择里的模型家族词
|
||||
* 找同款;③ 最后才目录首个。session.selectModel 同时把新选择存为默认,顺带修复后续新会话。 */
|
||||
async function ensureSessionModel(sessionId: string): Promise<void> {
|
||||
const api = hostApi?.sessions
|
||||
const api = modelApiOf(clientCtx, sessionBridge?.sessions, hostApi?.sessions)
|
||||
if (!api || typeof api.models !== 'function' || typeof api.selectModel !== 'function') return
|
||||
try {
|
||||
const mRes = await api.models({ sessionId })
|
||||
@@ -486,7 +489,7 @@ async function ensureSessionModel(sessionId: string): Promise<void> {
|
||||
let effort: string | undefined
|
||||
// ① 无条件继承用户当前会话的选择(用户正在用哪个模型,新会话就用哪个)
|
||||
try {
|
||||
const cur = sessionBridge?.list?.getSnapshot?.()?.current
|
||||
const cur = currentSessionOf(sessionBridge?.list?.getSnapshot?.())
|
||||
if (cur && cur !== sessionId) {
|
||||
const cRes = await api.models({ sessionId: cur })
|
||||
if (cRes?.result?.ok) {
|
||||
@@ -582,57 +585,14 @@ function listWorkspaces(): { id: string; title: string; path: string }[] {
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
/** 预览文本清洗:去掉围栏代码块(```…```,含 dsh-ui 等)与行内代码,压缩空白;
|
||||
* 代码为主的片段会被滤空 → 调用方回退到更早的消息。只清洗显示用副本,不改原文。 */
|
||||
function cleanPreviewText(raw: string): string {
|
||||
let s = String(raw ?? '')
|
||||
s = s.replace(/```[a-zA-Z0-9_+-]*[\s\S]*?```/g, ' ')
|
||||
s = s.replace(/```[a-zA-Z0-9_+-]*[\s\S]*$/g, ' ')
|
||||
s = s.replace(/`[^`\n]{1,200}`/g, ' ')
|
||||
s = s.replace(/```/g, ' ')
|
||||
s = s.replace(/\s+/g, ' ').trim()
|
||||
return s
|
||||
}
|
||||
|
||||
/** 冷会话最近消息缓存与预热:宿主 history 只读通道(face.history 为运行期内建方法,非公开接口,
|
||||
* 只读侦察确认可用;拉取是带宽成本不是 Token 成本,模型不参与)。 */
|
||||
/** 冷会话最近消息缓存与预热:0.2 临时持有会话、读 eventSource 后释放;旧版保留 history 通道。
|
||||
* 只读消息文本,不请求模型;拉取是带宽成本,不产生模型 Token。 */
|
||||
const previewCache = new Map<string, string>()
|
||||
const previewFetching = new Set<string>()
|
||||
let previewSweepBusy = false
|
||||
let previewTimer: number | null = null
|
||||
|
||||
/** 从 history 事件流尾部提取最近一条成品消息文本(优先 text 块;清洗代码后仍太短则回退更早消息) */
|
||||
async function coldPreviewOf(face: any): Promise<string> {
|
||||
if (!face || typeof face.history !== 'function') return ''
|
||||
const r1 = await face.history({ maxMessages: 6 })
|
||||
const evs = r1?.result?.value?.events
|
||||
if (!Array.isArray(evs)) return ''
|
||||
const textOf = (blocks: any): string => {
|
||||
let fallback = ''
|
||||
if (!Array.isArray(blocks)) return ''
|
||||
for (const b of blocks) {
|
||||
const s = typeof b?.text === 'string' ? b.text.trim() : ''
|
||||
if (!s) continue
|
||||
if (b?.type === 'text') return s
|
||||
if (!fallback) fallback = s
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
for (let i = evs.length - 1; i >= 0; i--) {
|
||||
const ev = evs[i]?.event
|
||||
if (!ev) continue
|
||||
const d = ev.data ?? {}
|
||||
let raw = ''
|
||||
if (ev.type === 'user/message') raw = textOf(d.content ?? d.blocks)
|
||||
else if (ev.type === 'assistant/message') { const m = d.message ?? d; raw = textOf(m.content ?? m.blocks) }
|
||||
if (!raw) continue
|
||||
const clean = cleanPreviewText(raw)
|
||||
if (clean.length >= 8) return clean.slice(0, 220)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 预热所有已绑定会话的最近消息(逐个只读拉取;已在拉/已拉过未完成的跳过,失败静默回退内存路径) */
|
||||
/** 预热所有已绑定会话的最近消息(逐个只读拉取;同一在途请求不重复,失败回退缓存/内存路径) */
|
||||
async function sweepPreviews() {
|
||||
if (previewSweepBusy) return
|
||||
previewSweepBusy = true
|
||||
@@ -642,9 +602,9 @@ async function sweepPreviews() {
|
||||
if (previewFetching.has(sid)) continue
|
||||
previewFetching.add(sid)
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session
|
||||
const txt = await coldPreviewOf(face)
|
||||
if (txt) { previewCache.set(sid, txt); notifyConsole() }
|
||||
const txt = await readSessionPreview(sessionBridge?.sessions, sid)
|
||||
previewCache.set(sid, txt)
|
||||
notifyConsole()
|
||||
} catch {} finally { previewFetching.delete(sid) }
|
||||
}
|
||||
} finally { previewSweepBusy = false }
|
||||
@@ -652,14 +612,17 @@ async function sweepPreviews() {
|
||||
|
||||
/** 防抖调度:控制室开着且会话快照变化时刷新预览(合并 6s 内的连续变化) */
|
||||
function schedulePreviewSweep() {
|
||||
if (previewTimer != null) return
|
||||
// Temporary retain/release also changes the catalog: do not schedule ourselves forever.
|
||||
if (previewSweepBusy || previewTimer != null) return
|
||||
previewTimer = window.setTimeout(() => { previewTimer = null; sweepPreviews() }, 6000)
|
||||
}
|
||||
|
||||
/** 从会话快照节点里提取最近一条文本(纯读内存镜像,零 Token;代码清洗后无则 '') */
|
||||
function lastTextOf(sid: string): string {
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
|
||||
const binding = sessionBridge?.sessions?.binding?.(sid)
|
||||
if (binding?.eventSource?.getSnapshot) return previewFromEvents(binding.eventSource.getSnapshot().entries)
|
||||
const face = binding?.session?.getSnapshot?.()
|
||||
const nodes: any[] = face?.nodes ?? []
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const n = nodes[i]
|
||||
@@ -684,7 +647,7 @@ async function fetchSessionGroups(): Promise<{ groups: { title: string; sessions
|
||||
try {
|
||||
const snap = sessionBridge?.list?.getSnapshot?.()
|
||||
const byId = snap?.byId ?? {}
|
||||
const current = snap?.current ?? ''
|
||||
const current = currentSessionOf(snap)
|
||||
// 子代理会话(后台产生的,用户面板看不到)排除
|
||||
const subKids = new Set<string>()
|
||||
try {
|
||||
@@ -763,33 +726,7 @@ function hideBindTip() {
|
||||
async function promptIntoSession(sessionId: string, text: string): Promise<void> {
|
||||
const b = sessionBridge
|
||||
if (!b) throw new Error('bridge unavailable')
|
||||
const sessions = b.sessions as any
|
||||
// 新会话入列可能异步:最多等 2s 直到 binding 可解析
|
||||
let session: any = null
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try { session = sessions?.binding?.(sessionId)?.session ?? null } catch { session = null }
|
||||
if (session) break
|
||||
await new Promise((r) => setTimeout(r, 200))
|
||||
}
|
||||
if (session) {
|
||||
// 1) 宿主包装(正确签名:会话面 + 空图片 + queue 投递)
|
||||
if (typeof b.conversation?.sendSession === 'function') {
|
||||
try { await b.conversation.sendSession(session, text, [], 'queue'); return } catch { /* 包装失败则直连 */ }
|
||||
}
|
||||
// 2) 直连会话面 prompt(宿主 sendSession 内部同款路径)
|
||||
if (typeof session.prompt === 'function') {
|
||||
const result = await session.prompt([{ type: 'text', text }], 'queue')
|
||||
if (result && result.ok) return
|
||||
if (result && !result.ok) throw new Error('session.prompt: ' + (result.error?.code ?? 'rejected') + (result.error?.message ? ': ' + result.error.message : ''))
|
||||
}
|
||||
}
|
||||
// 3) 备用:作用域上下文里取 conversation 服务再发
|
||||
try {
|
||||
const scoped = sessions?.scope?.(sessionId)
|
||||
const conv = scoped?.get?.('conversation')
|
||||
if (conv && typeof conv.send === 'function') { await conv.send(text); return }
|
||||
} catch { /* 落入最终报错 */ }
|
||||
throw new Error('no send path: session face unavailable')
|
||||
await sendHostSession(b, sessionId, text)
|
||||
}
|
||||
|
||||
/** 新建会话的分组选择:未分组 / 加入现有分组 / 新建一个分组(父目录 + 名称) */
|
||||
@@ -870,11 +807,11 @@ export async function createCustomSession(projectId: string, projectName: string
|
||||
let createOpts: any = {}
|
||||
if (workspaceId) createOpts = { workspaceId }
|
||||
else if (folder) createOpts = { cwd: folder }
|
||||
const sessionId = await b.sessions.create(createOpts)
|
||||
const sessionId = await createHostSession(b.sessions, b.workspaces, createOpts)
|
||||
await ensureSessionPreset(sessionId) // 新会话应用部署默认预设
|
||||
await ensureSessionModel(sessionId) // 修复继承失效 provider(selectModel 同步存默认,顺带修复后续新会话)
|
||||
markPluginSessionOpen(sessionId) // 插件发起的切换:不触发「切会话关项目」联动
|
||||
try { await b.sessions.open?.(sessionId) } catch {}
|
||||
try { await openHostSession(clientCtx, b.sessions, sessionId) } catch {}
|
||||
await promptIntoSession(sessionId, text)
|
||||
return sessionId
|
||||
}
|
||||
@@ -885,7 +822,7 @@ export async function sendCustomToSession(sessionId: string, projectId: string,
|
||||
if (!b) throw new Error('bridge unavailable')
|
||||
const text = buildWindowTaskText(projectId, projectName, windowLabel, requirement, folder, 'send')
|
||||
markPluginSessionOpen(sessionId) // 插件发起的切换:不触发「切会话关项目」联动
|
||||
try { await b.sessions.open?.(sessionId) } catch {}
|
||||
try { await openHostSession(clientCtx, b.sessions, sessionId) } catch {}
|
||||
await promptIntoSession(sessionId, text)
|
||||
}
|
||||
|
||||
@@ -915,7 +852,7 @@ const sessionScopeStore: {
|
||||
/** 完整会话快照(模块级):项目卡片的任务完成/待决提醒镜像数据源 */
|
||||
const sessionsSnapshotStore: { snapshot: any | null; listeners: Set<() => void> } = { snapshot: null, listeners: new Set() }
|
||||
|
||||
/** 提醒确认(ack)持久化:会话 id → 已确认的状态('done' | 'need') */
|
||||
/** 提醒确认:会话 id → done / 旧 need / need:不含正文的待决身份集合。 */
|
||||
function loadNotifyAck(): Record<string, string> {
|
||||
try {
|
||||
const raw = localStorage.getItem('dsh.worktable.notifyAck.v1')
|
||||
@@ -986,8 +923,8 @@ function buildMountContent(folder: string, d: any): any {
|
||||
return { kind: 'iframe', url: '/api/worktable/site/' + encodeURIComponent(dir) + '/' + encodeURIComponent(name), title: name }
|
||||
}
|
||||
|
||||
/** 每会话最近一次观察到的「需要判断」布尔值:状态转移时清除旧 ack(新一轮待决重新点亮) */
|
||||
const notifyStateSeenRef: { current: Record<string, boolean> } = { current: {} }
|
||||
/** 每会话最近观察到的待决身份;同为 need 的问题替换也必须重新点亮。 */
|
||||
const notifyStateSeenRef: { current: Record<string, string | null> } = { current: {} }
|
||||
/** 每会话最近观察到的「已完成」布尔值:新一次完成时清除旧 done ack(控制室绿光重新点亮) */
|
||||
const doneSeenRef: { current: Record<string, boolean> } = { current: {} }
|
||||
/** 清除某会话的 ack 记录(状态转移时调用,保证新问题不会被旧确认压住) */
|
||||
@@ -1010,10 +947,18 @@ function sessionNotifyState(entry: any): 'done' | 'need' | null {
|
||||
return null
|
||||
}
|
||||
|
||||
let sessionStatusSource: any = null
|
||||
let sessionConversationSource: any = null
|
||||
let adaptSessionSnapshot = createSessionSnapshotAdapter()
|
||||
function pendingAckForSession(sid: string): string | null {
|
||||
let face: any
|
||||
try { face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.() } catch {}
|
||||
return pendingAckOf(sessionsSnapshotStore.snapshot, sid, face)
|
||||
}
|
||||
function syncSessionScope(list: any) {
|
||||
try {
|
||||
const snap = list.getSnapshot()
|
||||
const current: string = snap?.current ?? ''
|
||||
const snap = adaptSessionSnapshot(list.getSnapshot(), sessionStatusSource)
|
||||
const current = currentSessionOf(snap)
|
||||
const entry = snap?.byId?.[current] ?? snap?.items?.find((it: any) => it.sessionId === current) ?? null ?? null
|
||||
const cat = snap?.subagentsByParent?.[current]
|
||||
let subagents: any[] = []
|
||||
@@ -1249,32 +1194,14 @@ function WorktableSection(props: any) {
|
||||
const pr = projectsRef.current
|
||||
const snap = sessionsSnapshotStore.snapshot
|
||||
const byId: Record<string, any> = snap?.byId ?? {}
|
||||
const jobsMap: Record<string, any[]> = snap?.jobsBySession ?? {}
|
||||
const now = Date.now()
|
||||
const kidsSetOf = (sid: string): Set<string> => {
|
||||
const set = new Set<string>()
|
||||
for (const [cid, ce] of Object.entries<any>(byId)) if (ce?.parentId === sid) set.add(cid)
|
||||
const v = (snap?.subagentsByParent ?? {})[sid]
|
||||
const arr = Array.isArray(v) ? v : (v?.entries ?? v?.items ?? [])
|
||||
if (Array.isArray(arr)) arr.forEach((c: any) => {
|
||||
const cid = c?.sessionId ?? c?.id
|
||||
if (typeof cid === 'string') set.add(cid)
|
||||
})
|
||||
return set
|
||||
}
|
||||
const kidsSetOf = (sid: string) => childSessionIdsOf(snap, sid)
|
||||
// 三态判定(同卡片提醒逻辑,但不过滤 ack——监控室永远显示事实状态)
|
||||
const statusOf = (sid: string | undefined): ConsoleCardData['status'] => {
|
||||
if (!sid) return 'idle'
|
||||
const e = byId[sid]
|
||||
if (!e) return 'idle'
|
||||
if (sessionNotifyState(e) === 'need') return 'need'
|
||||
for (const cid of kidsSetOf(sid)) {
|
||||
if (byId[cid] && sessionNotifyState(byId[cid]) === 'need') return 'need'
|
||||
}
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
|
||||
if (Array.isArray(face?.pending) && face.pending.length > 0) return 'need'
|
||||
} catch {}
|
||||
if (pendingAckForSession(sid) != null) return 'need'
|
||||
if (e.completed === true) return 'done'
|
||||
// 会话面兜底:宿主快照可能滞后或缺少完成字段
|
||||
try {
|
||||
@@ -1284,39 +1211,17 @@ function WorktableSection(props: any) {
|
||||
if (e.running === true) return 'busy'
|
||||
return 'idle'
|
||||
}
|
||||
// 运行时长:本会话正在运行的后台任务最早 startedAt 起算;无任务时读会话面 turnTimings 未结束轮次
|
||||
// 运行时长:任务 startedAt → 新版 chat legacy.turnTimings → 旧会话面;未知不虚构。
|
||||
const runtimeOf = (sid: string | undefined): number | null => {
|
||||
if (!sid) return null
|
||||
const e = byId[sid]
|
||||
if (!e || e.running !== true) return null
|
||||
let start: number | null = null
|
||||
const jobs = jobsMap[sid] ?? []
|
||||
for (const j of jobs) {
|
||||
if (j?.status === 'running' && typeof j.startedAt === 'number' && (start == null || j.startedAt < start)) start = j.startedAt
|
||||
}
|
||||
if (start != null) return Math.max(0, now - start)
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
|
||||
const timings = face?.turnTimings
|
||||
if (timings instanceof Map) {
|
||||
for (const t of Array.from(timings.values()).reverse()) {
|
||||
if (t && typeof t.startTime === 'number' && t.endTime == null) return Math.max(0, now - t.startTime)
|
||||
}
|
||||
} else if (timings && typeof timings === 'object') {
|
||||
for (const k of Object.keys(timings).reverse()) {
|
||||
const t = (timings as any)[k]
|
||||
if (t && typeof t.startTime === 'number' && t.endTime == null) return Math.max(0, now - t.startTime)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return null
|
||||
return sessionRuntimeMs(snap, sid, sessionBridge?.sessions, sessionConversationSource, now)
|
||||
}
|
||||
const ackMap = loadNotifyAck()
|
||||
const make = (id: string, name: string, icon: string, self: boolean): ConsoleCardData => {
|
||||
const sid = pr.projects.bindings[id]
|
||||
const status = statusOf(sid)
|
||||
// 发光 = 完成/待决且本轮未被确认;点卡片确认后熄灭
|
||||
const glow = !!sid && ((status === 'done' && ackMap[sid] !== 'done') || (status === 'need' && ackMap[sid] !== 'need'))
|
||||
const glow = !!sid && ((status === 'done' && ackMap[sid] !== 'done') || (status === 'need' && ackMap[sid] !== pendingAckForSession(sid)))
|
||||
return {
|
||||
id, name, icon,
|
||||
status,
|
||||
@@ -1361,6 +1266,7 @@ function WorktableSection(props: any) {
|
||||
// 崩溃),改为 apply 里订阅 ctx.sessions.list 后写入模块级 store,此处直接读取。
|
||||
useEffect(() => {
|
||||
const env = {
|
||||
fillHostInput: (text: string) => appendHostInput(sessionBridge, text),
|
||||
getScope: () => {
|
||||
const s = sessionScopeStore.snapshot
|
||||
return s ? { sessionId: s.sessionId, cwd: s.cwd } : null
|
||||
@@ -1481,14 +1387,14 @@ function WorktableSection(props: any) {
|
||||
else {
|
||||
// 入驻项目无视图覆盖:仅切换其绑定对话(对齐卡片自带打开行为)
|
||||
const bound = pr.bindings[id]
|
||||
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
|
||||
if (bound) { try { openHostSession(clientCtx, sessionBridge?.sessions, bound) } catch {} }
|
||||
}
|
||||
},
|
||||
onJump: (id) => {
|
||||
const sid = projectsRef.current.projects.bindings[id]
|
||||
if (!sid) return
|
||||
markPluginSessionOpen(sid) // 插件发起的切换:不触发「切会话关项目」联动
|
||||
try { sessionBridge?.sessions?.open?.(sid) } catch {}
|
||||
try { openHostSession(clientCtx, sessionBridge?.sessions, sid) } catch {}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1537,7 +1443,7 @@ function WorktableSection(props: any) {
|
||||
if (!splitStore.active) {
|
||||
if (!suppressRestoreRef.current) {
|
||||
const prev = projectAttachRef.sessionId
|
||||
if (prev) { try { sessionBridge?.sessions?.open?.(prev) } catch {} }
|
||||
if (prev) { try { openHostSession(clientCtx, sessionBridge?.sessions, prev) } catch {} }
|
||||
}
|
||||
projectAttachRef.sessionId = null
|
||||
projectAttachRef.attached = null
|
||||
@@ -1722,11 +1628,11 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
if (splitStore.active && splitStore.spec?.id === spec.id) {
|
||||
// 记录打开前会话(关项目时回切)与该项目「归属会话」(切到别的会话 = 自动关项目)
|
||||
let prev: string | null = null
|
||||
try { prev = sessionBridge?.list?.getSnapshot?.()?.current ?? null } catch {}
|
||||
try { prev = currentSessionOf(sessionBridge?.list?.getSnapshot?.()) || null } catch {}
|
||||
projectAttachRef.sessionId = prev
|
||||
projectAttachRef.attached = projectsRef.current.projects.bindings[spec.id] ?? prev
|
||||
const bound = projectsRef.current.projects.bindings[spec.id]
|
||||
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
|
||||
if (bound) { try { openHostSession(clientCtx, sessionBridge?.sessions, bound) } catch {} }
|
||||
ackProjectNotify(spec.id)
|
||||
// 补挂:此前项目未打开时暂存的产物(entries = 多窗口挂载列表),现在自动挂进各目标窗格;
|
||||
// 全部落位成功记录指纹(供自愈扫挂去重)
|
||||
@@ -1760,14 +1666,14 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
splitStore.open(spec)
|
||||
if (splitStore.active && splitStore.spec?.id === CONSOLE_ID) {
|
||||
let prev: string | null = null
|
||||
try { prev = sessionBridge?.list?.getSnapshot?.()?.current ?? null } catch {}
|
||||
try { prev = currentSessionOf(sessionBridge?.list?.getSnapshot?.()) || null } catch {}
|
||||
projectAttachRef.sessionId = prev
|
||||
const bound = explicitBound !== undefined ? explicitBound : projectsRef.current.projects.bindings[CONSOLE_ID]
|
||||
projectAttachRef.attached = bound ?? prev
|
||||
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
|
||||
if (bound) { try { openHostSession(clientCtx, sessionBridge?.sessions, bound) } catch {} }
|
||||
ackProjectNotify(CONSOLE_ID)
|
||||
try { notifyConsole() } catch {}
|
||||
// 打开即预热所有绑定会话的最近消息(冷会话走 history 只读通道)
|
||||
// 打开即预热所有绑定会话的最近消息(新宿主临时持有,旧宿主 history)
|
||||
try { sweepPreviews() } catch {}
|
||||
}
|
||||
}, [projects.views, t])
|
||||
@@ -1819,7 +1725,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
let createOpts: any = {}
|
||||
if (workspaceId) createOpts = { workspaceId }
|
||||
else if (folder) createOpts = { cwd: folder }
|
||||
const sessionId = await b.sessions.create(createOpts)
|
||||
const sessionId = await createHostSession(b.sessions, b.workspaces, createOpts)
|
||||
await ensureSessionPreset(sessionId) // 管理对话同样应用部署默认预设
|
||||
await ensureSessionModel(sessionId) // 管理对话同样修复失效 provider
|
||||
markPluginSessionOpen(sessionId)
|
||||
@@ -2095,20 +2001,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
|
||||
/** 收集某会话的子代理 id 集:byId 的 parentId 标注 + subagentsByParent 目录双通道 */
|
||||
const collectKids = useCallback((sid: string): Set<string> => {
|
||||
const kids = new Set<string>()
|
||||
const snap = sessionsSnapshotStore.snapshot
|
||||
const byId = snap?.byId ?? {}
|
||||
for (const [cid, ce] of Object.entries<any>(byId)) {
|
||||
if (ce?.parentId === sid) kids.add(cid)
|
||||
}
|
||||
const subMap = snap?.subagentsByParent ?? {}
|
||||
const v = (subMap as any)[sid]
|
||||
const arr = Array.isArray(v) ? v : (v?.entries ?? v?.items ?? [])
|
||||
if (Array.isArray(arr)) arr.forEach((c: any) => {
|
||||
const cid = c?.sessionId ?? c?.id
|
||||
if (typeof cid === 'string') kids.add(cid)
|
||||
})
|
||||
return kids
|
||||
return childSessionIdsOf(sessionsSnapshotStore.snapshot, sid)
|
||||
}, [])
|
||||
|
||||
/** 项目 → 提醒态:绑定会话(含其子代理)待决(黄) > 完成(绿) > 工作中(蓝);
|
||||
@@ -2121,26 +2014,13 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
for (const [pid, sid] of Object.entries(projects.bindings)) {
|
||||
const e = byId[sid]
|
||||
if (!e) continue
|
||||
// 是否需要判断:自身列表字段 / 子代理 / 会话面兜底,三通道聚合
|
||||
let needNow = sessionNotifyState(e) === 'need'
|
||||
if (!needNow) {
|
||||
for (const cid of collectKids(sid)) {
|
||||
const ce = byId[cid]
|
||||
if (ce && sessionNotifyState(ce) === 'need') { needNow = true; break }
|
||||
}
|
||||
}
|
||||
if (!needNow) {
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
|
||||
if (Array.isArray(face?.pending) && face.pending.length > 0) needNow = true
|
||||
} catch {}
|
||||
}
|
||||
const pendingAck = pendingAckForSession(sid)
|
||||
// 状态转移 → 清除旧 ack(新一轮待决重新点亮)
|
||||
if (needNow !== seen[sid]) {
|
||||
if (seen[sid] !== undefined) clearNotifyAck(sid)
|
||||
seen[sid] = needNow
|
||||
if (pendingAck !== seen[sid]) {
|
||||
if (seen[sid] !== undefined) { clearNotifyAck(sid); delete ack[sid] }
|
||||
seen[sid] = pendingAck
|
||||
}
|
||||
if (needNow) { if (ack[sid] !== 'need') map[pid] = 'need'; continue }
|
||||
if (pendingAck != null) { if (ack[sid] !== pendingAck) map[pid] = 'need'; continue }
|
||||
if (sessionNotifyState(e) === 'done' && ack[sid] !== 'done') { map[pid] = 'done'; continue }
|
||||
if (e.running === true) map[pid] = 'busy'
|
||||
}
|
||||
@@ -2152,17 +2032,14 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
const sid = projectsRef.current.projects.bindings[projectId]
|
||||
if (!sid) return
|
||||
const byId = sessionsSnapshotStore.snapshot?.byId ?? {}
|
||||
let needNow = sessionNotifyState(byId[sid]) === 'need'
|
||||
const pendingAck = pendingAckForSession(sid)
|
||||
for (const cid of collectKids(sid)) {
|
||||
if (sessionNotifyState(byId[cid]) === 'need') { needNow = true; saveNotifyAck(cid, 'need') }
|
||||
if (sessionNotifyState(byId[cid]) === 'need') {
|
||||
const childAck = pendingAckForSession(cid)
|
||||
if (childAck != null) { saveNotifyAck(cid, childAck); notifyStateSeenRef.current[cid] = childAck }
|
||||
}
|
||||
}
|
||||
if (!needNow) {
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
|
||||
if (Array.isArray(face?.pending) && face.pending.length > 0) needNow = true
|
||||
} catch {}
|
||||
}
|
||||
if (needNow) saveNotifyAck(sid, 'need')
|
||||
if (pendingAck != null) { saveNotifyAck(sid, pendingAck); notifyStateSeenRef.current[sid] = pendingAck }
|
||||
else {
|
||||
const st = sessionNotifyState(byId[sid])
|
||||
if (st === 'done') saveNotifyAck(sid, 'done')
|
||||
@@ -2543,7 +2420,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
}
|
||||
// 无视图覆盖:项目自带打开行为照旧,仅当绑定了会话时切换右侧对话窗
|
||||
const bound = projectsRef.current.projects.bindings[pid]
|
||||
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
|
||||
if (bound) { try { openHostSession(clientCtx, sessionBridge?.sessions, bound) } catch {} }
|
||||
ackProjectNotify(pid)
|
||||
}
|
||||
document.addEventListener('click', onDocClick, true)
|
||||
@@ -2655,7 +2532,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
if (view || layout) openSplit((view ?? layout) as LayoutSpec)
|
||||
else {
|
||||
const bound = pr.bindings[id]
|
||||
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
|
||||
if (bound) { try { openHostSession(clientCtx, sessionBridge?.sessions, bound) } catch {} }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3308,7 +3185,7 @@ export function apply(ctx: any) {
|
||||
clientCtx = ctx
|
||||
try { hostApi = ctx.get?.('connection')?.api ?? null } catch { hostApi = null }
|
||||
try { (window as any).__dshHostApi = hostApi } catch {}
|
||||
try { (window as any).__dshOpenSession = (id: string) => ctx.sessions?.open?.(id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text); (window as any).__dshWorkspaces = ctx.workspaces; (window as any).__dshBuildWindowTaskText = buildWindowTaskText; (window as any).__dshSyncSessionScope = () => syncSessionScope(sessionBridge?.list) } catch {}
|
||||
try { (window as any).__dshOpenSession = (id: string) => openHostSession(ctx, ctx.sessions, id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text); (window as any).__dshWorkspaces = ctx.workspaces; (window as any).__dshBuildWindowTaskText = buildWindowTaskText; (window as any).__dshSyncSessionScope = () => syncSessionScope(sessionBridge?.list) } catch {}
|
||||
|
||||
|
||||
ctx.effect(() => {
|
||||
@@ -3367,9 +3244,31 @@ export function apply(ctx: any) {
|
||||
// 会话作用域(当前会话 cwd 与后台任务)→ 功能窗数据源
|
||||
const sessionsList = ctx.sessions?.list
|
||||
if (sessionsList && typeof sessionsList.getSnapshot === 'function') {
|
||||
adaptSessionSnapshot = createSessionSnapshotAdapter()
|
||||
syncSessionScope(sessionsList)
|
||||
const disposeScope = sessionsList.subscribe(() => syncSessionScope(sessionsList))
|
||||
ctx.effect(() => disposeScope, 'dsh-worktable: session scope watch')
|
||||
// Optional dependency: older DSH hosts have no uiSession service.
|
||||
ctx.inject(['uiSession'], (statusCtx: any) => {
|
||||
const source = statusCtx.uiSession?.sessionStatus
|
||||
if (!source?.getSnapshot || !source?.subscribe) return
|
||||
sessionStatusSource = source
|
||||
syncSessionScope(sessionsList)
|
||||
const disposeStatus = source.subscribe(() => syncSessionScope(sessionsList))
|
||||
statusCtx.effect(() => () => {
|
||||
disposeStatus()
|
||||
if (sessionStatusSource === source) sessionStatusSource = null
|
||||
}, 'dsh-worktable: session status watch')
|
||||
})
|
||||
// Optional read-only timer source; getting a snapshot does not activate a chat target.
|
||||
ctx.inject(['uiConversation'], (conversationCtx: any) => {
|
||||
const source = conversationCtx.uiConversation
|
||||
if (typeof source?.binding !== 'function') return
|
||||
sessionConversationSource = source
|
||||
conversationCtx.effect(() => () => {
|
||||
if (sessionConversationSource === source) sessionConversationSource = null
|
||||
}, 'dsh-worktable: conversation timer source')
|
||||
})
|
||||
}
|
||||
|
||||
// 分栏工作区浮层(M1 通用引擎,shell.overlay 座位)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/** DSH 0.1 stores current on the list; DSH 0.2 uses mainView retention. */
|
||||
export function currentSessionOf(snapshot: any): string {
|
||||
if (typeof snapshot?.current === 'string') return snapshot.current
|
||||
const rows = Object.values(snapshot?.byId ?? {}) as any[]
|
||||
return rows.find((row) => (row?.retainedBy?.mainView ?? 0) > 0)?.id ?? ''
|
||||
}
|
||||
|
||||
/** Navigate through the host's UI owner, without modifying the host service. */
|
||||
export function openHostSession(ctx: any, sessions: any, id: string): any {
|
||||
// Cordis property access throws for an uninjected service; get() safely returns undefined.
|
||||
const ui = typeof ctx?.get === 'function' ? ctx.get('uiWorkspace') : ctx?.uiWorkspace
|
||||
if (typeof ui?.openSession === 'function') return ui.openSession(id)
|
||||
if (typeof sessions?.open === 'function') return sessions.open(id)
|
||||
throw new Error('Host session navigation unavailable')
|
||||
}
|
||||
|
||||
/** Own a temporary reference on 0.2; borrowing a cold binding is no longer sufficient. */
|
||||
export async function sendHostSession(bridge: any, id: string, text: string): Promise<void> {
|
||||
const sessions = bridge?.sessions
|
||||
const check = (result: any) => {
|
||||
if (result?.kind === 'error' || result?.ok === false) {
|
||||
throw new Error(result.text || result.error?.message || result.error?.code || 'Session submission rejected')
|
||||
}
|
||||
}
|
||||
const send = async (session: any) => {
|
||||
// Select one available transport BEFORE sending. A rejection/timeout must never retry
|
||||
// through a second transport: the first request may already have reached the host.
|
||||
if (session && typeof bridge?.conversation?.sendSession === 'function') {
|
||||
check(await bridge.conversation.sendSession(session, text, [], 'queue'))
|
||||
return
|
||||
}
|
||||
if (session && typeof session.prompt === 'function') {
|
||||
check(await session.prompt([{ type: 'text', text }], 'queue'))
|
||||
return
|
||||
}
|
||||
const conv = sessions?.scope?.(id)?.get?.('conversation')
|
||||
if (typeof conv?.send === 'function') { check(await conv.send(text)); return }
|
||||
throw new Error('no send path: session face unavailable')
|
||||
}
|
||||
if (typeof sessions?.using === 'function') {
|
||||
await sessions.using(id, { source: 'dshWorktable' }, async (ref: any) => {
|
||||
const binding = await ref.ready
|
||||
await send(binding.session)
|
||||
})
|
||||
return
|
||||
}
|
||||
let face: any = null
|
||||
for (let i = 0; i < 10; i++) {
|
||||
face = sessions?.binding?.(id)?.session
|
||||
if (face) break
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
}
|
||||
await send(face)
|
||||
}
|
||||
|
||||
/** Read-only adapter. A stopped run is not proof that a task succeeded. */
|
||||
export function createSessionSnapshotAdapter() {
|
||||
const previousRunning = new Map<string, boolean>()
|
||||
const stopped = new Set<string>()
|
||||
return (snapshot: any, statusSource?: any): any => {
|
||||
// Preserve the legacy contract verbatim; idle old sessions must not become completed.
|
||||
if (!snapshot || typeof snapshot.current === 'string') return snapshot
|
||||
const statuses = statusSource?.getSnapshot?.()
|
||||
const byId: Record<string, any> = {}
|
||||
const subagentsByParent = { ...snapshot.subagentsByParent }
|
||||
for (const [id, row] of Object.entries(snapshot.byId ?? {}) as [string, any][]) {
|
||||
const status = statuses?.get?.(id)
|
||||
const running = status?.running ?? row.running
|
||||
if (running === true) stopped.delete(id)
|
||||
else if (running === false && previousRunning.get(id) === true) stopped.add(id)
|
||||
if (typeof running === 'boolean') previousRunning.set(id, running)
|
||||
byId[id] = {
|
||||
...row, running,
|
||||
pendingInteraction: status ? status.pendingInteraction : row.pendingInteraction,
|
||||
// completionUnread clears when opening the main view. Keep a witnessed run-stop
|
||||
// until the next run so an active project's widget-result handshake still runs.
|
||||
completed: running !== true && (stopped.has(id) || status?.completionUnread === true || row.completed === true),
|
||||
}
|
||||
const children = snapshot.projectionsBySession?.[id]?.values?.subagentCatalog ?? row.projectionValues?.subagentCatalog
|
||||
if (Array.isArray(children)) subagentsByParent[id] = children
|
||||
}
|
||||
if (snapshot.phase === 'ready') {
|
||||
for (const id of previousRunning.keys()) if (!byId[id]) { previousRunning.delete(id); stopped.delete(id) }
|
||||
}
|
||||
return { ...snapshot, byId, subagentsByParent }
|
||||
}
|
||||
}
|
||||
|
||||
/** Parent links and the host's child catalog can each contain otherwise missing children. */
|
||||
export function childSessionIdsOf(snapshot: any, sid: string): Set<string> {
|
||||
const children = new Set<string>()
|
||||
for (const [id, row] of Object.entries(snapshot?.byId ?? {}) as [string, any][]) {
|
||||
if (id !== sid && row?.parentId === sid) children.add(id)
|
||||
}
|
||||
const catalog = snapshot?.subagentsByParent?.[sid]
|
||||
const rows = Array.isArray(catalog) ? catalog : (catalog?.entries ?? catalog?.items ?? [])
|
||||
if (Array.isArray(rows)) for (const row of rows) {
|
||||
const id = row?.sessionId ?? row?.id
|
||||
if (typeof id === 'string' && id !== sid) children.add(id)
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
/** Persist only opaque interaction identities, never their questions or answers.
|
||||
* Legacy hosts with no interaction key keep the old boolean-state acknowledgement.
|
||||
* New hosts must not let acknowledgement of question A suppress replacement B.
|
||||
*/
|
||||
export function pendingAckOf(snapshot: any, sid: string, legacyFace?: any): string | null {
|
||||
const identities: string[] = []
|
||||
let identified = false
|
||||
const add = (id: string, pending: any) => {
|
||||
if (pending == null) return
|
||||
const key = typeof pending?.key === 'string' ? pending.key : ''
|
||||
if (key) identified = true
|
||||
identities.push(JSON.stringify([id, String(pending?.kind ?? ''), key]))
|
||||
}
|
||||
const ids = [sid, ...childSessionIdsOf(snapshot, sid)]
|
||||
for (const id of ids) add(id, snapshot?.byId?.[id]?.pendingInteraction)
|
||||
// Read the old face only when the parent list has not already supplied its pending state.
|
||||
if (snapshot?.byId?.[sid]?.pendingInteraction == null && Array.isArray(legacyFace?.pending)) {
|
||||
for (const pending of legacyFace.pending) add(sid, pending)
|
||||
}
|
||||
if (!identities.length) return null
|
||||
return identified ? 'need:' + JSON.stringify([...new Set(identities)].sort()) : 'need'
|
||||
}
|
||||
|
||||
/** Read host memory only. Do not activate a chat target or retain a cold session to get a timer. */
|
||||
export function sessionRuntimeMs(snapshot: any, sid: string, sessions: any, uiConversation?: any, now = Date.now()): number | null {
|
||||
if (snapshot?.byId?.[sid]?.running !== true) return null
|
||||
const validStart = (value: any): value is number => typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
let start: number | null = null
|
||||
for (const job of snapshot?.jobsBySession?.[sid] ?? []) {
|
||||
if (job?.status === 'running' && validStart(job.startedAt) && (start == null || job.startedAt < start)) start = job.startedAt
|
||||
}
|
||||
if (start != null) return Math.max(0, now - start)
|
||||
|
||||
let timings: any
|
||||
try { timings = uiConversation?.binding?.(sid)?.target?.('chat')?.getSnapshot?.()?.legacy?.turnTimings } catch {}
|
||||
if (timings == null) {
|
||||
try { timings = sessions?.binding?.(sid)?.session?.getSnapshot?.()?.turnTimings } catch {}
|
||||
}
|
||||
// ReadonlyMap/another realm's Map must work as well as the old object representation.
|
||||
const turns: any[] = typeof timings?.values === 'function'
|
||||
? Array.from(timings.values()) : Object.values(timings ?? {})
|
||||
for (const turn of turns) {
|
||||
if (turn?.endTime == null && validStart(turn?.startTime) && (start == null || turn.startTime > start)) start = turn.startTime
|
||||
}
|
||||
return start == null ? null : Math.max(0, now - start)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/** Text-only preview. Never returns reasoning, tool arguments, or modifies stored messages. */
|
||||
export function cleanPreviewText(raw: string): string {
|
||||
return String(raw ?? '')
|
||||
.replace(/```[a-zA-Z0-9_+-]*[\s\S]*?```/g, ' ')
|
||||
.replace(/```[a-zA-Z0-9_+-]*[\s\S]*$/g, ' ')
|
||||
.replace(/`[^`\n]{1,200}`/g, ' ')
|
||||
.replace(/```/g, ' ')
|
||||
.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
export function previewFromEvents(entries: any): string {
|
||||
if (!Array.isArray(entries)) return ''
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const ev = entries[i]?.event
|
||||
if (ev?.type !== 'user/message' && ev?.type !== 'assistant/message') continue
|
||||
const data = ev.type === 'assistant/message' ? ev.data?.message ?? ev.data : ev.data
|
||||
const blocks = data?.content ?? data?.blocks
|
||||
if (!Array.isArray(blocks)) continue
|
||||
const raw = blocks.filter((b: any) => b?.type === 'text' && typeof b.text === 'string').map((b: any) => b.text).join('\n')
|
||||
const text = cleanPreviewText(raw)
|
||||
if (text.length >= 8) return text.slice(0, 220)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export async function readSessionPreview(sessions: any, id: string): Promise<string> {
|
||||
if (typeof sessions?.using === 'function') {
|
||||
return sessions.using(id, { source: 'dshWorktablePreview' }, async (ref: any) => {
|
||||
const binding = await ref.ready
|
||||
return previewFromEvents(binding.eventSource?.getSnapshot?.()?.entries)
|
||||
})
|
||||
}
|
||||
const face = sessions?.binding?.(id)?.session
|
||||
if (typeof face?.history !== 'function') return ''
|
||||
const result = await face.history({ maxMessages: 6 })
|
||||
return previewFromEvents(result?.result?.value?.events)
|
||||
}
|
||||
|
||||
/** Normalize only the transport envelope; preserve the existing selection policy. */
|
||||
export function presetApiOf(ctx: any, legacy: any): any {
|
||||
const api = typeof ctx?.get === 'function' ? ctx.get('remote.agentPresets') : ctx?.remote?.agentPresets
|
||||
if (typeof api?.list !== 'function' || typeof api?.select !== 'function') return legacy
|
||||
return {
|
||||
list: async () => ({ result: await api.list() }),
|
||||
select: async ({ sessionId, agentPreset }: any) => ({ result: await api.select(sessionId, agentPreset) }),
|
||||
}
|
||||
}
|
||||
|
||||
/** 0.2 model directories belong to retained scopes. Do not cache them after release. */
|
||||
export function modelApiOf(ctx: any, sessions: any, legacy: any): any {
|
||||
if (typeof sessions?.using !== 'function') return legacy
|
||||
const resolver = typeof ctx?.get === 'function' ? ctx.get('modelDirectories') : ctx?.modelDirectories
|
||||
if (typeof resolver?.directoryFor !== 'function') return legacy
|
||||
const withDirectory = (id: string, operation: (directory: any) => Promise<any>) =>
|
||||
sessions.using(id, { source: 'dshWorktableModel' }, async (ref: any) => {
|
||||
await ref.ready
|
||||
return operation(resolver.directoryFor(id))
|
||||
})
|
||||
return {
|
||||
models: async ({ sessionId }: any) => withDirectory(sessionId, async (directory) => ({ result: { ok: true, value: await directory.load() } })),
|
||||
selectModel: async ({ sessionId, ...selection }: any) => withDirectory(sessionId, async (directory) => ({ result: await directory.select(selection) })),
|
||||
}
|
||||
}
|
||||
|
||||
/** 0.2's blank composer requires a registered Workspace, not only a Session cwd. */
|
||||
export async function createHostSession(sessions: any, workspaces: any, options: { workspaceId?: string; cwd?: string; sessionId?: string } = {}): Promise<string> {
|
||||
if (options.workspaceId || typeof sessions?.using !== 'function') return sessions.create(options)
|
||||
// The Host owns path canonicalization and idempotent registration. Never invent an id.
|
||||
const workspace = options.cwd
|
||||
? await workspaces?.create?.({ path: options.cwd })
|
||||
: await workspaces?.initializeDefault?.()
|
||||
if (typeof workspace?.workspaceId !== 'string' || !workspace.workspaceId) throw new Error('workspace unavailable for new session')
|
||||
const { cwd: _cwd, ...rest } = options
|
||||
return sessions.create({ ...rest, workspaceId: workspace.workspaceId })
|
||||
}
|
||||
@@ -220,6 +220,7 @@ export type ConsoleCardData = {
|
||||
}
|
||||
|
||||
type SplitEnv = {
|
||||
fillHostInput: (text: string) => boolean
|
||||
getScope: () => SplitScope | null
|
||||
getJobs: () => SplitJob[]
|
||||
getSubagents: () => any[]
|
||||
@@ -504,25 +505,7 @@ async function copyTextSafe(text: string): Promise<boolean> {
|
||||
|
||||
/** 注入宿主对话框输入框(不发送);失败返回 false */
|
||||
function fillHostInput(text: string): boolean {
|
||||
try {
|
||||
const ta = document.querySelector<HTMLTextAreaElement>('textarea[data-phase]')
|
||||
?? Array.from(document.querySelectorAll<HTMLTextAreaElement>('textarea')).pop()
|
||||
if (!ta) return false
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set
|
||||
// 连续标注:已有内容不顶掉,换行追加(用户可以同一对话连续标注多处)
|
||||
const next = ta.value && ta.value.trim() ? ta.value + '\n\n' + text : text
|
||||
if (setter) setter.call(ta, next)
|
||||
else ta.value = next
|
||||
ta.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
try {
|
||||
ta.focus()
|
||||
ta.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
ta.setSelectionRange(ta.value.length, ta.value.length)
|
||||
} catch {}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return splitEnv?.fillHostInput(text) ?? false
|
||||
}
|
||||
|
||||
/** ✓ 确认:打包并注入,不发送 */
|
||||
|
||||
Reference in New Issue
Block a user