Skip to content

Commit d1be612

Browse files
pankgeorgclaude
andcommitted
feat: sync Pluto-side code edits into VSCode cell text
cell_inputs.<id>.code replace patches (browser UI edits, MCP update_cell) were logged UNHANDLED — the switch case listened for 'cell_input' (singular) which never matches. The patch now rewrites the matching cell's text, unwrapping md""" wrappers for markup cells. Echoes of our own executeCell push are dropped by text equality, and the remote-edit suppression (now shared via beginRemoteEdit/ endRemoteEditSoon) guards the applyEdit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 204258b commit d1be612

1 file changed

Lines changed: 81 additions & 22 deletions

File tree

src/controller.ts

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -654,10 +654,7 @@ export class PlutoNotebookController {
654654
`[CellOrderSync] Applying remote structure: ${currentOrder.length} -> ${plutoOrder.length} cells`
655655
);
656656

657-
this.remoteEditDepth.set(
658-
notebookPath,
659-
(this.remoteEditDepth.get(notebookPath) ?? 0) + 1
660-
);
657+
this.beginRemoteEdit(notebookPath);
661658
try {
662659
const edit = new vscode.WorkspaceEdit();
663660
edit.set(notebook.uri, [
@@ -668,16 +665,71 @@ export class PlutoNotebookController {
668665
]);
669666
await vscode.workspace.applyEdit(edit);
670667
} finally {
671-
// Change events may be delivered after applyEdit resolves — release
672-
// the suppression on the next tick
673-
setTimeout(() => {
674-
const depth = this.remoteEditDepth.get(notebookPath) ?? 1;
675-
if (depth <= 1) {
676-
this.remoteEditDepth.delete(notebookPath);
677-
} else {
678-
this.remoteEditDepth.set(notebookPath, depth - 1);
679-
}
680-
}, 0);
668+
this.endRemoteEditSoon(notebookPath);
669+
}
670+
}
671+
672+
private beginRemoteEdit(notebookPath: string): void {
673+
this.remoteEditDepth.set(
674+
notebookPath,
675+
(this.remoteEditDepth.get(notebookPath) ?? 0) + 1
676+
);
677+
}
678+
679+
/**
680+
* Releases remote-edit suppression on the next tick — change events may
681+
* be delivered after applyEdit resolves.
682+
*/
683+
private endRemoteEditSoon(notebookPath: string): void {
684+
setTimeout(() => {
685+
const depth = this.remoteEditDepth.get(notebookPath) ?? 1;
686+
if (depth <= 1) {
687+
this.remoteEditDepth.delete(notebookPath);
688+
} else {
689+
this.remoteEditDepth.set(notebookPath, depth - 1);
690+
}
691+
}, 0);
692+
}
693+
694+
/**
695+
* Applies a Pluto-side code edit (browser UI, MCP update_cell) to the
696+
* matching VSCode cell's text. Our own executeCell pushes echo back as
697+
* the same patch — the text-equality check drops those.
698+
*/
699+
private async _applyRemoteCodeEdit(
700+
notebook: vscode.NotebookDocument,
701+
cellId: CellId,
702+
rawCode: string
703+
): Promise<void> {
704+
const cell = this.getCellByPlutoId(notebook, cellId);
705+
if (!cell) {
706+
return;
707+
}
708+
let newText = rawCode ?? "";
709+
if (cell.kind === vscode.NotebookCellKind.Markup) {
710+
const markdown = extractMarkdownContent(newText);
711+
if (isDefined(markdown)) {
712+
newText = markdown;
713+
}
714+
}
715+
if (cell.document.getText() === newText) {
716+
return;
717+
}
718+
const notebookPath = notebook.uri.fsPath;
719+
this.beginRemoteEdit(notebookPath);
720+
try {
721+
const edit = new vscode.WorkspaceEdit();
722+
edit.replace(
723+
cell.document.uri,
724+
new vscode.Range(0, 0, cell.document.lineCount, 0),
725+
newText
726+
);
727+
await vscode.workspace.applyEdit(edit);
728+
this.outputChannel.appendLine(
729+
`[CodeSync] Cell ${cellId} code updated from Pluto`
730+
);
731+
} finally {
732+
this.endRemoteEditSoon(notebookPath);
681733
}
682734
}
683735

@@ -776,16 +828,23 @@ export class PlutoNotebookController {
776828
);
777829
break;
778830
}
779-
case "cell_input": {
831+
case "cell_inputs": {
780832
if (rest[1] === "code" && patch.op === "replace") {
781-
// TODO here we need to update the code for the cell
833+
void this._applyRemoteCodeEdit(
834+
notebook,
835+
rest[0] as CellId,
836+
patch.value as string
837+
).catch((error) => {
838+
this.outputChannel.appendLine(
839+
`[CodeSync] Failed for ${rest[0]}: ${
840+
error instanceof Error ? error.message : String(error)
841+
}`
842+
);
843+
});
782844
}
783-
784-
this.outputChannel.appendLine(
785-
`[UNHANDLED] cell_input ${patch.path.join(".")} action ${
786-
patch.op
787-
}`
788-
);
845+
// Other cell_inputs fields (cell_id, code_folded, metadata)
846+
// have no document representation to update; structural
847+
// add/remove is handled via cell_order sync
789848
break;
790849
}
791850
case "cell_results":

0 commit comments

Comments
 (0)