@roarpeng/graphflow 1.9.15 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +9 -8
  3. package/README.zh.md +3 -2
  4. package/dist/config/resolve.d.ts +13 -1
  5. package/dist/config/resolve.d.ts.map +1 -1
  6. package/dist/config/resolve.js +29 -3
  7. package/dist/config/resolve.js.map +1 -1
  8. package/dist/config/workspace-root.d.ts +6 -0
  9. package/dist/config/workspace-root.d.ts.map +1 -1
  10. package/dist/config/workspace-root.js +3 -2
  11. package/dist/config/workspace-root.js.map +1 -1
  12. package/dist/graph/graphify-file-client.d.ts +49 -0
  13. package/dist/graph/graphify-file-client.d.ts.map +1 -1
  14. package/dist/graph/graphify-file-client.js +113 -15
  15. package/dist/graph/graphify-file-client.js.map +1 -1
  16. package/dist/graph/knowledge-extraction.d.ts +43 -0
  17. package/dist/graph/knowledge-extraction.d.ts.map +1 -0
  18. package/dist/graph/knowledge-extraction.js +274 -0
  19. package/dist/graph/knowledge-extraction.js.map +1 -0
  20. package/dist/graph/token-savings.d.ts +39 -0
  21. package/dist/graph/token-savings.d.ts.map +1 -1
  22. package/dist/graph/token-savings.js +138 -0
  23. package/dist/graph/token-savings.js.map +1 -1
  24. package/dist/integrations/skill-installer.d.ts.map +1 -1
  25. package/dist/integrations/skill-installer.js +9 -2
  26. package/dist/integrations/skill-installer.js.map +1 -1
  27. package/dist/learning/canary-gate.d.ts +5 -1
  28. package/dist/learning/canary-gate.d.ts.map +1 -1
  29. package/dist/learning/canary-gate.js +5 -2
  30. package/dist/learning/canary-gate.js.map +1 -1
  31. package/dist/learning/dialogue-thread.d.ts +15 -0
  32. package/dist/learning/dialogue-thread.d.ts.map +1 -1
  33. package/dist/learning/dialogue-thread.js +58 -1
  34. package/dist/learning/dialogue-thread.js.map +1 -1
  35. package/dist/learning/episodic-memory.d.ts.map +1 -1
  36. package/dist/learning/episodic-memory.js +19 -3
  37. package/dist/learning/episodic-memory.js.map +1 -1
  38. package/dist/learning/skill-admission.d.ts +55 -10
  39. package/dist/learning/skill-admission.d.ts.map +1 -1
  40. package/dist/learning/skill-admission.js +177 -35
  41. package/dist/learning/skill-admission.js.map +1 -1
  42. package/dist/learning/skill-flywheel.d.ts +24 -6
  43. package/dist/learning/skill-flywheel.d.ts.map +1 -1
  44. package/dist/learning/skill-flywheel.js +99 -19
  45. package/dist/learning/skill-flywheel.js.map +1 -1
  46. package/dist/learning/skill-markdown.d.ts +12 -0
  47. package/dist/learning/skill-markdown.d.ts.map +1 -0
  48. package/dist/learning/skill-markdown.js +226 -0
  49. package/dist/learning/skill-markdown.js.map +1 -0
  50. package/dist/learning/skill-store.d.ts.map +1 -1
  51. package/dist/learning/skill-store.js +23 -4
  52. package/dist/learning/skill-store.js.map +1 -1
  53. package/dist/learning/skill-types.d.ts +10 -0
  54. package/dist/learning/skill-types.d.ts.map +1 -1
  55. package/dist/learning/skill-types.js.map +1 -1
  56. package/dist/learning/turn-distillation.d.ts +16 -0
  57. package/dist/learning/turn-distillation.d.ts.map +1 -0
  58. package/dist/learning/turn-distillation.js +161 -0
  59. package/dist/learning/turn-distillation.js.map +1 -0
  60. package/dist/learning/workflow-skill.d.ts +2 -0
  61. package/dist/learning/workflow-skill.d.ts.map +1 -1
  62. package/dist/learning/workflow-skill.js +15 -0
  63. package/dist/learning/workflow-skill.js.map +1 -1
  64. package/dist/surfaces/cli/index.js +108 -31
  65. package/dist/surfaces/cli/index.js.map +1 -1
  66. package/dist/surfaces/cli/output.d.ts.map +1 -1
  67. package/dist/surfaces/cli/output.js +4 -0
  68. package/dist/surfaces/cli/output.js.map +1 -1
  69. package/dist/surfaces/cli/runtime/dialogue.d.ts +36 -0
  70. package/dist/surfaces/cli/runtime/dialogue.d.ts.map +1 -1
  71. package/dist/surfaces/cli/runtime/dialogue.js +80 -2
  72. package/dist/surfaces/cli/runtime/dialogue.js.map +1 -1
  73. package/dist/surfaces/cli/runtime/graph.d.ts.map +1 -1
  74. package/dist/surfaces/cli/runtime/graph.js +14 -10
  75. package/dist/surfaces/cli/runtime/graph.js.map +1 -1
  76. package/dist/surfaces/cli/runtime/knowledge.d.ts +37 -0
  77. package/dist/surfaces/cli/runtime/knowledge.d.ts.map +1 -0
  78. package/dist/surfaces/cli/runtime/knowledge.js +162 -0
  79. package/dist/surfaces/cli/runtime/knowledge.js.map +1 -0
  80. package/dist/surfaces/cli/runtime/routing.js +3 -3
  81. package/dist/surfaces/cli/runtime/routing.js.map +1 -1
  82. package/dist/surfaces/cli/runtime/types.d.ts +6 -0
  83. package/dist/surfaces/cli/runtime/types.d.ts.map +1 -1
  84. package/dist/surfaces/cli/runtime.d.ts +2 -1
  85. package/dist/surfaces/cli/runtime.d.ts.map +1 -1
  86. package/dist/surfaces/cli/runtime.js +7 -1
  87. package/dist/surfaces/cli/runtime.js.map +1 -1
  88. package/dist/surfaces/mcp/server.d.ts +24 -0
  89. package/dist/surfaces/mcp/server.d.ts.map +1 -1
  90. package/dist/surfaces/mcp/server.js +265 -1
  91. package/dist/surfaces/mcp/server.js.map +1 -1
  92. package/dist/surfaces/mcp/tool-definitions.d.ts +1 -0
  93. package/dist/surfaces/mcp/tool-definitions.d.ts.map +1 -1
  94. package/dist/surfaces/mcp/tool-definitions.js +9 -1
  95. package/dist/surfaces/mcp/tool-definitions.js.map +1 -1
  96. package/dist/surfaces/mcp/tool-handlers.d.ts +4 -1
  97. package/dist/surfaces/mcp/tool-handlers.d.ts.map +1 -1
  98. package/dist/surfaces/mcp/tool-handlers.js +69 -19
  99. package/dist/surfaces/mcp/tool-handlers.js.map +1 -1
  100. package/dsh/client.js +431 -0
  101. package/dsh/plugin.mjs +927 -2
  102. package/package.json +14 -3
  103. package/plugin.json +1 -1
  104. package/web/README.md +36 -0
  105. package/web/plugin.mjs +448 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roarpeng/graphflow",
3
- "version": "1.9.15",
3
+ "version": "1.12.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -12,6 +12,7 @@
12
12
  "mcp.json",
13
13
  "cordis.patch.yml",
14
14
  "dsh",
15
+ "web",
15
16
  "skills",
16
17
  "src/surfaces/trae-skill",
17
18
  "src/surfaces/trae-rules",
@@ -39,6 +40,9 @@
39
40
  "default": "./dist/index.js"
40
41
  },
41
42
  "./dsh": "./dsh/plugin.mjs",
43
+ "./dsh/package.json": "./package.json",
44
+ "./web": "./web/plugin.mjs",
45
+ "./client": "./dsh/client.js",
42
46
  "./package.json": "./package.json"
43
47
  },
44
48
  "bin": {
@@ -104,6 +108,13 @@
104
108
  "dsh": {
105
109
  "bundle": {
106
110
  "patch": "./cordis.patch.yml"
111
+ },
112
+ "client": {
113
+ "platform": "web",
114
+ "inject": [
115
+ "@deepseek-ai/dsh-client-runtime",
116
+ "@deepseek-ai/dsh-client-connection"
117
+ ]
107
118
  }
108
119
  },
109
120
  "author": "Roarpeng",
@@ -123,16 +134,16 @@
123
134
  "eslint": "^10.4.0",
124
135
  "husky": "^9.1.7",
125
136
  "lint-staged": "^16.4.0",
137
+ "pino-pretty": "^13.1.3",
126
138
  "prettier": "^3.8.3",
127
139
  "tree-sitter-wasms": "^0.1.13",
128
140
  "tsx": "^4.22.3",
129
141
  "typescript": "^6.0.3",
130
- "pino-pretty": "^13.1.3",
131
142
  "vitest": "^3.2.6"
132
143
  },
133
144
  "dependencies": {
134
145
  "@huggingface/transformers": "^3.8.1",
135
- "@modelcontextprotocol/sdk": "^1.29.0",
146
+ "@modelcontextprotocol/sdk": "^1.30.0",
136
147
  "gpt-tokenizer": "^3.4.0",
137
148
  "pino": "^10.3.1",
138
149
  "web-tree-sitter": "^0.25.10"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "graphflow",
4
- "version": "1.9.15",
4
+ "version": "1.12.1",
5
5
  "description": "图谱上下文压缩与任务规划插件(10 个 MCP 工具)。能力:压缩上下文、DAG 规划、增量建图、技能飞轮、诊断与产物导入导出。DeepSeek Harness:dsh plugin --profile web add @roarpeng/graphflow;工具名为 mcp__graphflow__graphflow_*。先调 graphflow_context(传 rootDir),复杂任务再 plan,改完代码后 index。",
6
6
  "author": {
7
7
  "name": "Roarpeng",
package/web/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # GraphFlow Web 知识节点栏
2
+
3
+ DeepSeek Harness web 右侧"知识节点"面板的源码种子。
4
+
5
+ ## 现状
6
+
7
+ - **自动按轮记录**(持久、随包生效):`dsh/plugin.mjs`(glue)监听 `agent/inbox/inserted`,
8
+ 自动把每一轮用户提问写成 `dialogue-turn` 图节点(写入 agent 会话工作区的 `.graphflow/`)。
9
+ 回复补全沿用既有机制:agent 调用 `graphflow_context({ assistantReply })` 时填充(与
10
+ Claude Code SessionEnd 同一契约)。可用 `GRAPHFLOW_AUTO_CAPTURE=0` 关闭。
11
+ - **右侧知识节点栏**(本会话生效):以动态 Cordis 插件(Host + Client 双半部)挂载:
12
+ - `conversation.session.header.utilities`:会话头部"知识节点"开关按钮
13
+ - `shell.overlay`:右缘浮动面板(additive,不替换任何出厂 UI),展示
14
+ Workbench 主题(主线/旁支、活跃、待回复、消息数)+ 对话轮次(Q/A、跳转标记)
15
+ - 点击节点:通过会话 binding 的 `prompt('queue')` 通道提交续聊指令,agent 自动调用
16
+ `graphflow_context({ topicId | resumeFromTurnId })` 恢复上下文继续
17
+
18
+ ## 数据来源
19
+
20
+ 面板数据由动态插件的 host 半部经 `harness.handle("gf.nodes.list")` 提供,
21
+ 内部调用(当前工作区下):
22
+ - `graphflow workbench tree --json`
23
+ - `graphflow dialogue list --json --limit 50`
24
+
25
+ ## 打包为静态客户端插件(未来)
26
+
27
+ dsh web 的静态客户端包必须预编译为 `window.__ModuleLoader__.load({id, factory})`
28
+ 工厂格式并经 `/plugins/<id>/client.js` 提供;纯 ESM 文件无法直接加载。
29
+ `plugin.mjs` 中的 `hostHalf()` / `clientHalf()` 即未来 `code.host` / `code.client`
30
+ 的源码。届时需:
31
+ 1. 在 `package.json` 增加 `dsh.client` 声明(`platform: "web"` + `inject` 列表)
32
+ 2. 用 dsh monorepo 的 rolldown 管线把 `./web` 编译为客户端 bundle
33
+ 3. 在 `cordis.patch.yml` 增加第三行(客户端插件行)
34
+
35
+ 在完成上述打包前,**不要**在 `cordis.patch.yml` 添加该行(client-modules 扫描到
36
+ 缺失 bundle 会使 profile 组合失败)。
package/web/plugin.mjs ADDED
@@ -0,0 +1,448 @@
1
+ /**
2
+ * GraphFlow — DeepSeek Harness web knowledge-node panel (reference source).
3
+ *
4
+ * This file is the SOURCE SEED of the dynamic Cordis plugin that renders the
5
+ * right-side "知识节点" (knowledge nodes) panel in the DeepSeek Harness web
6
+ * UI. It ships in the package (exports["./web"]) as documentation and as the
7
+ * future bundle seed: once the dsh client-modules build pipeline can compile
8
+ * a `dsh.client` package (rolldown factory bundle served at
9
+ * `/plugins/<id>/client.js`), the two halves below become `code.host` and
10
+ * `code.client` of a static client package with a `dsh.client` declaration.
11
+ *
12
+ * Until then it is activated per harness session as a dynamic Cordis plugin:
13
+ * - host half: harness.handle("gf.nodes.list") → runs the graphflow CLI
14
+ * (workbench tree + dialogue list) in the session workspace via the host
15
+ * `shell` service and returns structured JSON.
16
+ * - client half: registers
17
+ * * a "知识节点" toggle in `conversation.session.header.utilities`
18
+ * (opens the panel and remembers the session),
19
+ * * the panel itself in `shell.overlay` (frame-wide, right-anchored,
20
+ * additive — the shipped details column stays untouched), rendering
21
+ * workbench topics (mainline/side) + dialogue turns, and
22
+ * * click-to-resume: submits a continuation instruction through the
23
+ * session binding's `prompt('queue')` channel (the same pipe the
24
+ * composer uses), so the agent restores context via
25
+ * graphflow_context({ topicId | resumeFromTurnId }) and continues.
26
+ *
27
+ * Plain JavaScript only: no TS/JSX/import. React arrives as a closure symbol.
28
+ */
29
+
30
+ /** Host half — the body of `code.host` (returns a Cordis Plugin). */
31
+ export function hostHalf() {
32
+ return {
33
+ name: "graphflow-web",
34
+ apply(ctx) {
35
+ const shell = ctx.get("shell");
36
+ if (shell === undefined) return;
37
+
38
+ const runCli = async (command, workspaceRoot) => {
39
+ const spec = shell.resolve({
40
+ command,
41
+ workdir: workspaceRoot,
42
+ timeoutMs: 20000,
43
+ stdoutMaxBytes: 4_000_000,
44
+ });
45
+ return shell.run(spec);
46
+ };
47
+
48
+ const parseCliOut = (result) => {
49
+ if (!result || result.exitCode !== 0) return null;
50
+ const stdout = typeof result.stdout === "string" ? result.stdout : "";
51
+ if (!stdout.trim()) return null;
52
+ try {
53
+ const parsed = JSON.parse(stdout);
54
+ return parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : parsed;
55
+ } catch {
56
+ return null;
57
+ }
58
+ };
59
+
60
+ harness.handle("gf.nodes.list", async (args) => {
61
+ const workspaceRoot =
62
+ args && typeof args.workspaceRoot === "string" && args.workspaceRoot.trim()
63
+ ? args.workspaceRoot
64
+ : null;
65
+ if (!workspaceRoot) return { ok: false, error: "no-workspace" };
66
+ try {
67
+ const wb = await runCli("graphflow workbench tree --json", workspaceRoot);
68
+ const dl = await runCli("graphflow dialogue list --json --limit 50", workspaceRoot);
69
+ return {
70
+ ok: true,
71
+ workbench: parseCliOut(wb),
72
+ dialogues: parseCliOut(dl),
73
+ };
74
+ } catch (error) {
75
+ return { ok: false, error: String(error && error.message ? error.message : error) };
76
+ }
77
+ });
78
+ },
79
+ };
80
+ }
81
+
82
+ /** Client half — the body of `code.client` (returns a Cordis Plugin). */
83
+ export function clientHalf() {
84
+ return {
85
+ name: "graphflow-web",
86
+ apply(ctx) {
87
+ const slots = ctx.get("slots");
88
+ if (slots === undefined) return;
89
+
90
+ // ── package-private panel state (in-memory, per plugin activation) ──
91
+ const panel = { open: false, sessionId: undefined, listeners: new Set() };
92
+ const subscribe = (fn) => {
93
+ panel.listeners.add(fn);
94
+ return () => {
95
+ panel.listeners.delete(fn);
96
+ };
97
+ };
98
+ const setOpen = (open, sessionId) => {
99
+ panel.open = open;
100
+ if (sessionId !== undefined) panel.sessionId = sessionId;
101
+ for (const fn of Array.from(panel.listeners)) {
102
+ try {
103
+ fn();
104
+ } catch {
105
+ // ignore
106
+ }
107
+ }
108
+ };
109
+
110
+ // ── header utility toggle ──
111
+ slots.inject("conversation.session.header.utilities", () =>
112
+ slots.register(
113
+ { name: "conversation.session.header.utilities", id: "gf-knowledge-toggle", order: 90, label: () => "知识节点" },
114
+ (props) => React.createElement(ToggleButton, { ...props, panel, setOpen, subscribe })
115
+ )
116
+ );
117
+
118
+ // ── right-side knowledge panel (frame-wide overlay, additive) ──
119
+ slots.inject("shell.overlay", () =>
120
+ slots.register(
121
+ { name: "shell.overlay", id: "gf-knowledge", order: 10, label: () => "知识节点" },
122
+ (props) =>
123
+ React.createElement(KnowledgePanel, {
124
+ ...props,
125
+ panel,
126
+ setOpen,
127
+ subscribe,
128
+ sessionsOf: () => ctx.get("sessions"),
129
+ })
130
+ )
131
+ );
132
+ },
133
+ };
134
+ }
135
+
136
+ // ─────────────────────────── shared helpers ───────────────────────────
137
+
138
+ function buildResumeInstruction(kind, id, cwd, title) {
139
+ const arg = kind === "topic" ? `topicId: "${id}"` : `resumeFromTurnId: "${id}"`;
140
+ return (
141
+ `继续对话:请先调用 mcp__graphflow__graphflow_context({ rootDir: "${cwd}", ${arg} })` +
142
+ `,读取该知识节点记录的历史问答与上下文,然后基于它继续当前任务。` +
143
+ `节点: ${title || id}`
144
+ );
145
+ }
146
+
147
+ // ─────────────────────────── components ───────────────────────────
148
+
149
+ function ToggleButton(props) {
150
+ const { panel, setOpen, subscribe, sessionId } = props;
151
+ const [open, setLocal] = React.useState(panel.open);
152
+ React.useEffect(() => subscribe(() => setLocal(panel.open)), []);
153
+ return React.createElement(
154
+ "button",
155
+ {
156
+ type: "button",
157
+ "aria-label": "知识节点",
158
+ title: "知识节点",
159
+ onClick: () => setOpen(!panel.open, sessionId),
160
+ className: "gf-toggle" + (open ? " gf-toggle-active" : ""),
161
+ },
162
+ "知识节点"
163
+ );
164
+ }
165
+
166
+ function KnowledgePanel(props) {
167
+ const { panel, setOpen, subscribe, useSessions, sessionsOf } = props;
168
+ const [open, setLocal] = React.useState(panel.open);
169
+ React.useEffect(() => subscribe(() => setLocal(panel.open)), []);
170
+ const currentId = useSessions((s) => s.current);
171
+ const cwd = useSessions((s) => (s.current ? s.byId[s.current]?.cwd : undefined));
172
+ const [data, setData] = React.useState(null);
173
+ const [loading, setLoading] = React.useState(false);
174
+ const [error, setError] = React.useState(null);
175
+ const [tick, setTick] = React.useState(0);
176
+
177
+ const refresh = React.useCallback(() => {
178
+ if (!cwd) return;
179
+ setLoading(true);
180
+ host
181
+ .call("gf.nodes.list", { workspaceRoot: cwd })
182
+ .then((res) => {
183
+ setData(res);
184
+ setError(null);
185
+ })
186
+ .catch((err) => setError(String(err && err.message ? err.message : err)))
187
+ .finally(() => setLoading(false));
188
+ }, [cwd]);
189
+
190
+ React.useEffect(() => {
191
+ if (open && cwd) refresh();
192
+ }, [open, cwd, tick]);
193
+
194
+ if (!open) return null;
195
+
196
+ const resume = (kind, id, title) => {
197
+ const sessions = sessionsOf();
198
+ const binding = sessions && currentId ? sessions.binding(currentId) : undefined;
199
+ if (!binding || !binding.session || !cwd) return;
200
+ const text = buildResumeInstruction(kind, id, cwd, title);
201
+ binding.session.prompt([{ type: "text", text }], "queue").catch(() => {});
202
+ };
203
+
204
+ const header = React.createElement(
205
+ "div",
206
+ { className: "gf-panel-header" },
207
+ React.createElement("div", { className: "gf-panel-title" }, "知识节点"),
208
+ React.createElement(
209
+ "button",
210
+ {
211
+ type: "button",
212
+ className: "gf-panel-btn",
213
+ "aria-label": "刷新",
214
+ title: "刷新",
215
+ onClick: () => setTick((v) => v + 1),
216
+ },
217
+ "刷新"
218
+ ),
219
+ React.createElement(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ className: "gf-panel-btn",
224
+ "aria-label": "关闭",
225
+ title: "关闭",
226
+ onClick: () => setOpen(false, undefined),
227
+ },
228
+ "✕"
229
+ )
230
+ );
231
+
232
+ let body;
233
+ if (loading && !data) {
234
+ body = React.createElement("div", { className: "gf-empty" }, "加载中…");
235
+ } else if (error) {
236
+ body = React.createElement("div", { className: "gf-error" }, `加载失败: ${error}`);
237
+ } else if (!data || (data.ok !== true)) {
238
+ body = React.createElement(
239
+ "div",
240
+ { className: "gf-empty" },
241
+ "暂无数据(需要安装 GraphFlow MCP 插件并建立知识图谱)"
242
+ );
243
+ } else {
244
+ const wbData = data.workbench;
245
+ const outlines = Array.isArray(wbData)
246
+ ? wbData
247
+ : wbData && Array.isArray(wbData.outlines)
248
+ ? wbData.outlines
249
+ : [];
250
+ const dialogues = Array.isArray(data.dialogues) ? data.dialogues : [];
251
+ const sections = [];
252
+ if (outlines.length === 0 && dialogues.length === 0) {
253
+ sections.push(
254
+ React.createElement(
255
+ "div",
256
+ { className: "gf-empty" },
257
+ "暂无知识节点 — 使用 graphflow_plan / graphflow_context 后,这里会出现主题与对话记录"
258
+ )
259
+ );
260
+ }
261
+ for (const outline of outlines) {
262
+ const children = [
263
+ React.createElement("div", { className: "gf-section-title" }, `Workbench · ${clip(outline.task, 60)}`),
264
+ ];
265
+ for (const node of outline.nodes || []) {
266
+ children.push(renderTopicNode(node, resume));
267
+ }
268
+ sections.push(React.createElement("div", { key: outline.rootId }, children));
269
+ }
270
+ if (dialogues.length > 0) {
271
+ const children = [React.createElement("div", { className: "gf-section-title" }, "对话记录")];
272
+ for (const turn of dialogues) {
273
+ children.push(
274
+ React.createElement(
275
+ "button",
276
+ {
277
+ key: turn.id,
278
+ type: "button",
279
+ className: "gf-node",
280
+ onClick: () => resume("turn", turn.id, `#${turn.seq} ${turn.userQuery}`),
281
+ },
282
+ React.createElement(
283
+ "div",
284
+ { className: "gf-node-title" },
285
+ React.createElement("span", { className: "gf-badge" }, `#${turn.seq}`),
286
+ React.createElement(
287
+ "span",
288
+ { className: "gf-node-label" },
289
+ clip(turn.title || turn.userQuery, 40)
290
+ ),
291
+ turn.jumped ? React.createElement("span", { className: "gf-badge gf-badge-jump" }, "跳转") : null
292
+ ),
293
+ React.createElement(
294
+ "div",
295
+ { className: "gf-node-preview" },
296
+ turn.summary && turn.summary.trim()
297
+ ? `A: ${clip(turn.summary, 60)}`
298
+ : turn.assistantReply && turn.assistantReply.trim()
299
+ ? `A: ${clip(turn.assistantReply, 60)}`
300
+ : "A: (待回复)"
301
+ )
302
+ )
303
+ );
304
+ }
305
+ sections.push(React.createElement("div", { key: "dialogues" }, children));
306
+ }
307
+ body = React.createElement("div", { className: "gf-panel-body" }, sections);
308
+ }
309
+
310
+ return React.createElement("div", { className: "gf-panel" }, header, body);
311
+ }
312
+
313
+ function renderTopicNode(node, resume) {
314
+ const badges = [];
315
+ if (node.active) badges.push(React.createElement("span", { key: "a", className: "gf-badge gf-badge-active" }, "当前"));
316
+ if (node.kind === "side") badges.push(React.createElement("span", { key: "s", className: "gf-badge" }, "旁支"));
317
+ if (node.pendingReply) badges.push(React.createElement("span", { key: "p", className: "gf-badge gf-badge-pending" }, "待回复"));
318
+ if (node.messageCount > 0) {
319
+ badges.push(React.createElement("span", { key: "m", className: "gf-badge" }, `${node.messageCount} 条`));
320
+ }
321
+ const children = [];
322
+ for (const child of node.children || []) {
323
+ children.push(renderTopicNode(child, resume));
324
+ }
325
+ return React.createElement(
326
+ "div",
327
+ { key: node.id },
328
+ React.createElement(
329
+ "button",
330
+ {
331
+ type: "button",
332
+ className: "gf-node",
333
+ onClick: () => resume("topic", node.id, node.title),
334
+ },
335
+ React.createElement("div", { className: "gf-node-title" }, React.createElement("span", { className: "gf-node-label" }, node.title), badges),
336
+ node.lastUserPreview
337
+ ? React.createElement("div", { className: "gf-node-preview" }, clip(node.lastUserPreview, 70))
338
+ : null
339
+ ),
340
+ children
341
+ );
342
+ }
343
+
344
+ function clip(text, max) {
345
+ const normalized = String(text || "").replace(/\s+/g, " ").trim();
346
+ if (normalized.length <= max) return normalized;
347
+ return `${normalized.slice(0, max - 1)}…`;
348
+ }
349
+
350
+ // ─────────────────────────── styles ───────────────────────────
351
+
352
+ export const PANEL_CSS = `
353
+ .gf-panel {
354
+ position: fixed;
355
+ top: 64px;
356
+ right: 16px;
357
+ bottom: 96px;
358
+ width: 340px;
359
+ max-width: calc(100vw - 32px);
360
+ display: flex;
361
+ flex-direction: column;
362
+ background: var(--dsw-alias-bg-overlay, #202020);
363
+ border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.4));
364
+ border-radius: 12px;
365
+ box-shadow: 0 8px 32px rgba(0,0,0,.35);
366
+ color: var(--dsw-alias-label-primary, #eee);
367
+ font-size: 13px;
368
+ line-height: 1.45;
369
+ overflow: hidden;
370
+ z-index: 1000;
371
+ pointer-events: auto;
372
+ }
373
+ .gf-panel-header {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 6px;
377
+ padding: 10px 12px;
378
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3));
379
+ flex: none;
380
+ }
381
+ .gf-panel-title { font-weight: 600; flex: 1; }
382
+ .gf-panel-btn {
383
+ background: none;
384
+ border: none;
385
+ color: var(--dsw-alias-label-secondary, #aaa);
386
+ cursor: pointer;
387
+ font-size: 12px;
388
+ padding: 2px 8px;
389
+ border-radius: 6px;
390
+ }
391
+ .gf-panel-btn:hover { background: rgba(128,128,128,.18); }
392
+ .gf-panel-body { flex: 1; overflow: auto; padding: 4px 10px 16px; }
393
+ .gf-section-title {
394
+ font-size: 11px;
395
+ font-weight: 600;
396
+ color: var(--dsw-alias-label-secondary, #aaa);
397
+ margin: 12px 2px 6px;
398
+ letter-spacing: .04em;
399
+ }
400
+ .gf-node {
401
+ display: block;
402
+ width: 100%;
403
+ text-align: left;
404
+ background: none;
405
+ border: none;
406
+ border-radius: 8px;
407
+ padding: 6px 8px;
408
+ margin: 2px 0;
409
+ color: var(--dsw-alias-label-primary, #eee);
410
+ cursor: pointer;
411
+ font: inherit;
412
+ }
413
+ .gf-node:hover { background: rgba(128,128,128,.14); }
414
+ .gf-node-title { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
415
+ .gf-node-label { font-weight: 500; min-width: 0; }
416
+ .gf-badge {
417
+ font-size: 10px;
418
+ padding: 1px 6px;
419
+ border-radius: 8px;
420
+ background: rgba(128,128,128,.22);
421
+ color: var(--dsw-alias-label-secondary, #aaa);
422
+ flex: none;
423
+ }
424
+ .gf-badge-active { background: var(--dsw-alias-brand-primary, #4a90d9); color: #fff; }
425
+ .gf-badge-pending { background: var(--dsw-alias-state-warn-primary, #e0a13c); color: #000; }
426
+ .gf-badge-jump { background: var(--dsw-alias-state-warn-primary, #e0a13c); color: #000; }
427
+ .gf-node-preview {
428
+ color: var(--dsw-alias-label-secondary, #aaa);
429
+ font-size: 12px;
430
+ margin-top: 2px;
431
+ overflow: hidden;
432
+ text-overflow: ellipsis;
433
+ white-space: nowrap;
434
+ }
435
+ .gf-empty { color: var(--dsw-alias-label-secondary, #aaa); padding: 18px 6px; text-align: center; font-size: 12px; }
436
+ .gf-error { color: var(--dsw-alias-state-error-primary, #e06c6c); padding: 10px; font-size: 12px; }
437
+ .gf-toggle {
438
+ background: none;
439
+ border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.4));
440
+ color: var(--dsw-alias-label-secondary, #aaa);
441
+ cursor: pointer;
442
+ font-size: 12px;
443
+ padding: 3px 10px;
444
+ border-radius: 8px;
445
+ }
446
+ .gf-toggle:hover { background: rgba(128,128,128,.14); }
447
+ .gf-toggle-active { color: var(--dsw-alias-brand-primary, #4a90d9); border-color: var(--dsw-alias-brand-primary, #4a90d9); }
448
+ `;