nolo-cli 0.1.22 → 0.1.26

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 (128) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/agentRecordConfig.ts +4 -0
  3. package/agent-runtime/hostAdapter.ts +2 -0
  4. package/agent-runtime/hybridRecordStore.ts +30 -9
  5. package/agent-runtime/index.ts +7 -0
  6. package/agent-runtime/localLoop.ts +14 -4
  7. package/agent-runtime/localToolPolicy.ts +22 -10
  8. package/agent-runtime/localWorkspaceTools.ts +140 -374
  9. package/agent-runtime/platformChatProvider.ts +3 -0
  10. package/agent-runtime/runtimeToolPolicy.ts +133 -0
  11. package/agent-runtime/types.ts +47 -0
  12. package/agentAliases.ts +1 -1
  13. package/agentCommandSupport.ts +77 -0
  14. package/agentInternalCommandEntries.ts +34 -0
  15. package/agentListCommands.ts +123 -0
  16. package/agentListHelpers.ts +187 -0
  17. package/agentMachineCommands.ts +402 -0
  18. package/agentRecordCommands.ts +141 -0
  19. package/agentRecordHelpers.ts +395 -0
  20. package/agentRunCommand.ts +75 -3
  21. package/agentRuntimeCommands.ts +7 -773
  22. package/ai/agent/_executeModel.ts +29 -3
  23. package/ai/agent/buildSystemPrompt.ts +17 -0
  24. package/ai/agent/cliChatClient.ts +38 -4
  25. package/ai/agent/cliExecutor.ts +304 -25
  26. package/ai/agent/createAgentSchema.ts +2 -2
  27. package/ai/agent/guidedCreation/assistant.ts +82 -0
  28. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  29. package/ai/agent/guidedCreation/draft.ts +62 -0
  30. package/ai/agent/guidedCreation/types.ts +38 -0
  31. package/ai/agent/machineRunPermissions.ts +21 -3
  32. package/ai/agent/runtimeGuidance.ts +1 -1
  33. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  34. package/ai/agent/streamAgentChatTurn.ts +593 -36
  35. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  36. package/ai/ai.locale.ts +14 -2
  37. package/ai/chat/fetchUtils.native.ts +2 -0
  38. package/ai/chat/fetchUtils.ts +2 -0
  39. package/ai/chat/sendOpenAICompletionsRequest.ts +82 -1
  40. package/ai/chat/sendOpenAIResponseRequest.ts +65 -0
  41. package/ai/llm/getPricing.ts +14 -0
  42. package/ai/llm/kimi.ts +4 -1
  43. package/ai/llm/modelAvatar.ts +1 -0
  44. package/ai/llm/providers.ts +4 -1
  45. package/ai/llm/reasoningModels.ts +1 -0
  46. package/ai/taskRun/taskRunProtocol.ts +25 -10
  47. package/ai/token/modelUsageQuery.ts +17 -0
  48. package/ai/tools/agent/agentTools.ts +38 -0
  49. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  50. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  51. package/ai/tools/checkEnvTool.ts +52 -20
  52. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  53. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  54. package/ai/tools/index.ts +24 -14
  55. package/ai/tools/marxistsOfflineBook.ts +253 -0
  56. package/ai/tools/modelUsageTools.ts +9 -2
  57. package/ai/tools/readTool.ts +31 -0
  58. package/ai/tools/toolApiClient.ts +35 -1
  59. package/ai/tools/toolNameAliases.ts +13 -0
  60. package/cliCommandAdapters.ts +28 -0
  61. package/cliCommandDispatch.ts +66 -0
  62. package/cliCommandFactories.ts +34 -0
  63. package/cliCommandTypes.ts +49 -0
  64. package/cliEnvHelpers.ts +44 -0
  65. package/client/agentRun.test.ts +330 -24
  66. package/client/agentRun.ts +117 -35
  67. package/client/hybridRecordStore.test.ts +42 -0
  68. package/client/hybridRecordStore.ts +11 -0
  69. package/client/localRuntimeAdapter.test.ts +191 -21
  70. package/client/localRuntimeAdapter.ts +99 -10
  71. package/client/localRuntimeDryRun.test.ts +5 -2
  72. package/client/taskRunPrompt.ts +1 -0
  73. package/commandRegistry.ts +11 -61
  74. package/connector-experimental/capabilities.ts +1 -0
  75. package/database/actions/common.ts +4 -3
  76. package/database/config.ts +19 -0
  77. package/database/server/coreDataProxy.ts +43 -26
  78. package/index.ts +54 -191
  79. package/internalCommandEntries.ts +12 -0
  80. package/machineCommands.ts +688 -64
  81. package/offlineMarxistsAgentCommand.ts +314 -0
  82. package/package.json +22 -2
  83. package/render/canvas/canvasEditContext.ts +127 -0
  84. package/render/canvas/canvasRuntime.ts +57 -0
  85. package/render/canvas/canvasSnapshotParser.ts +76 -0
  86. package/render/canvas/canvasTree.ts +308 -0
  87. package/render/canvas/types.ts +46 -0
  88. package/render/layout/deleteBehavior.ts +52 -0
  89. package/render/layout/mainLayoutSidebar.ts +17 -0
  90. package/render/layout/mainLayoutViewMode.ts +56 -0
  91. package/render/layout/topbarUtils.ts +87 -0
  92. package/render/layout/useDevReloadPending.ts +30 -0
  93. package/render/page/createPageAction.ts +183 -0
  94. package/render/page/docSlice.ts +468 -0
  95. package/render/page/server/createPage.ts +174 -0
  96. package/render/page/server/handleCreatePage.ts +91 -0
  97. package/render/page/server/index.ts +4 -0
  98. package/render/page/types.ts +17 -0
  99. package/render/page/useKeyboardSave.ts +48 -0
  100. package/render/styles/zIndex.ts +12 -0
  101. package/render/surf/WeatherIconStyles.ts +17 -0
  102. package/render/surf/color.ts +9 -0
  103. package/render/surf/config.ts +46 -0
  104. package/render/surf/screens/style.ts +1 -0
  105. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  106. package/render/surf/utils/groupedWeatherData.ts +32 -0
  107. package/render/surf/weatherUtils.ts +50 -0
  108. package/render/table/activityColumns.ts +6 -0
  109. package/render/table/createTableAction.ts +270 -0
  110. package/render/table/deleteTableAction.ts +129 -0
  111. package/render/table/fetchAndCacheTableRows.ts +174 -0
  112. package/render/table/tableSlice.ts +1106 -0
  113. package/render/table/tableView.ts +289 -0
  114. package/render/table/toolValueUtils.ts +363 -0
  115. package/render/table/types.ts +252 -0
  116. package/render/table/useCreateTable.ts +72 -0
  117. package/render/table/useTable.ts +61 -0
  118. package/render/table/utils/tableSerialization.ts +50 -0
  119. package/render/web/elements/artifactPreviewCode.ts +43 -0
  120. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  121. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  122. package/render/web/elements/mermaidPreview.ts +21 -0
  123. package/render/web/ui/useInlineEdit.ts +135 -0
  124. package/runtimeDoctorCommands.ts +118 -0
  125. package/scriptCommandEntries.ts +71 -0
  126. package/systemInternalCommandEntries.ts +71 -0
  127. package/tableCommands.ts +42 -5
  128. package/workflowInternalCommandEntries.ts +11 -0
@@ -0,0 +1,314 @@
1
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
2
+
3
+ type EnvLike = Record<string, string | undefined>;
4
+
5
+ type OutputLike = {
6
+ write(chunk: string): unknown;
7
+ };
8
+
9
+ type SetupOfflineMarxistsAgentDeps = {
10
+ env?: EnvLike;
11
+ output?: OutputLike;
12
+ fetchImpl?: typeof fetch;
13
+ now?: () => number;
14
+ };
15
+
16
+ type ParsedArgs = {
17
+ serverUrl: string;
18
+ authToken: string;
19
+ userId: string;
20
+ sourceAgentKey: string;
21
+ targetAgentId: string;
22
+ targetAgentKey: string;
23
+ spaceId: string;
24
+ name: string;
25
+ publicAgent: boolean;
26
+ json: boolean;
27
+ };
28
+
29
+ const DEFAULT_USER_ID = "b2e06f801f";
30
+ const DEFAULT_SPACE_ID = "01KKY77TT0DA9NY7TNW3R7255N";
31
+ const DEFAULT_SOURCE_AGENT_ID = "01MIMO25MONTH00000001PGT4X";
32
+ const DEFAULT_TARGET_AGENT_ID = "01OFFMARXBOOK000000010AHL1";
33
+ const DEFAULT_AGENT_NAME = "离线马克思主义文库书籍转换助手";
34
+ const TOOL_NAME = "convertMarxistsBookToOfflineHtml";
35
+ const AGENT_TOOLS = [TOOL_NAME];
36
+
37
+ function readFlagValue(args: string[], flag: string) {
38
+ const index = args.indexOf(flag);
39
+ return index >= 0 ? args[index + 1] : undefined;
40
+ }
41
+
42
+ function parseUserIdFromToken(token: string) {
43
+ const payload = token.split(".")[0];
44
+ if (!payload) return "";
45
+ try {
46
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
47
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
48
+ const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
49
+ return typeof parsed?.userId === "string" ? parsed.userId : "";
50
+ } catch {
51
+ return "";
52
+ }
53
+ }
54
+
55
+ function normalizeBaseUrl(value: string) {
56
+ return value.trim().replace(/\/+$/, "");
57
+ }
58
+
59
+ function parseArgs(args: string[], env: EnvLike): ParsedArgs | null {
60
+ if (args.includes("--help") || args.includes("-h")) return null;
61
+ const serverUrl = normalizeBaseUrl(
62
+ readFlagValue(args, "--server") ??
63
+ readFlagValue(args, "--server-url") ??
64
+ env.NOLO_SERVER ??
65
+ env.BASE_URL ??
66
+ DEFAULT_NOLO_SERVER_URL
67
+ );
68
+ const authToken = readFlagValue(args, "--token") ?? env.AUTH_TOKEN ?? env.NOLO_AUTH_TOKEN ?? "";
69
+ const userId =
70
+ readFlagValue(args, "--user-id") ??
71
+ env.NOLO_USER_ID ??
72
+ parseUserIdFromToken(authToken) ??
73
+ DEFAULT_USER_ID;
74
+ const sourceAgentKey =
75
+ readFlagValue(args, "--source-agent") ??
76
+ env.NOLO_MIMO_SOURCE_AGENT_KEY ??
77
+ `agent-${userId}-${DEFAULT_SOURCE_AGENT_ID}`;
78
+ const targetAgentId =
79
+ readFlagValue(args, "--target-agent-id") ??
80
+ env.NOLO_OFFLINE_MARXISTS_AGENT_ID ??
81
+ DEFAULT_TARGET_AGENT_ID;
82
+ const targetAgentKey =
83
+ readFlagValue(args, "--target-agent") ??
84
+ env.NOLO_OFFLINE_MARXISTS_AGENT_KEY ??
85
+ `agent-${userId}-${targetAgentId}`;
86
+ const spaceId =
87
+ readFlagValue(args, "--space") ??
88
+ env.NOLO_OFFLINE_MARXISTS_SPACE_ID ??
89
+ DEFAULT_SPACE_ID;
90
+
91
+ if (!authToken.trim()) return null;
92
+ return {
93
+ serverUrl,
94
+ authToken,
95
+ userId,
96
+ sourceAgentKey,
97
+ targetAgentId,
98
+ targetAgentKey,
99
+ spaceId,
100
+ name: readFlagValue(args, "--name") ?? DEFAULT_AGENT_NAME,
101
+ publicAgent: args.includes("--public"),
102
+ json: args.includes("--json"),
103
+ };
104
+ }
105
+
106
+ async function readDbRecord(args: {
107
+ fetchImpl: typeof fetch;
108
+ serverUrl: string;
109
+ authToken: string;
110
+ dbKey: string;
111
+ }) {
112
+ const res = await args.fetchImpl(
113
+ `${args.serverUrl}/api/v1/db/read/${encodeURIComponent(args.dbKey)}`,
114
+ { headers: { Authorization: `Bearer ${args.authToken}` } }
115
+ );
116
+ const data = await res.json().catch(() => ({}));
117
+ if (!res.ok) {
118
+ throw new Error(`read ${args.dbKey} failed (${res.status}): ${JSON.stringify(data)}`);
119
+ }
120
+ return (data as any)?.data ?? data;
121
+ }
122
+
123
+ async function writeDbRecord(args: {
124
+ fetchImpl: typeof fetch;
125
+ serverUrl: string;
126
+ authToken: string;
127
+ userId: string;
128
+ dbKey: string;
129
+ data: Record<string, any>;
130
+ }) {
131
+ const res = await args.fetchImpl(`${args.serverUrl}/api/v1/db/write/`, {
132
+ method: "POST",
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ Authorization: `Bearer ${args.authToken}`,
136
+ },
137
+ body: JSON.stringify({
138
+ customKey: args.dbKey,
139
+ userId: args.userId,
140
+ data: { ...args.data, dbKey: args.dbKey },
141
+ }),
142
+ });
143
+ const data = await res.json().catch(() => ({}));
144
+ if (!res.ok) {
145
+ throw new Error(`write ${args.dbKey} failed (${res.status}): ${JSON.stringify(data)}`);
146
+ }
147
+ }
148
+
149
+ function buildTargetAgent(args: {
150
+ parsed: ParsedArgs;
151
+ source: Record<string, any>;
152
+ now: number;
153
+ }) {
154
+ const { parsed, source, now } = args;
155
+ if (!source.apiKey) {
156
+ throw new Error(`source agent ${parsed.sourceAgentKey} has no apiKey`);
157
+ }
158
+ if (!source.customProviderUrl) {
159
+ throw new Error(`source agent ${parsed.sourceAgentKey} has no customProviderUrl`);
160
+ }
161
+ return {
162
+ id: parsed.targetAgentId,
163
+ type: "agent",
164
+ name: parsed.name,
165
+ description:
166
+ "把 Marxists.org 中文旧式书籍页面转换为可离线打开的单文件 HTML,保留原 CSS、表格排版和背景纹理。",
167
+ prompt:
168
+ "你是离线书籍转换助手。用户给出 Marxists.org 中文书籍页面或要求保存离线书籍时,只能调用 convertMarxistsBookToOfflineHtml,不要使用普通网页抓取流程。最终回答必须逐字给出 rawData.fileUrl、rawData.pageCount、rawData.hasNetworkUrls、rawData.byteLength。",
169
+ isPublic: parsed.publicAgent,
170
+ model: source.model ?? "mimo-v2.5-pro",
171
+ provider: source.provider ?? "custom",
172
+ apiSource: source.apiSource ?? "custom",
173
+ apiKey: source.apiKey,
174
+ customProviderUrl: source.customProviderUrl,
175
+ apiKeyHeader: source.apiKeyHeader ?? "api-key",
176
+ tools: AGENT_TOOLS,
177
+ temperature: source.temperature ?? 0.2,
178
+ maxTokens: source.maxTokens ?? 4096,
179
+ category: "document-tools",
180
+ tags: ["offline-book", "marxists", "mimo"],
181
+ userId: parsed.userId,
182
+ createdAt: source.createdAt ?? now,
183
+ updatedAt: now,
184
+ };
185
+ }
186
+
187
+ async function attachAgentToSpace(args: {
188
+ fetchImpl: typeof fetch;
189
+ parsed: ParsedArgs;
190
+ contentKey: string;
191
+ title: string;
192
+ now: number;
193
+ }) {
194
+ const spaceKey = `space-${args.parsed.spaceId}`;
195
+ const space = await readDbRecord({
196
+ fetchImpl: args.fetchImpl,
197
+ serverUrl: args.parsed.serverUrl,
198
+ authToken: args.parsed.authToken,
199
+ dbKey: spaceKey,
200
+ });
201
+ await writeDbRecord({
202
+ fetchImpl: args.fetchImpl,
203
+ serverUrl: args.parsed.serverUrl,
204
+ authToken: args.parsed.authToken,
205
+ userId: args.parsed.userId,
206
+ dbKey: spaceKey,
207
+ data: {
208
+ ...space,
209
+ contents: {
210
+ ...(space.contents ?? {}),
211
+ [args.contentKey]: {
212
+ ...(space.contents?.[args.contentKey] ?? {}),
213
+ title: args.title,
214
+ type: "agent",
215
+ contentKey: args.contentKey,
216
+ createdAt: space.contents?.[args.contentKey]?.createdAt ?? args.now,
217
+ updatedAt: args.now,
218
+ },
219
+ },
220
+ updatedAt: args.now,
221
+ },
222
+ });
223
+ }
224
+
225
+ function writeUsage(output: OutputLike) {
226
+ output.write(
227
+ "Usage: nolo agent setup-offline-marxists [--server https://nolo.chat] [--source-agent <key>] [--space <spaceId>] [--target-agent-id <id>] [--json]\n" +
228
+ "Requires AUTH_TOKEN from `nolo login` or --token.\n"
229
+ );
230
+ }
231
+
232
+ export async function runSetupOfflineMarxistsAgentCommand(
233
+ args: string[],
234
+ deps: SetupOfflineMarxistsAgentDeps = {}
235
+ ) {
236
+ const env = deps.env ?? process.env;
237
+ const output = deps.output ?? process.stdout;
238
+ const parsed = parseArgs(args, env);
239
+ if (!parsed) {
240
+ writeUsage(output);
241
+ return 1;
242
+ }
243
+ const fetchImpl = deps.fetchImpl ?? fetch;
244
+ const now = deps.now?.() ?? Date.now();
245
+
246
+ try {
247
+ const source = await readDbRecord({
248
+ fetchImpl,
249
+ serverUrl: parsed.serverUrl,
250
+ authToken: parsed.authToken,
251
+ dbKey: parsed.sourceAgentKey,
252
+ });
253
+ const target = buildTargetAgent({ parsed, source, now });
254
+ await writeDbRecord({
255
+ fetchImpl,
256
+ serverUrl: parsed.serverUrl,
257
+ authToken: parsed.authToken,
258
+ userId: parsed.userId,
259
+ dbKey: parsed.targetAgentKey,
260
+ data: target,
261
+ });
262
+ const publicKey = `agent-pub-${parsed.targetAgentId}`;
263
+ if (parsed.publicAgent) {
264
+ await writeDbRecord({
265
+ fetchImpl,
266
+ serverUrl: parsed.serverUrl,
267
+ authToken: parsed.authToken,
268
+ userId: parsed.userId,
269
+ dbKey: publicKey,
270
+ data: target,
271
+ });
272
+ }
273
+ await attachAgentToSpace({
274
+ fetchImpl,
275
+ parsed,
276
+ contentKey: parsed.publicAgent ? publicKey : parsed.targetAgentKey,
277
+ title: parsed.name,
278
+ now,
279
+ });
280
+
281
+ const result = {
282
+ serverUrl: parsed.serverUrl,
283
+ sourceAgentKey: parsed.sourceAgentKey,
284
+ agentKey: parsed.targetAgentKey,
285
+ agentUrl: `${parsed.serverUrl}/${parsed.targetAgentKey}`,
286
+ publicKey: parsed.publicAgent ? publicKey : null,
287
+ spaceId: parsed.spaceId,
288
+ model: target.model,
289
+ provider: target.provider,
290
+ customProviderUrl: target.customProviderUrl,
291
+ tools: target.tools,
292
+ };
293
+ if (parsed.json) {
294
+ output.write(`${JSON.stringify(result, null, 2)}\n`);
295
+ } else {
296
+ output.write(
297
+ [
298
+ `created/updated: ${result.agentKey}`,
299
+ `url: ${result.agentUrl}`,
300
+ `space: ${result.spaceId}`,
301
+ `model: ${result.model}`,
302
+ `tools: ${result.tools.join(", ")}`,
303
+ "",
304
+ ].join("\n")
305
+ );
306
+ }
307
+ return 0;
308
+ } catch (error) {
309
+ output.write(
310
+ `setup-offline-marxists failed: ${error instanceof Error ? error.message : String(error)}\n`
311
+ );
312
+ return 1;
313
+ }
314
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -11,17 +11,35 @@
11
11
  "index.ts",
12
12
  "agentRuntimeLocal.ts",
13
13
  "agentAliases.ts",
14
+ "agentCommandSupport.ts",
15
+ "agentListCommands.ts",
14
16
  "localRuntimeDb.ts",
15
17
  "agentRuntimeCommands.ts",
18
+ "agentListHelpers.ts",
19
+ "agentMachineCommands.ts",
16
20
  "agentPullCommand.ts",
21
+ "agentRecordCommands.ts",
22
+ "agentRecordHelpers.ts",
17
23
  "agentRunCommand.ts",
18
24
  "authCommands.ts",
25
+ "cliCommandAdapters.ts",
26
+ "cliCommandFactories.ts",
27
+ "cliCommandDispatch.ts",
28
+ "cliCommandTypes.ts",
29
+ "cliEnvHelpers.ts",
19
30
  "commandRegistry.ts",
20
31
  "connectorRunArtifact.ts",
21
32
  "connectorWebSocketTarget.ts",
22
33
  "defaultServer.ts",
34
+ "agentInternalCommandEntries.ts",
35
+ "internalCommandEntries.ts",
23
36
  "machineCommands.ts",
37
+ "offlineMarxistsAgentCommand.ts",
38
+ "runtimeDoctorCommands.ts",
39
+ "scriptCommandEntries.ts",
40
+ "systemInternalCommandEntries.ts",
24
41
  "taskRunCommand.ts",
42
+ "workflowInternalCommandEntries.ts",
25
43
  "runtimeModeArgs.ts",
26
44
  "tableCommands.ts",
27
45
  "updateCommands.ts",
@@ -32,8 +50,9 @@
32
50
  "connector-experimental/**/*.ts",
33
51
  "agent-runtime/**/*.ts",
34
52
  "database/**/*.ts",
53
+ "ai/**/*.ts",
35
54
  "connector-experimental/**/*.ts",
36
- "ai/**/*.ts"
55
+ "render/**/*.ts"
37
56
  ],
38
57
  "publishConfig": {
39
58
  "access": "public"
@@ -47,6 +66,7 @@
47
66
  "dependencies": {
48
67
  "date-fns-tz": "^2.0.0",
49
68
  "level": "^10.0.0",
69
+ "js-yaml": "^4.1.0",
50
70
  "ulid": "^2.3.0"
51
71
  }
52
72
  }
@@ -0,0 +1,127 @@
1
+ import React from "react";
2
+ import type { AgentRuntimeOptions } from "ai/agent/types";
3
+ import type { CanvasEvent } from "./types";
4
+
5
+ export type CanvasEditSelection = {
6
+ sourceMessageId?: string;
7
+ selectedNodeId: string;
8
+ part: string;
9
+ path: string[];
10
+ type: string;
11
+ props: Record<string, unknown>;
12
+ style: Record<string, unknown>;
13
+ };
14
+
15
+ const CANVAS_EDIT_SELECTION_EVENT = "nolo:canvas-edit-selection";
16
+
17
+ let currentSelection: CanvasEditSelection | null = null;
18
+ let pendingSelection: CanvasEditSelection | null = null;
19
+ const listeners = new Set<(selection: CanvasEditSelection | null) => void>();
20
+ const patchListeners = new Map<string, Set<(events: CanvasEvent[]) => void>>();
21
+ const queuedPatches = new Map<string, CanvasEvent[][]>();
22
+
23
+ export function publishCanvasEditSelection(
24
+ selection: CanvasEditSelection | null
25
+ ) {
26
+ currentSelection = selection;
27
+ listeners.forEach((listener) => listener(selection));
28
+
29
+ if (typeof window !== "undefined") {
30
+ window.dispatchEvent(
31
+ new CustomEvent(CANVAS_EDIT_SELECTION_EVENT, { detail: selection })
32
+ );
33
+ }
34
+ }
35
+
36
+ export function subscribeCanvasEditSelection(
37
+ listener: (selection: CanvasEditSelection | null) => void
38
+ ) {
39
+ listeners.add(listener);
40
+ listener(currentSelection);
41
+ return () => {
42
+ listeners.delete(listener);
43
+ };
44
+ }
45
+
46
+ export function useCanvasEditSelection() {
47
+ const [selection, setSelection] =
48
+ React.useState<CanvasEditSelection | null>(currentSelection);
49
+
50
+ React.useEffect(
51
+ () => subscribeCanvasEditSelection(setSelection),
52
+ []
53
+ );
54
+
55
+ return selection;
56
+ }
57
+
58
+ export function markPendingCanvasEditSelection(
59
+ selection: CanvasEditSelection | null
60
+ ) {
61
+ pendingSelection = selection;
62
+ }
63
+
64
+ export function consumePendingCanvasEditSelection() {
65
+ const selection = pendingSelection;
66
+ pendingSelection = null;
67
+ return selection;
68
+ }
69
+
70
+ export function publishCanvasMessagePatch(
71
+ sourceMessageId: string,
72
+ events: CanvasEvent[]
73
+ ) {
74
+ if (!events.length) return;
75
+
76
+ const queued = queuedPatches.get(sourceMessageId) ?? [];
77
+ queued.push(events);
78
+ queuedPatches.set(sourceMessageId, queued);
79
+
80
+ patchListeners
81
+ .get(sourceMessageId)
82
+ ?.forEach((listener) => listener(events));
83
+ }
84
+
85
+ export function subscribeCanvasMessagePatches(
86
+ sourceMessageId: string,
87
+ listener: (events: CanvasEvent[]) => void
88
+ ) {
89
+ const listenersForMessage = patchListeners.get(sourceMessageId) ?? new Set();
90
+ listenersForMessage.add(listener);
91
+ patchListeners.set(sourceMessageId, listenersForMessage);
92
+
93
+ queuedPatches
94
+ .get(sourceMessageId)
95
+ ?.forEach((events) => listener(events));
96
+
97
+ return () => {
98
+ listenersForMessage.delete(listener);
99
+ if (!listenersForMessage.size) {
100
+ patchListeners.delete(sourceMessageId);
101
+ }
102
+ };
103
+ }
104
+
105
+ export function buildCanvasNodeEditingTarget(
106
+ selection: CanvasEditSelection
107
+ ): NonNullable<AgentRuntimeOptions["editingTarget"]> {
108
+ return {
109
+ kind: "canvas_node",
110
+ key: selection.selectedNodeId,
111
+ title: selection.part,
112
+ summary: [
113
+ "用户正在编辑 Canvas Tree 画布中的一个已选中节点。",
114
+ "只输出针对选中节点的 canvas_snapshot updateNode 事件,除非用户明确要求新增内容。",
115
+ "不要重建整棵树,不要输出 Markdown、解释、React、HTML、CSS 或 JS 源码。",
116
+ ].join("\n"),
117
+ metadata: {
118
+ sourceMessageId: selection.sourceMessageId,
119
+ selectedNodeId: selection.selectedNodeId,
120
+ part: selection.part,
121
+ path: selection.path,
122
+ type: selection.type,
123
+ props: selection.props,
124
+ style: selection.style,
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,57 @@
1
+ export type CanvasRuntimeAction =
2
+ | {
3
+ type: "toggle";
4
+ key: string;
5
+ }
6
+ | {
7
+ type: "setState";
8
+ key: string;
9
+ value: unknown;
10
+ }
11
+ | {
12
+ type: "scrollTo";
13
+ targetId: string;
14
+ };
15
+
16
+ export type CanvasRuntimeState = Record<string, unknown>;
17
+
18
+ export function parseCanvasRuntimeAction(action: unknown): CanvasRuntimeAction | null {
19
+ if (!action || typeof action !== "object") return null;
20
+ const candidate = action as Record<string, unknown>;
21
+ const type = candidate.type;
22
+
23
+ if (type === "toggle" && typeof candidate.key === "string" && candidate.key.trim()) {
24
+ return { type, key: candidate.key };
25
+ }
26
+
27
+ if (type === "setState" && typeof candidate.key === "string" && candidate.key.trim()) {
28
+ return { type, key: candidate.key, value: candidate.value };
29
+ }
30
+
31
+ if (type === "scrollTo" && typeof candidate.targetId === "string" && candidate.targetId.trim()) {
32
+ return { type, targetId: candidate.targetId };
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ export function reduceCanvasRuntimeAction(
39
+ state: CanvasRuntimeState,
40
+ action: CanvasRuntimeAction
41
+ ): CanvasRuntimeState {
42
+ if (action.type === "toggle") {
43
+ return {
44
+ ...state,
45
+ [action.key]: !state[action.key],
46
+ };
47
+ }
48
+
49
+ if (action.type === "setState") {
50
+ return {
51
+ ...state,
52
+ [action.key]: action.value,
53
+ };
54
+ }
55
+
56
+ return state;
57
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ applyCanvasEvent,
3
+ createCanvasDocument,
4
+ normalizeCanvasEvent,
5
+ } from "./canvasTree";
6
+ import type { CanvasDocument, CanvasEvent } from "./types";
7
+
8
+ export type ParsedCanvasSnapshotMessage = {
9
+ document: CanvasDocument;
10
+ events: CanvasEvent[];
11
+ eventCount: number;
12
+ };
13
+
14
+ export function hasCanvasSnapshotSignal(content: string): boolean {
15
+ return content.includes("canvas_snapshot");
16
+ }
17
+
18
+ export function extractCanvasSnapshotText(content: unknown): string | null {
19
+ if (typeof content === "string") {
20
+ return hasCanvasSnapshotSignal(content) ? content : null;
21
+ }
22
+ if (!Array.isArray(content)) return null;
23
+
24
+ const text = content
25
+ .map((item) =>
26
+ item?.type === "text" && typeof item.text === "string" ? item.text : ""
27
+ )
28
+ .filter(Boolean)
29
+ .join("\n");
30
+
31
+ return hasCanvasSnapshotSignal(text) ? text : null;
32
+ }
33
+
34
+ function isCanvasEvent(value: unknown): value is CanvasEvent {
35
+ if (!value || typeof value !== "object") return false;
36
+ const event = value as { type?: unknown };
37
+ return (
38
+ event.type === "appendNode" ||
39
+ event.type === "updateNode" ||
40
+ event.type === "selectNode"
41
+ );
42
+ }
43
+
44
+ export function parseCanvasSnapshotMessage(
45
+ content: string
46
+ ): ParsedCanvasSnapshotMessage | null {
47
+ if (!hasCanvasSnapshotSignal(content)) return null;
48
+
49
+ let document = createCanvasDocument("root");
50
+ const events: CanvasEvent[] = [];
51
+ let eventCount = 0;
52
+
53
+ for (const line of content.split(/\r?\n/)) {
54
+ const trimmed = line.trim();
55
+ if (!trimmed.startsWith("{") || !trimmed.includes("canvas_snapshot")) {
56
+ continue;
57
+ }
58
+
59
+ try {
60
+ const parsed = JSON.parse(trimmed);
61
+ if (parsed?.type !== "canvas_snapshot" || !isCanvasEvent(parsed.event)) {
62
+ continue;
63
+ }
64
+ const event = normalizeCanvasEvent(parsed.event);
65
+ if (!event) continue;
66
+ document = applyCanvasEvent(document, event);
67
+ events.push(event);
68
+ eventCount += 1;
69
+ } catch {
70
+ // Streaming messages often end with a partial JSON line. Ignore it until
71
+ // the next render provides a complete line.
72
+ }
73
+ }
74
+
75
+ return eventCount > 0 ? { document, events, eventCount } : null;
76
+ }