pmx-canvas 0.6.2 → 0.6.3

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.
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * This module must never import server.ts or index.ts.
10
10
  */
11
+ import { resolve } from 'node:path';
11
12
  import { z } from 'zod';
12
13
  import { canvasState, type CanvasAnnotation, type CanvasLayout, type CanvasNodeState } from '../../canvas-state.js';
13
14
  import {
@@ -1145,7 +1146,7 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
1145
1146
  },
1146
1147
  },
1147
1148
  handler: async (input, ctx) => {
1148
- const body: Record<string, unknown> = input;
1149
+ let body: Record<string, unknown> = input;
1149
1150
  const id = input.id;
1150
1151
  const existing = canvasState.getNode(id);
1151
1152
  if (!existing) throw new OperationError(`Node "${id}" not found.`, 404);
@@ -1162,7 +1163,48 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
1162
1163
  const result = await refreshCanvasWebpageNode(id, { ...(url ? { url } : {}) });
1163
1164
  return result as unknown as Record<string, unknown>;
1164
1165
  }
1166
+
1167
+ let nextFilePath: string | null = null;
1168
+ let nextFileContent: ReturnType<typeof readFileNodeContent> | null = null;
1169
+ if (existing.type === 'file') {
1170
+ const nestedData = isRecord(body.data) ? body.data : undefined;
1171
+ const hasTopLevelPath = body.path !== undefined;
1172
+ const hasNestedPath = nestedData?.path !== undefined;
1173
+ if (hasTopLevelPath || hasNestedPath) {
1174
+ const normalizePath = (value: unknown): string => {
1175
+ if (typeof value !== 'string' || !value.trim()) {
1176
+ throw new OperationError('File node "path" must be a non-empty string.');
1177
+ }
1178
+ return resolve(canvasState.getWorkspaceRoot(), value.trim());
1179
+ };
1180
+ const normalizedTopLevelPath = hasTopLevelPath ? normalizePath(body.path) : undefined;
1181
+ const normalizedNestedPath = hasNestedPath ? normalizePath(nestedData?.path) : undefined;
1182
+ body = {
1183
+ ...body,
1184
+ ...(normalizedTopLevelPath ? { path: normalizedTopLevelPath } : {}),
1185
+ ...(nestedData
1186
+ ? { data: { ...nestedData, ...(normalizedNestedPath ? { path: normalizedNestedPath } : {}) } }
1187
+ : {}),
1188
+ };
1189
+ nextFilePath = normalizedNestedPath ?? normalizedTopLevelPath ?? null;
1190
+ nextFileContent = nextFilePath ? readFileNodeContent(nextFilePath) : null;
1191
+ if (!nextFileContent || nextFileContent.kind === 'unavailable') {
1192
+ throw new OperationError(`File node target "${nextFilePath}" is missing or unreadable.`);
1193
+ }
1194
+ }
1195
+ }
1196
+
1165
1197
  const { patch, groupChildIds } = buildNodePatch(existing, body);
1198
+ const prevPath = existing.type === 'file' && typeof existing.data.path === 'string' ? existing.data.path : null;
1199
+ if (nextFilePath && nextFileContent && patch.data) {
1200
+ const data = { ...patch.data };
1201
+ const prevBase = prevPath?.split(/[\\/]/).pop();
1202
+ if (typeof body.title !== 'string' && (!data.title || data.title === prevBase)) {
1203
+ data.title = nextFilePath.split(/[\\/]/).pop();
1204
+ }
1205
+ applyFileContentToNodeData(data, nextFileContent);
1206
+ patch.data = data;
1207
+ }
1166
1208
  canvasState.updateNode(id, patch);
1167
1209
  if (groupChildIds !== undefined && !setGroupChildrenFromApi(id, groupChildIds)) {
1168
1210
  throw new OperationError(`Group "${id}" not found.`, 404);
@@ -1171,20 +1213,10 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
1171
1213
  // watcher IN PLACE — before this, pointing a file node elsewhere meant
1172
1214
  // recreating it, which lost its edges, pins, and position.
1173
1215
  if (existing.type === 'file') {
1174
- const prevPath = typeof existing.data.path === 'string' ? existing.data.path : null;
1175
1216
  const repointed = canvasState.getNode(id);
1176
1217
  const nextPath = repointed && typeof repointed.data.path === 'string' ? repointed.data.path : null;
1177
1218
  if (nextPath && nextPath !== prevPath) {
1178
1219
  if (prevPath) unwatchFileForNode(id, prevPath);
1179
- const data = { ...repointed!.data };
1180
- // The title follows the file unless the caller pinned one in this
1181
- // patch or had renamed the node away from the old filename.
1182
- const prevBase = prevPath?.split(/[\\/]/).pop();
1183
- if (typeof body.title !== 'string' && (!data.title || data.title === prevBase)) {
1184
- data.title = nextPath.split(/[\\/]/).pop();
1185
- }
1186
- applyFileContentToNodeData(data, readFileNodeContent(nextPath));
1187
- canvasState.updateNode(id, { data });
1188
1220
  watchFileForNode(id, nextPath);
1189
1221
  scheduleCodeGraphRecompute(() => {
1190
1222
  ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
@@ -26,8 +26,10 @@ export function contentHeightReporterSource(frameToken: string): string {
26
26
  `function m(){var d=document.documentElement;return Math.max(d?d.scrollHeight:0,document.body?document.body.scrollHeight:0);}` +
27
27
  `function r(){var h=m();if(Math.abs(h-last)<=4)return;last=h;window.parent.postMessage({source:'pmx-canvas-frame',type:'content-height',token:T,height:h},'*');}` +
28
28
  `function s(){if(timer)return;timer=setTimeout(function(){timer=null;r();},100);}` +
29
- `if(document.readyState!=='loading')s();window.addEventListener('load',s);` +
30
- `try{new ResizeObserver(s).observe(document.documentElement);}catch(e){}setTimeout(s,60);})();`
29
+ `if(document.readyState!=='loading')s();window.addEventListener('load',s,true);` +
30
+ `try{new ResizeObserver(s).observe(document.documentElement);}catch(e){}` +
31
+ `new MutationObserver(s).observe(document.documentElement,{subtree:true,childList:true,characterData:true,attributes:true});` +
32
+ `if(document.fonts)document.fonts.ready.then(s);setTimeout(s,60);})();`
31
33
  );
32
34
  }
33
35