dsh-decision-split
Advanced tools
+57
-166
@@ -18,17 +18,4 @@ window.__ModuleLoader__.load({ | ||
| ".ddec-side-empty{color:var(--dsw-alias-label-tertiary);padding:8px 0;font-size:13px;line-height:20px}", | ||
| ".ddec-card{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:12px 16px;margin:8px 0}", | ||
| ".ddec-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}", | ||
| ".ddec-card-title{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}", | ||
| ".ddec-summary{max-height:340px;overflow:auto;margin-bottom:10px}", | ||
| ".ddec-md.ddec-empty-text{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}", | ||
| ".ddec-textarea{width:100%;min-height:72px;resize:vertical;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 12px;font-family:var(--dsw-font-family);font-size:14px;line-height:22px}", | ||
| ".ddec-textarea::placeholder{color:var(--dsw-alias-label-tertiary)}", | ||
| ".ddec-textarea:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}", | ||
| ".ddec-foot{display:flex;justify-content:flex-end;margin-top:10px}", | ||
| ".ddec-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;border:none;border-radius:14px;cursor:pointer;font-size:12px;line-height:18px;padding:0 10px;height:28px;font-family:var(--dsw-font-family)}", | ||
| ".ddec-btn-primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}", | ||
| ".ddec-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}", | ||
| ".ddec-btn-ghost{background:transparent;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}", | ||
| ".ddec-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}", | ||
| ".ddec-btn:disabled{cursor:not-allowed;opacity:.4}", | ||
| ".ddec-open{display:inline-flex;align-items:center;justify-content:center;gap:4px;color:var(--dsw-alias-label-secondary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;cursor:pointer;font-size:12px;line-height:18px;padding:0 10px;height:28px;font-family:var(--dsw-font-family);margin-bottom:6px}", | ||
| ".ddec-open:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}", | ||
| ".ddec-md{min-width:0;overflow-wrap:anywhere;font:var(--dsw-font-markdown-base);color:var(--dsw-alias-label-primary)}", | ||
@@ -72,4 +59,2 @@ ".ddec-md strong{font-weight:600}", | ||
| var MARKER = /^(#{1,6})\s+(决策摘要|决策要点|待决策清单|决策清单|决策事项|方案对比|方案选择|需要决策|待决策|待确认|待拍板|请你决定|请拍板|决策项|决策点|Decision\s+Summary|Decision\s+Points|Decision\s+Required|Decision\s+Needed|Please\s+Decide|Please\s+Choose|Need\s+Your\s+Decision|Options|Choices)[::]?\s*$/im; | ||
| function textOf(node) { | ||
@@ -84,16 +69,2 @@ if (node === undefined || node === null || !Array.isArray(node.blocks)) return ""; | ||
| function summaryOf(text) { | ||
| var m = text.match(MARKER); | ||
| if (m === null) return null; | ||
| var level = m[1].length; | ||
| var lines = text.slice(m.index).split("\n"); | ||
| var kept = []; | ||
| for (var i = 1; i < lines.length; i++) { | ||
| var hh = lines[i].match(/^(#{1,6})\s/); | ||
| if (hh && hh[1].length <= level) break; | ||
| kept.push(lines[i]); | ||
| } | ||
| return kept.join("\n").trim(); | ||
| } | ||
| function titleOf(text) { | ||
@@ -112,81 +83,2 @@ var lines = text.replace(/\r\n?/g, "\n").split("\n"); | ||
| // 语义判定:只有内容确实呈现「多个决策点」或「多个可选方案」时才触发, | ||
| // 而不是看标题存在与否,也不是看内容长短。判定只依赖内容本身。 | ||
| function hasCompareTable(text) { | ||
| var lines = text.split("\n"); | ||
| for (var i = 0; i < lines.length - 1; i++) { | ||
| if (/^\s*\|[^\n]+\|\s*$/.test(lines[i]) && /^\s*\|?[\s:\-|]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].indexOf("-") !== -1) { | ||
| if (!/方案|选项|备选|对比|选择/.test(lines[i])) continue; | ||
| var rows = 0; | ||
| for (var j = i + 2; j < lines.length; j++) { | ||
| var row = lines[j].trim(); | ||
| if (row === "" || row.charAt(0) !== "|") break; | ||
| rows++; | ||
| } | ||
| if (rows >= 2) return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| function decisionPointCount(text) { | ||
| var m = text.match(/\*{0,2}\s*(?:决策|决策点|待决策|需决策|待确认)[^::]{0,6}[::]/g); | ||
| return m === null ? 0 : m.length; | ||
| } | ||
| function alternatives(text) { | ||
| var seen = {}; | ||
| var re = /(?:方案|选项|备选)\s*([A-Za-z一二三四五六七八九十0-9]+)/g; | ||
| var m; | ||
| while ((m = re.exec(text)) !== null) { | ||
| if (seen[m[0]] === undefined) seen[m[0]] = true; | ||
| } | ||
| return Object.keys(seen).length; | ||
| } | ||
| function needsDecision(text) { | ||
| if (typeof text !== "string") return false; | ||
| text = text.replace(/\r\n?/g, "\n"); | ||
| if (decisionPointCount(text) >= 2) return true; | ||
| if (alternatives(text) >= 2) return true; | ||
| if (hasCompareTable(text)) return true; | ||
| return false; | ||
| } | ||
| // 无决策段标题时,把命中的决策点/方案条目抽取成列表,便于逐个阅读拍板。 | ||
| function decisionSummary(text) { | ||
| text = text.replace(/\r\n?/g, "\n"); | ||
| var lines = text.split("\n"); | ||
| var out = []; | ||
| var seen = {}; | ||
| var re = /(?:方案|选项|备选|决策|决策点|待决策|待确认)\s*[A-Za-z一二三四五六七八九十0-9]+\s*[::]|(?:方案|选项|备选)\s*[A-Za-z一二三四五六七八九十0-9]+/; | ||
| for (var i = 0; i < lines.length; i++) { | ||
| var t = lines[i].replace(/\r$/, "").trim(); | ||
| if (t === "" || /^#{1,6}\s/.test(t) || /^```/.test(t) || t.charAt(0) === "|") continue; | ||
| var ln = t.replace(/^[-*+]?\s*/, ""); | ||
| if (re.test(ln) && seen[ln] === undefined) { | ||
| seen[ln] = true; | ||
| out.push("- " + ln); | ||
| } | ||
| } | ||
| return out.join("\n"); | ||
| } | ||
| function tocOf(text) { | ||
| text = text.replace(/\r\n?/g, "\n"); | ||
| var lines = text.split("\n"); | ||
| var toc = []; | ||
| for (var i = 0; i < lines.length; i++) { | ||
| var m = lines[i].match(/^(#{1,6})\s+(.*)$/); | ||
| if (m && m[2].trim() !== "") toc.push(m[2].trim()); | ||
| } | ||
| return toc; | ||
| } | ||
| function tocMarkdown(text) { | ||
| var toc = tocOf(text); | ||
| if (toc.length === 0) return ""; | ||
| return toc.map(function (t) { return "- " + t; }).join("\n"); | ||
| } | ||
| function escapeHtml(s) { | ||
@@ -283,2 +175,27 @@ return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | ||
| // 详情栏宽度适配(固化在本插件内,无需改动 harness): | ||
| // `ctx.layout` 只暴露 open/close/toggle,打开时默认 360px(harness 硬编码)。 | ||
| // 插件包装 openDetails:面板动作由 AppFrame 挂载时经 attachPanels 送达, | ||
| // 拿到后在其基础上把详情栏设为 480px(在 harness clamp [300,520] 内, | ||
| // 窄视口让步链由 store 自动处理,用户拖拽值不受影响)。 | ||
| // harness 接口若变化导致缺失,则静默回退为默认宽度。 | ||
| var DDEC_DETAILS_WIDTH = 480; | ||
| var ddecWired = null; | ||
| if (typeof layout === "object" && layout !== null) { | ||
| var ddecAttach = typeof layout.attachPanels === "function" ? layout.attachPanels.bind(layout) : null; | ||
| var ddecOpen = typeof layout.openDetails === "function" ? layout.openDetails.bind(layout) : null; | ||
| if (ddecAttach !== null && ddecOpen !== null) { | ||
| layout.attachPanels = function (actions) { | ||
| ddecWired = actions; | ||
| ddecAttach(actions); | ||
| }; | ||
| layout.openDetails = function () { | ||
| ddecOpen(); | ||
| if (ddecWired !== null && typeof ddecWired.setDetails === "function") { | ||
| ddecWired.setDetails(DDEC_DETAILS_WIDTH); | ||
| } | ||
| }; | ||
| } | ||
| } | ||
| var activeSeq = null; | ||
@@ -299,5 +216,4 @@ var listeners = new Set(); | ||
| function DecisionCard(props) { | ||
| var seq = props.seq; | ||
| var text = props.useSession(function (s) { | ||
| function textAt(useSession, seq) { | ||
| return useSession(function (s) { | ||
| if (s === undefined || s === null || !Array.isArray(s.nodes)) return ""; | ||
@@ -310,47 +226,26 @@ for (var i = 0; i < s.nodes.length; i++) { | ||
| }); | ||
| var summary = summaryOf(text); | ||
| var st = React.useState(""); | ||
| var draft = st[0]; | ||
| var setDraft = st[1]; | ||
| var hasSection = summary !== null && summary.trim() !== ""; | ||
| var isFallback = false; | ||
| // 触发条件:要么有 agent 主动写的决策段(内容非空),要么内容语义上呈现多个决策点/多个方案。 | ||
| if (!hasSection && !needsDecision(text)) return null; | ||
| if (!hasSection) { | ||
| summary = decisionSummary(text) || tocMarkdown(text); | ||
| isFallback = true; | ||
| } | ||
| } | ||
| function send() { | ||
| var body = draft.trim(); | ||
| if (body === "") return; | ||
| if (props.inputActions) { | ||
| props.inputActions.setDraft("【决策】\n" + body); | ||
| props.inputActions.submit(); | ||
| // 常驻入口:每条含文本的 agent 消息尾部显示「在侧栏查看全文」按钮。 | ||
| // 首次点击打开右侧详情栏展示该条消息的完整 markdown;侧栏正在展示 | ||
| // 该条消息时再次点击则收回。点击其它消息的按钮会切换展示目标。 | ||
| function OpenSidebarButton(props) { | ||
| var text = textAt(props.useSession, props.seq); | ||
| if (text.trim() === "") return null; | ||
| var shown = useActiveSeq() === props.seq; | ||
| function toggle() { | ||
| if (activeSeq === props.seq) { | ||
| setActiveSeq(null); | ||
| if (layout) layout.closeDetails(); | ||
| } else { | ||
| setActiveSeq(props.seq); | ||
| if (layout) layout.openDetails(); | ||
| } | ||
| setDraft(""); | ||
| } | ||
| function openSide() { | ||
| setActiveSeq(seq); | ||
| if (layout) layout.openDetails(); | ||
| } | ||
| return React.createElement("div", { className: "ddec-card" }, | ||
| React.createElement("div", { className: "ddec-card-head" }, | ||
| React.createElement("span", { className: "ddec-card-title" }, isFallback ? "决策分屏" : "决策摘要"), | ||
| React.createElement("button", { type: "button", className: "ddec-btn ddec-btn-ghost", onClick: openSide }, "侧栏查看全文") | ||
| ), | ||
| summary === "" | ||
| ? React.createElement("div", { className: "ddec-md ddec-summary ddec-empty-text" }, isFallback ? "(完整方案见侧栏:点右上角「侧栏查看全文」)" : "(该段暂未填写具体决策点)") | ||
| : React.createElement("div", { className: "ddec-md ddec-summary", dangerouslySetInnerHTML: { __html: renderMarkdown(summary) } }), | ||
| React.createElement("textarea", { | ||
| className: "ddec-textarea", | ||
| placeholder: "在此写下你的决策,点“发送决策”发给 agent", | ||
| value: draft, | ||
| onChange: function (e) { setDraft(e.target.value); } | ||
| }), | ||
| React.createElement("div", { className: "ddec-foot" }, | ||
| React.createElement("button", { type: "button", className: "ddec-btn ddec-btn-primary", onClick: send }, "发送决策") | ||
| ) | ||
| ); | ||
| return React.createElement("button", { | ||
| type: "button", | ||
| className: "ddec-open", | ||
| "aria-expanded": shown || undefined, | ||
| onClick: toggle | ||
| }, shown ? "收起侧栏" : "在侧栏查看全文"); | ||
| } | ||
@@ -368,12 +263,8 @@ | ||
| }); | ||
| var text = props.useSession(function (s) { | ||
| if (seq === null || s === undefined || s === null || !Array.isArray(s.nodes)) return ""; | ||
| for (var i = 0; i < s.nodes.length; i++) { | ||
| var n = s.nodes[i]; | ||
| if (n && n.kind === "assistant" && n.seq === seq) return textOf(n); | ||
| } | ||
| return ""; | ||
| }); | ||
| var text = textAt(props.useSession, seq); | ||
| var title = titleOf(text); | ||
| function close() { if (layout) layout.closeDetails(); } | ||
| function close() { | ||
| setActiveSeq(null); | ||
| if (layout) layout.closeDetails(); | ||
| } | ||
@@ -391,3 +282,3 @@ return React.createElement("div", { className: "ddec-side" }, | ||
| seq === null || !found | ||
| ? React.createElement("div", { className: "ddec-side-empty" }, "点击某条方案消息下方的「侧栏查看全文」,完整内容会显示在这里。") | ||
| ? React.createElement("div", { className: "ddec-side-empty" }, "点击某条消息下方的「在侧栏查看全文」,完整内容会显示在这里。") | ||
| : React.createElement("div", { className: "ddec-md", dangerouslySetInnerHTML: { __html: renderMarkdown(text) } }) | ||
@@ -401,3 +292,3 @@ ) | ||
| { name: "conversation.chat.turnTail", priority: 1, select: function (owner) { return owner.seq; } }, | ||
| DecisionCard | ||
| OpenSidebarButton | ||
| ); | ||
@@ -404,0 +295,0 @@ }); |
+4
-9
@@ -1,9 +0,4 @@ | ||
| export const inject = ["systemPrompt"]; | ||
| export function apply(ctx) { | ||
| ctx.systemPrompt.section({ | ||
| name: "dsh-decision-split:decision-format", | ||
| order: 190, | ||
| text: '当你的回复需要用户就「多个决策点」或「多个可选方案」逐一作出选择时,请在回复末尾追加一个「## 决策摘要」段,用「**决策 N:**」(或「**方案 N:**」,N 从 1 开始)逐条列出每个待决定的点或方案。界面会自动据此弹出决策卡片,用户可在卡片里直接输入决策并回传给你。仅当确实存在需要用户拍板的多个决策点或多个备选方案时才添加;普通说明、无需用户决策的回复不要添加。', | ||
| }); | ||
| } | ||
| // Host half deliberately empty after simplification: the sidebar-only plugin | ||
| // needs no system-prompt injection. Decisions flow through the harness's | ||
| // native ask_user_question tool instead of a markdown "## 决策摘要" section. | ||
| export function apply() {} |
+2
-2
| { | ||
| "name": "dsh-decision-split", | ||
| "version": "0.1.9", | ||
| "description": "决策分屏:agent 输出含「决策摘要」段的方案时,主窗口显示摘要与决策编辑框,右侧详情栏独立展示完整 markdown,决策一键发回 agent。", | ||
| "version": "0.2.2", | ||
| "description": "决策分屏:agent 消息尾部提供「在侧栏查看全文」入口,右侧详情栏独立滚动展示该条消息的完整 markdown,边看全文边回主窗口操作。", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "main": "lib/index.js", |
+9
-21
| # dsh-decision-split | ||
| 决策分屏插件 —— DeepSeek Harness Web UI 的决策工作台。 | ||
| 决策分屏插件 —— DeepSeek Harness Web UI 的分屏阅读工作台。 | ||
| **解决什么痛点**:agent 输出长方案时,单一的滚动窗口让「通读全文」和「逐条拍板」互相干扰——看完后面忘了前面,对照多个决策点要反复上下滚动,决策只能靠脑子硬记。本插件把两者拆开:右侧详情栏独立滚动展示完整方案,主窗口固定显示决策摘要与决策输入框,边看全文边写决策、一键回传 agent。 | ||
| **解决什么痛点**:agent 输出长方案时,单一的滚动窗口让「通读全文」和「边看边操作」互相干扰——看完后面忘了前面,对照多处细节要反复上下滚动。本插件在消息尾部提供一个「在侧栏查看全文」入口,把该条 agent 消息的完整 markdown 放到右侧详情栏独立滚动展示,主窗口保持当前操作位置不变。 | ||
| 当 agent 输出包含「决策摘要」段(任意标题层级,如 `## 决策摘要`、`### 决策要点`)的方案时: | ||
| - **入口**:每条含文本的 agent 消息尾部固定显示「在侧栏查看全文」按钮; | ||
| - **右侧详情栏**:独立滚动展示该条消息的完整 markdown(含表格/代码块/行内 code),侧栏标题自动取消息首个标题;打开时默认以 480px 宽展示(插件内置适配:在 harness 面板宽度 clamp [300,520] 内改默认,窄视口让步链与用户拖拽均不受影响,无需改 harness 任何文件); | ||
| - **无输入框、无决策回传**:决策与提问统一交给 harness 原生的 `ask_user_question` 能力,本插件不再内嵌输入框,避免与原生输入框、原生决策卡片冲突。 | ||
| - **主窗口**:消息下方自动出现「决策摘要」卡片 —— 摘要内容 + 决策输入框 + 「侧栏查看全文」「发送决策」两个按钮; | ||
| - **右侧详情栏**:独立滚动展示该方案的完整 markdown,侧栏标题自动取方案首个标题; | ||
| - **发送决策**:决策以 `【决策】…` 前缀作为一条消息回传给 agent,让它基于你的决策继续推进。 | ||
| 视觉完全对齐 DSH Web UI 原生设计变量(`--dsw-*` tokens、MarkdownText 字体规范、原生按钮样式)。 | ||
@@ -19,3 +17,3 @@ | ||
| *主窗口的「决策摘要」卡片(摘要 + 决策输入框 + 操作按钮),以及右侧详情栏的完整方案展示。* | ||
| *消息尾部的「在侧栏查看全文」入口,以及右侧详情栏的完整方案展示。* | ||
@@ -33,3 +31,3 @@ ## 安装 | ||
| ```sh | ||
| dsh plugin --profile web add https://github.com/yuanbaoerer/dsh-decision-split/releases/latest/download/dsh-decision-split-0.1.6.tgz | ||
| dsh plugin --profile web add https://github.com/yuanbaoerer/dsh-decision-split/releases/latest/download/dsh-decision-split-0.2.2.tgz | ||
| ``` | ||
@@ -41,16 +39,6 @@ | ||
| 装上即用,无需额外配置。触发遵循一个核心原则:**只有当回复确实呈现「多个决策点」或「多个可选方案」、需要你逐个阅读逐个拍板时才出卡**,而不是看标题存不存在、也不是看内容长短。 | ||
| 装上即用,无需额外配置。每条含文本的 agent 消息尾部都会出现「在侧栏查看全文」按钮,点击即在右侧详情栏独立滚动阅读该条消息的完整内容;侧栏正在展示该条消息时再点一次按钮(按钮此时显示为「收起侧栏」)即收回;点击其它消息的按钮可切换展示目标,点侧栏右上角 × 也可收起。 | ||
| - **语义触发(客户端识别)**:回复内容满足以下任一条件即触发(纯说明性长文、普通问答不会误触发): | ||
| - **≥2 个决策点**:如 `**决策 1:**`、`**决策 2:**`、`决策点二:`、`待确认:` 等; | ||
| - **≥2 个方案/选项**:如 `方案 A`、`方案 一`、`选项 1`、`备选 2` 等; | ||
| - **对比类表格**:表头含 `方案/选项/备选/对比/选择` 且有 ≥2 行对比数据。 | ||
| - **决策段触发**:回复中出现 agent 主动写的非空决策段标题(任意层级 `#`–`######`、忽略大小写、允许结尾冒号)也触发,摘要区展示该段内容: | ||
| > 决策/提问请使用对话里的原生提问能力(agent 调用 `ask_user_question` 时界面会弹出可点选卡片),本插件专注「分屏阅读」这一件事。 | ||
| `决策摘要` · `决策要点` · `待决策清单` · `决策清单` · `决策事项` · `方案对比` · `方案选择` · `需要决策` · `待决策` · `待确认` · `待拍板` · `请你决定` · `请拍板` · `决策项` · `决策点` · `Decision Summary` · `Decision Points` · `Decision Required` · `Decision Needed` · `Please Decide` · `Please Choose` · `Need Your Decision` · `Options` · `Choices` | ||
| - **host 注入提示词**:插件会在系统提示中告知 agent——当回复包含需要你拍板的决策点、或给出多个方案让你选择时,用 `**决策 N:**` 逐条列出,以便即便 agent 不写标题、客户端也能按上面的语义规则识别出来。 | ||
| > 也可在对话里给 agent 补一句「输出方案时若需我拍板,请在最后用 `## 决策摘要` 段列出决策点」——手动约定与自动注入效果相同,属可选的加强。 | ||
| ## License | ||
@@ -57,0 +45,0 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
20690
-25.47%281
-27.58%48
-20%1
Infinity%