@ycodium-ai/plugin-visualize 0.0.0-stage → 0.2.2944

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.
package/dist/index.js ADDED
@@ -0,0 +1,505 @@
1
+ // src/index.ts
2
+ import { definePlugin } from "@ycodium-ai/plugin-api";
3
+
4
+ // src/paths.ts
5
+ var VIZ_FILE = /^viz\/[a-z0-9][a-z0-9-]{0,80}-[0-9a-f]{8}\.html$/u;
6
+ function slugOf(title) {
7
+ const slug = title.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "").slice(0, 48);
8
+ return slug.length > 0 ? slug : "visualization";
9
+ }
10
+ function contentHash(text) {
11
+ let hash = 2166136261;
12
+ for (let i = 0; i < text.length; i++) {
13
+ hash ^= text.charCodeAt(i);
14
+ hash = Math.imul(hash, 16777619);
15
+ }
16
+ return (hash >>> 0).toString(16).padStart(8, "0");
17
+ }
18
+ function vizPathFor(title, fragment) {
19
+ return `viz/${slugOf(title)}-${contentHash(fragment)}.html`;
20
+ }
21
+ function acceptVizPath(path) {
22
+ const normalized = path.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
23
+ if (normalized.includes("..") || normalized.startsWith("/") || normalized.includes("//")) {
24
+ return null;
25
+ }
26
+ return VIZ_FILE.test(normalized) ? normalized : null;
27
+ }
28
+
29
+ // src/presentation.ts
30
+ function cardIndex(card) {
31
+ return {
32
+ invocationId: card.invocationId,
33
+ title: card.title,
34
+ mode: card.mode,
35
+ path: card.path,
36
+ sizeBytes: card.sizeBytes,
37
+ action: card.action
38
+ };
39
+ }
40
+
41
+ // src/ledger.ts
42
+ var MAX_CARDS_PER_THREAD = 32;
43
+ var CARDS_PROJECTION = "cards";
44
+ function createLedger(input) {
45
+ const live = /* @__PURE__ */ new Map();
46
+ const indexes = /* @__PURE__ */ new Map();
47
+ const listeners = /* @__PURE__ */ new Map();
48
+ const notify = (threadId) => {
49
+ for (const listener of listeners.get(threadId) ?? []) listener();
50
+ };
51
+ return {
52
+ async remember(threadId, card) {
53
+ const prior = await readIndex(input.cache, indexes, threadId);
54
+ const nextIndex = [...prior, cardIndex(card)].slice(-MAX_CARDS_PER_THREAD);
55
+ const kept = new Set(nextIndex.map((entry) => entry.invocationId));
56
+ const bodies = [...live.get(threadId) ?? [], card].filter(
57
+ (entry) => kept.has(entry.invocationId)
58
+ );
59
+ live.set(threadId, bodies);
60
+ indexes.set(threadId, nextIndex);
61
+ await input.cache.setJson(indexKey(threadId), nextIndex);
62
+ await input.cache.setJson(cardKey(threadId, card.invocationId), card);
63
+ try {
64
+ await input.projections.set({
65
+ name: CARDS_PROJECTION,
66
+ threadId,
67
+ value: { cards: nextIndex }
68
+ });
69
+ } catch (error) {
70
+ input.log.warn("visualize projection was not stored", {
71
+ threadId,
72
+ detail: error instanceof Error ? error.message : String(error)
73
+ });
74
+ }
75
+ notify(threadId);
76
+ },
77
+ async get(threadId, invocationId) {
78
+ return readCard(input.cache, live, threadId, invocationId);
79
+ },
80
+ async findByPath(threadId, path) {
81
+ const index = await readIndex(input.cache, indexes, threadId);
82
+ for (let i = index.length - 1; i >= 0; i -= 1) {
83
+ const entry = index[i];
84
+ if (entry === void 0 || entry.path !== path) continue;
85
+ const card = await readCard(input.cache, live, threadId, entry.invocationId);
86
+ if (card !== null) return card;
87
+ }
88
+ const remembered = live.get(threadId);
89
+ if (remembered === void 0) return null;
90
+ for (let i = remembered.length - 1; i >= 0; i -= 1) {
91
+ const card = remembered[i];
92
+ if (card !== void 0 && card.path === path) return card;
93
+ }
94
+ return null;
95
+ },
96
+ indexOf(threadId) {
97
+ return readIndex(input.cache, indexes, threadId);
98
+ },
99
+ subscribe(threadId, listener, signal) {
100
+ const bucket = listeners.get(threadId) ?? /* @__PURE__ */ new Set();
101
+ bucket.add(listener);
102
+ listeners.set(threadId, bucket);
103
+ return new Promise((resolve) => {
104
+ const finish = () => {
105
+ bucket.delete(listener);
106
+ resolve();
107
+ };
108
+ if (signal.aborted) {
109
+ finish();
110
+ return;
111
+ }
112
+ signal.addEventListener("abort", finish, { once: true });
113
+ });
114
+ }
115
+ };
116
+ }
117
+ async function readIndex(cache, indexes, threadId) {
118
+ const memory = indexes.get(threadId);
119
+ if (memory !== void 0) return memory;
120
+ const stored = await cache.getJson(indexKey(threadId));
121
+ const parsed = stored === null ? [] : parseIndex(stored.value);
122
+ indexes.set(threadId, parsed);
123
+ return parsed;
124
+ }
125
+ function parseIndex(value) {
126
+ if (!Array.isArray(value)) return [];
127
+ const out = [];
128
+ for (const entry of value) {
129
+ if (!isIndexEntry(entry)) continue;
130
+ out.push(entry);
131
+ }
132
+ return out;
133
+ }
134
+ function isIndexEntry(value) {
135
+ if (typeof value !== "object" || value === null) return false;
136
+ const record = value;
137
+ return typeof record["invocationId"] === "string" && typeof record["title"] === "string" && (record["mode"] === "inline" || record["mode"] === "wide") && typeof record["path"] === "string" && typeof record["sizeBytes"] === "number" && (record["action"] === "create" || record["action"] === "update");
138
+ }
139
+ function isCard(value) {
140
+ if (!isIndexEntry(value)) return false;
141
+ const record = value;
142
+ return record["kind"] === "visualize" && typeof record["fragment"] === "string";
143
+ }
144
+ async function readCard(cache, live, threadId, invocationId) {
145
+ const found = live.get(threadId)?.find((card) => card.invocationId === invocationId);
146
+ if (found !== void 0) return found;
147
+ const stored = await cache.getJson(cardKey(threadId, invocationId));
148
+ if (stored === null || !isCard(stored.value)) return null;
149
+ return stored.value;
150
+ }
151
+ function indexKey(threadId) {
152
+ return `index:${threadId}`;
153
+ }
154
+ function cardKey(threadId, invocationId) {
155
+ return `card:${threadId}:${invocationId}`;
156
+ }
157
+
158
+ // src/limits.ts
159
+ var DEFAULT_MAX_FRAGMENT_BYTES = 1e6;
160
+ function maxFragmentBytesOf(value) {
161
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : DEFAULT_MAX_FRAGMENT_BYTES;
162
+ if (!Number.isFinite(parsed)) return DEFAULT_MAX_FRAGMENT_BYTES;
163
+ const whole = Math.floor(parsed);
164
+ if (whole < 1) return DEFAULT_MAX_FRAGMENT_BYTES;
165
+ return Math.min(whole, DEFAULT_MAX_FRAGMENT_BYTES);
166
+ }
167
+
168
+ // src/tool.ts
169
+ import {
170
+ MAX_PLUGIN_TOOL_PRESENTATION_BYTES,
171
+ pluginJsonByteLength
172
+ } from "@ycodium-ai/plugin-api";
173
+
174
+ // src/confirm.ts
175
+ var LIVE_TOOL_OUTPUT_LINE = 84;
176
+ function confirmationLine(input) {
177
+ const line = input.action === "update" ? `updated card at ${input.path}` : `workspace copy at ${input.path}`;
178
+ return line.length <= LIVE_TOOL_OUTPUT_LINE ? line : line.slice(0, LIVE_TOOL_OUTPUT_LINE);
179
+ }
180
+
181
+ // src/fragment.ts
182
+ var VISUALIZE_TOOL_NAME = "visualize";
183
+ var VISUALIZE_WIRE_NAME = "visualize_visualize";
184
+ var SKELETON_TAG = /<!doctype\b|<\s*(?:html|head|body)\b/iu;
185
+ function byteLength(text) {
186
+ return new TextEncoder().encode(text).length;
187
+ }
188
+ function validateFragment(fragment, maxBytes) {
189
+ if (fragment.trim().length === 0) {
190
+ throw new Error("invalid visualization: the fragment is empty");
191
+ }
192
+ const sizeBytes = byteLength(fragment);
193
+ if (sizeBytes > maxBytes) {
194
+ throw new Error(
195
+ `invalid visualization: fragment is ${sizeBytes} bytes, over the ${maxBytes}-byte limit \u2014 shrink the inline data first (fewer rows, coarser buckets, fewer decimals)`
196
+ );
197
+ }
198
+ const skeleton = SKELETON_TAG.exec(fragment);
199
+ if (skeleton) {
200
+ throw new Error(
201
+ `invalid visualization: fragment contains a document-skeleton tag (${JSON.stringify(skeleton[0])}) \u2014 write only the inline body; the card supplies the document`
202
+ );
203
+ }
204
+ const external = externalAssetUrl(fragment);
205
+ if (external !== null) {
206
+ throw new Error(
207
+ `invalid visualization: fragment loads an external resource (${JSON.stringify(external)}) \u2014 inline the script or stylesheet in the fragment; do not use a CDN or any network URL`
208
+ );
209
+ }
210
+ return sizeBytes;
211
+ }
212
+ function externalAssetUrl(fragment) {
213
+ for (const match of fragment.matchAll(/<(script|link)\b([^>]*)>/giu)) {
214
+ const tag = match[1]?.toLowerCase();
215
+ const attrs = match[2] ?? "";
216
+ if (tag === "script") {
217
+ const src = attr(attrs, "src");
218
+ if (src !== null && src.length > 0 && !inlineAssetUrl(src)) return src;
219
+ } else if (attr(attrs, "rel")?.toLowerCase() === "stylesheet") {
220
+ const href = attr(attrs, "href");
221
+ if (href !== null && href.length > 0 && !inlineAssetUrl(href)) return href;
222
+ }
223
+ }
224
+ const imported = /@import\s+(?:url\(\s*)?['"]?(https?:\/\/[^'")\s]+)/iu.exec(fragment);
225
+ return imported?.[1] ?? null;
226
+ }
227
+ function inlineAssetUrl(value) {
228
+ const trimmed = value.trim().toLowerCase();
229
+ return trimmed.startsWith("data:") || trimmed.startsWith("blob:");
230
+ }
231
+ function attr(attrs, name) {
232
+ const match = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>]+))`, "iu").exec(
233
+ attrs
234
+ );
235
+ return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim() || null;
236
+ }
237
+ var PATCH_CONTEXT_CHARS = 160;
238
+ var MIN_ANCHOR_CHARS = 12;
239
+ function applyFragmentPatch(base, oldStr, newStr) {
240
+ if (oldStr.length === 0) {
241
+ throw new Error(
242
+ "invalid visualization patch: old_str is empty \u2014 pass the exact card text to replace"
243
+ );
244
+ }
245
+ const first = base.indexOf(oldStr);
246
+ if (first === -1) {
247
+ throw new Error(
248
+ `invalid visualization patch: old_str does not appear in the card. ${nearestAnchor(base, oldStr)}`
249
+ );
250
+ }
251
+ if (base.indexOf(oldStr, first + oldStr.length) !== -1) {
252
+ throw new Error(
253
+ `invalid visualization patch: old_str appears ${countOccurrences(base, oldStr)} times in the card \u2014 extend it with neighbouring lines until exactly one site matches`
254
+ );
255
+ }
256
+ return base.slice(0, first) + newStr + base.slice(first + oldStr.length);
257
+ }
258
+ function nearestAnchor(base, oldStr) {
259
+ let matched = 0;
260
+ let beyond = oldStr.length;
261
+ while (matched < beyond) {
262
+ const mid = Math.ceil((matched + beyond) / 2);
263
+ if (base.includes(oldStr.slice(0, mid))) matched = mid;
264
+ else beyond = mid - 1;
265
+ }
266
+ if (matched < MIN_ANCHOR_CHARS) {
267
+ return "None of it matched, so the card is not in the state you assumed \u2014 re-render the whole card instead.";
268
+ }
269
+ const at = base.indexOf(oldStr.slice(0, matched));
270
+ return `Its first ${matched} characters do match, at offset ${at}, where the card actually reads ${JSON.stringify(base.slice(at, at + PATCH_CONTEXT_CHARS))} \u2014 correct old_str against that and retry.`;
271
+ }
272
+ function countOccurrences(base, needle) {
273
+ let count = 0;
274
+ for (let at = base.indexOf(needle); at !== -1; at = base.indexOf(needle, at + needle.length)) {
275
+ count += 1;
276
+ }
277
+ return count;
278
+ }
279
+
280
+ // src/tool.ts
281
+ var DESCRIPTION = `Show the user an interactive HTML visualization as a live card in the conversation. Call \`${VISUALIZE_WIRE_NAME}\`. \`create\` (the default) takes the whole markup in \`fragment\`: literal inline HTML only (no <!doctype>, <html>, <head>, or <body>, and no <script src> or stylesheet URL \u2014 inline every script and style; the card supplies the document and theme). To correct a card you already rendered, call \`update\` with its \`path\` and one exact \`old_str\`/\`new_str\` replacement instead of re-sending the whole fragment. The card appears while you generate; a copy of the finished fragment is saved under viz/ in the session workspace. Load the visualize skill before the first call. The user sees the card; your reply must not paste the markup.`;
282
+ function visualizeTool(input) {
283
+ return {
284
+ name: VISUALIZE_TOOL_NAME,
285
+ description: DESCRIPTION,
286
+ inputSchema: {
287
+ type: "object",
288
+ properties: {
289
+ action: {
290
+ type: "string",
291
+ enum: ["create", "update"],
292
+ description: "`create` (default) renders a new card from `fragment`. `update` patches the card at `path`, replacing `old_str` with `new_str`. Use update for a correction touching fewer than 20 lines in fewer than 5 places, and at most 4 times per reply."
293
+ },
294
+ fragment: {
295
+ type: "string",
296
+ description: "create only, required: the inline HTML fragment to render (markup, style, and script \u2014 no document skeleton)."
297
+ },
298
+ title: {
299
+ type: "string",
300
+ description: 'Concise card title. Defaults to "Visualization" on create; required on update.'
301
+ },
302
+ mode: {
303
+ type: "string",
304
+ enum: ["inline", "wide"],
305
+ description: "Card width: `inline` (default) or `wide` for side-by-side panel comparisons."
306
+ },
307
+ path: {
308
+ type: "string",
309
+ description: "update only, required: workspace path of the card to patch, as its own call reported it (viz/<slug>-<hash>.html)."
310
+ },
311
+ old_str: {
312
+ type: "string",
313
+ description: "update only, required: the exact card text to replace, whitespace included. It must appear exactly once."
314
+ },
315
+ new_str: {
316
+ type: "string",
317
+ description: "update only, required: the replacement text. Empty deletes the matched region."
318
+ }
319
+ }
320
+ },
321
+ handle: (args, invocation) => runVisualize(args, invocation, input)
322
+ };
323
+ }
324
+ async function runVisualize(args, invocation, input) {
325
+ try {
326
+ const action = readAction(args["action"]);
327
+ const mode = readMode(args["mode"]);
328
+ const workspace = invocation.workspace;
329
+ if (workspace?.writeFile === void 0 || workspace.readFile === void 0) {
330
+ return failure(
331
+ "invalid visualization: this call has no session workspace to write the card into"
332
+ );
333
+ }
334
+ const invocationId = invocation.id;
335
+ if (invocationId === void 0 || invocationId.length === 0) {
336
+ return failure("invalid visualization: this call has no invocation id");
337
+ }
338
+ let path;
339
+ let fragment;
340
+ if (action === "update") {
341
+ const reported = acceptVizPath(required(args["path"], "path", action));
342
+ if (reported === null) {
343
+ return failure(
344
+ "invalid visualization: `path` must be the viz/<slug>-<hash>.html path this tool reported"
345
+ );
346
+ }
347
+ path = reported;
348
+ const current = await workspace.readFile(path);
349
+ fragment = applyFragmentPatch(
350
+ new TextDecoder().decode(current.bytes),
351
+ required(args["old_str"], "old_str", action),
352
+ present(args["new_str"], "new_str", action)
353
+ );
354
+ } else {
355
+ fragment = required(args["fragment"], "fragment", action);
356
+ const titleForPath = typeof args["title"] === "string" ? args["title"] : "";
357
+ path = vizPathFor(titleForPath.trim() || "Visualization", fragment);
358
+ }
359
+ const sizeBytes = validateFragment(fragment, input.maxFragmentBytes());
360
+ const title = action === "update" ? required(args["title"], "title", action).trim() : (typeof args["title"] === "string" ? args["title"].trim() : "") || "Visualization";
361
+ const card = {
362
+ kind: "visualize",
363
+ invocationId,
364
+ fragment,
365
+ title,
366
+ mode,
367
+ path,
368
+ sizeBytes,
369
+ action
370
+ };
371
+ const presentationBytes = pluginJsonByteLength(card);
372
+ if (presentationBytes > MAX_PLUGIN_TOOL_PRESENTATION_BYTES) {
373
+ return failure(
374
+ `invalid visualization: the card payload is ${presentationBytes} bytes, over the ${MAX_PLUGIN_TOOL_PRESENTATION_BYTES}-byte presentation limit \u2014 shrink the fragment`
375
+ );
376
+ }
377
+ await workspace.writeFile(path, new TextEncoder().encode(fragment));
378
+ if (invocation.threadId !== null) {
379
+ await input.ledger.remember(invocation.threadId, card);
380
+ }
381
+ return {
382
+ text: confirmationLine({ action, title, sizeBytes, path }),
383
+ presentation: card
384
+ };
385
+ } catch (error) {
386
+ return failure(error instanceof Error ? error.message : String(error));
387
+ }
388
+ }
389
+ function readAction(value) {
390
+ if (value === void 0 || value === "create") return "create";
391
+ if (value === "update") return "update";
392
+ throw new Error('invalid visualization: `action` must be "create" or "update"');
393
+ }
394
+ function readMode(value) {
395
+ return value === "wide" ? "wide" : "inline";
396
+ }
397
+ function required(value, name, action) {
398
+ if (typeof value !== "string" || value.trim().length === 0) {
399
+ throw new Error(`invalid visualization: \`${name}\` is required when action is "${action}"`);
400
+ }
401
+ return value;
402
+ }
403
+ function present(value, name, action) {
404
+ if (typeof value !== "string") {
405
+ throw new Error(`invalid visualization: \`${name}\` is required when action is "${action}"`);
406
+ }
407
+ return value;
408
+ }
409
+ function failure(text) {
410
+ return { isError: true, text, errorCode: "invalid_visualization" };
411
+ }
412
+
413
+ // src/index.ts
414
+ var visualizePlugin = definePlugin({
415
+ id: "visualize",
416
+ apiVersion: 4,
417
+ activate(ctx) {
418
+ const ledger = createLedger({
419
+ cache: ctx.cache,
420
+ projections: ctx.sessionProjections,
421
+ log: ctx.log
422
+ });
423
+ ctx.sessionProjections.register({ name: CARDS_PROJECTION });
424
+ ctx.toolkit({
425
+ name: "visualize",
426
+ tools: [
427
+ visualizeTool({
428
+ maxFragmentBytes: () => maxFragmentBytesOf(ctx.config.maxFragmentBytes),
429
+ ledger
430
+ })
431
+ ]
432
+ });
433
+ ctx.command({
434
+ name: "card",
435
+ description: "Read one stored visualization card for a thread.",
436
+ scope: "read",
437
+ payloadSchema: {
438
+ type: "object",
439
+ properties: {
440
+ threadId: { type: "string" },
441
+ invocationId: { type: "string" },
442
+ path: { type: "string" }
443
+ },
444
+ required: ["threadId", "invocationId"]
445
+ },
446
+ handle: async (payload) => {
447
+ const threadId = readString(payload["threadId"]);
448
+ const path = readString(payload["path"]);
449
+ if (path.length > 0) {
450
+ const byPath = await ledger.findByPath(threadId, path);
451
+ if (byPath !== null) return { card: byPath };
452
+ }
453
+ return {
454
+ card: await ledger.get(threadId, readString(payload["invocationId"]))
455
+ };
456
+ }
457
+ });
458
+ ctx.stream({
459
+ name: "cards",
460
+ description: "List visualization cards stored for a thread.",
461
+ scope: "read",
462
+ buffer: { latest: 1 },
463
+ payloadSchema: {
464
+ type: "object",
465
+ properties: {
466
+ threadId: { type: "string" }
467
+ },
468
+ required: ["threadId"]
469
+ },
470
+ handle: async (payload, emit, signal) => {
471
+ const threadId = readString(payload["threadId"]);
472
+ const push = async () => {
473
+ emit({ cards: await ledger.indexOf(threadId) });
474
+ };
475
+ await push();
476
+ await ledger.subscribe(
477
+ threadId,
478
+ () => {
479
+ void push();
480
+ },
481
+ signal
482
+ );
483
+ }
484
+ });
485
+ ctx.settingsCard({
486
+ titleKey: "settings.card.title",
487
+ descriptionKey: "settings.card.description",
488
+ fields: [
489
+ {
490
+ key: "maxFragmentBytes",
491
+ kind: "number",
492
+ labelKey: "settings.maxFragmentBytes.label",
493
+ descriptionKey: "settings.maxFragmentBytes.description"
494
+ }
495
+ ]
496
+ });
497
+ }
498
+ });
499
+ function readString(value) {
500
+ return typeof value === "string" ? value : "";
501
+ }
502
+ var index_default = visualizePlugin;
503
+ export {
504
+ index_default as default
505
+ };
@@ -0,0 +1,35 @@
1
+ {
2
+ "settings": {
3
+ "card": {
4
+ "title": "Visualize",
5
+ "description": "Agents call this to put an interactive HTML card in the conversation. The card is sandboxed. Nothing is sent from the card back into the chat."
6
+ },
7
+ "maxFragmentBytes": {
8
+ "label": "Maximum fragment size",
9
+ "description": "Largest inline HTML fragment, in bytes. The default is 1000000. Larger values are capped at 1000000 so the card still fits the presentation limit."
10
+ }
11
+ },
12
+ "status": {
13
+ "streaming": "Preview",
14
+ "ready": "Ready",
15
+ "loading": "Loading",
16
+ "missing": "Unavailable"
17
+ },
18
+ "card": {
19
+ "modeInline": "Inline",
20
+ "modeWide": "Wide",
21
+ "bytes": "{{count}} bytes",
22
+ "path": "Workspace copy: {{path}}",
23
+ "copyPath": "Copy path",
24
+ "copied": "Copied",
25
+ "frame": "Visualization: {{title}}",
26
+ "loading": "Loading the saved visualization.",
27
+ "missing": "This visualization is not on this machine anymore. The confirmation line is still in the tool result."
28
+ },
29
+ "dock": {
30
+ "waiting": "The visualization will appear here as it is written."
31
+ },
32
+ "tool": {
33
+ "title": "Visualize"
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "settings": {
3
+ "card": {
4
+ "title": "可视化",
5
+ "description": "智能体用它在会话里放一张可交互的 HTML 卡片。卡片在沙箱里运行。卡片里的操作不会发回对话。"
6
+ },
7
+ "maxFragmentBytes": {
8
+ "label": "片段大小上限",
9
+ "description": "一段内联 HTML 最多多少字节。默认 1000000。更大的值会被压到 1000000,好让卡片仍放得进展示体上限里。"
10
+ }
11
+ },
12
+ "status": {
13
+ "streaming": "预览",
14
+ "ready": "已就绪",
15
+ "loading": "加载中",
16
+ "missing": "不可用"
17
+ },
18
+ "card": {
19
+ "modeInline": "标准",
20
+ "modeWide": "加宽",
21
+ "bytes": "{{count}} 字节",
22
+ "path": "工作区副本:{{path}}",
23
+ "copyPath": "复制路径",
24
+ "copied": "已复制",
25
+ "frame": "可视化:{{title}}",
26
+ "loading": "正在加载已保存的可视化。",
27
+ "missing": "这台机器上已经没有这份可视化了。工具结果里的确认行还在。"
28
+ },
29
+ "dock": {
30
+ "waiting": "可视化会在撰写过程中出现在这里。"
31
+ },
32
+ "tool": {
33
+ "title": "可视化"
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-visualize",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.2944",
4
+ "description": "Renders an inline HTML fragment as a sandboxed card in the conversation.",
5
+ "keywords": [
6
+ "visualization",
7
+ "ycodium",
8
+ "ycodium-plugin"
9
+ ],
10
+ "license": "UNLICENSED",
11
+ "files": [
12
+ "dist",
13
+ "assets",
14
+ "skills",
15
+ "locales",
16
+ "README.md"
17
+ ],
18
+ "type": "module",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "~19.2.14",
24
+ "esbuild": "0.28.1",
25
+ "react": "19.2.6",
26
+ "typescript": "~6.0.3",
27
+ "vite-plus": "0.2.2",
28
+ "@ycodium-ai/plugin-api": "0.2.2944"
29
+ },
30
+ "ycodium": {
31
+ "plugin": "./dist/index.js",
32
+ "displayName": "Visualize",
33
+ "description": "Renders an inline HTML fragment as a sandboxed card in the conversation.",
34
+ "icon": "./assets/icon.svg",
35
+ "permissions": [
36
+ {
37
+ "kind": "workspace.read"
38
+ },
39
+ {
40
+ "kind": "workspace.write"
41
+ },
42
+ {
43
+ "kind": "ui.seats",
44
+ "seats": [
45
+ "conversation.input.dock",
46
+ "conversation.chat.turnTail"
47
+ ]
48
+ },
49
+ {
50
+ "kind": "sessionProjection"
51
+ }
52
+ ],
53
+ "skills": [
54
+ {
55
+ "dir": "./skills/visualize",
56
+ "files": [
57
+ "references/charts.md",
58
+ "references/design.md",
59
+ "examples/comparison-chart.html",
60
+ "examples/interactive-simulator.html"
61
+ ]
62
+ }
63
+ ],
64
+ "ui": {
65
+ "assets": "./assets",
66
+ "entry": "ui.js",
67
+ "toolViews": [
68
+ {
69
+ "tool": "visualize",
70
+ "view": "card",
71
+ "placement": "deliverable",
72
+ "titleKey": "tool.title"
73
+ }
74
+ ]
75
+ }
76
+ },
77
+ "scripts": {
78
+ "build": "node build.mjs",
79
+ "typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.ui.json",
80
+ "test": "vp test run"
81
+ }
6
82
  }