@aliceshimada/mica 1.1.0 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 - 2026-06-16
4
+
5
+ - Add `mma_create_notebook` tool: create a new blank notebook in the Wolfram FrontEnd.
6
+ - Add `mma_open_notebook` tool: open an existing notebook file (.nb) from disk.
7
+ - Add `CreateNotebook` and `OpenNotebook` permissions (default false).
8
+ - Fix agent-level tools when no live notebook exists for routing.
9
+
10
+ ## 1.1.1 - 2026-06-16
11
+
12
+ - Fix `RestartKernelRequest`: kill kernel via `Quit[]` before restarting.
13
+ - Fix `realpathSync` crash when `process.argv[1]` doesn't resolve.
14
+ - Fix `$BridgeNotebookPermissions` memory leak: prune on notebook close.
15
+
3
16
  ## 1.1.0 - 2026-06-09
4
17
 
5
18
  - Add `mma_kill_kernel` tool: quit a notebook's Wolfram kernel (control agent kernel is protected).
@@ -3,7 +3,7 @@ import http from "node:http";
3
3
  import { executeBackendMcpTool } from "../mcp/backendTools.js";
4
4
  import { renderDashboard } from "./dashboard.js";
5
5
  const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
6
- const DEFAULT_VERSION = "1.1.0";
6
+ const DEFAULT_VERSION = "1.2.0";
7
7
  export async function createBunHttpApp({ state, host = "127.0.0.1", port, authToken, version = DEFAULT_VERSION }) {
8
8
  const runtimeInfo = {
9
9
  host,
@@ -338,7 +338,7 @@ function readPermissions(value) {
338
338
  if (typeof value !== "object" || value === null || Array.isArray(value))
339
339
  throw new Error("BAD_REQUEST");
340
340
  const record = value;
341
- const keys = ["ReadNotebook", "InsertCell", "ModifyCell", "DeleteCell", "RunCell", "SaveNotebook"];
341
+ const keys = ["ReadNotebook", "InsertCell", "ModifyCell", "DeleteCell", "RunCell", "SaveNotebook", "CreateNotebook", "OpenNotebook"];
342
342
  const permissions = {};
343
343
  for (const key of keys) {
344
344
  if (typeof record[key] !== "boolean")
@@ -8,7 +8,7 @@ import { loadRuntimeConfig } from "../runtime/config.js";
8
8
  import { writeSessionFile } from "../runtime/session.js";
9
9
  import { createBunHttpApp } from "./httpServer.js";
10
10
  const MCP_SERVER_NAME = "mica-bun";
11
- const MICA_PACKAGE_VERSION = "1.1.0";
11
+ const MICA_PACKAGE_VERSION = "1.2.0";
12
12
  export async function startBunRuntime(deps = {}) {
13
13
  const config = deps.runtimeConfig ?? loadRuntimeConfig();
14
14
  const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
@@ -223,13 +223,18 @@ async function main() {
223
223
  process.exitCode = exitCode;
224
224
  }
225
225
  if (process.argv[1]) {
226
- const scriptReal = realpathSync(fileURLToPath(import.meta.url));
227
- const argReal = realpathSync(process.argv[1]);
228
- if (scriptReal === argReal) {
229
- main().catch((error) => {
230
- const message = error instanceof Error ? error.stack ?? error.message : String(error);
231
- process.stderr.write(`${message}\n`);
232
- process.exitCode = 1;
233
- });
226
+ try {
227
+ const scriptReal = realpathSync(fileURLToPath(import.meta.url));
228
+ const argReal = realpathSync(process.argv[1]);
229
+ if (scriptReal === argReal) {
230
+ main().catch((error) => {
231
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
232
+ process.stderr.write(`${message}\n`);
233
+ process.exitCode = 1;
234
+ });
235
+ }
236
+ }
237
+ catch {
238
+ // process.argv[1] may not resolve (e.g. node -e, shebang edge cases)
234
239
  }
235
240
  }
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DEFAULT_TIMEOUTS_MS } from "../backend/protocol.js";
3
- import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, killKernelSchema, listCellsSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, restartKernelSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
3
+ import { abortEvaluationSchema, createNotebookSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, killKernelSchema, listCellsSchema, modifyCellSchema, noArgsSchema, openNotebookSchema, readArtifactSchema, readCellSchema, restartKernelSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
4
4
  import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
5
5
  import { toolFailure, toolSuccess, withToolErrors } from "./toolResults.js";
6
6
  function assertLiveAgent(state) {
@@ -196,6 +196,16 @@ export const MICA_BACKEND_TOOL_DEFINITIONS = [
196
196
  description: notebookToolDescription("Select the active Mathematica notebook in the backend registry."),
197
197
  schema: selectNotebookSchema.shape,
198
198
  },
199
+ {
200
+ name: "mma_create_notebook",
201
+ description: notebookToolDescription("Create a new blank notebook in the Wolfram FrontEnd with the given window title."),
202
+ schema: createNotebookSchema.shape,
203
+ },
204
+ {
205
+ name: "mma_open_notebook",
206
+ description: notebookToolDescription("Open an existing notebook file (.nb) from disk in the Wolfram FrontEnd."),
207
+ schema: openNotebookSchema.shape,
208
+ },
199
209
  ...queuedNotebookTools.map((config) => ({
200
210
  name: config.name,
201
211
  description: notebookToolDescription(config.summary, config.extraGuidance),
@@ -231,6 +241,28 @@ export async function executeBackendMcpTool(state, tool, args = {}, extra) {
231
241
  return toolSuccess({ activeNotebookId: state.activeNotebookId, notebook: target.notebook });
232
242
  });
233
243
  }
244
+ // Agent-level tools: route to any live agent without requiring a notebook target
245
+ if (tool === "mma_create_notebook" || tool === "mma_open_notebook") {
246
+ return withToolErrors({ tool, args: recordArgs }, () => {
247
+ sweepStateLiveness(state);
248
+ const live = state.agents.list().find((a) => !a.offline && !a.retired);
249
+ if (!live)
250
+ throw new Error("NO_LIVE_AGENT");
251
+ const notebook = state.notebooks.listLive().find((n) => n.agentSessionId === live.agentSessionId);
252
+ const targetNotebookId = notebook?.notebookId ?? `__agent__${live.agentSessionId}`;
253
+ const requestId = randomUUID();
254
+ state.queue.enqueue({
255
+ requestId,
256
+ tool,
257
+ arguments: recordArgs,
258
+ targetNotebookId,
259
+ agentSessionId: live.agentSessionId,
260
+ timeoutMs: DEFAULT_TIMEOUTS_MS.mutation,
261
+ createdAt: Date.now(),
262
+ });
263
+ return toolSuccess({ status: "queued", requestId });
264
+ });
265
+ }
234
266
  const queuedConfig = queuedNotebookTools.find((config) => config.name === tool);
235
267
  if (!queuedConfig) {
236
268
  return toolFailure(new Error(`UNKNOWN_TOOL: ${tool}`), { tool, args: recordArgs });
@@ -14,6 +14,8 @@ const TOOL_GUIDE = [
14
14
  ["mma_abort_evaluation", "Abort a running notebook evaluation."],
15
15
  ["mma_kill_kernel", "Quit the Wolfram kernel for a notebook (control agent kernel is protected)."],
16
16
  ["mma_restart_kernel", "Restart the Wolfram kernel for a notebook so it can evaluate cells again."],
17
+ ["mma_create_notebook", "Create a new blank notebook in the Wolfram FrontEnd."],
18
+ ["mma_open_notebook", "Open an existing notebook file (.nb) from disk in the Wolfram FrontEnd."],
17
19
  ["mma_get_cell_output", "Read output and messages produced by one cell; this may refresh completed run status."],
18
20
  ["mma_read_artifact", "Read large output or message artifacts by byte page; ids may become stale after notebook edits or reruns."],
19
21
  ["mma_save_notebook", "Save the selected notebook when SaveNotebook permission is granted."],
@@ -35,7 +37,7 @@ export const MICA_AGENT_INSTRUCTIONS = [
35
37
  "Tools:",
36
38
  ...TOOL_GUIDE.map(([name, description]) => `- ${name}: ${description}`),
37
39
  ].join("\n");
38
- export function createMicaMcpServer(name, version = "1.1.0") {
40
+ export function createMicaMcpServer(name, version = "1.2.0") {
39
41
  return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
40
42
  }
41
43
  export function registerMicaPrompts(server) {
@@ -44,6 +44,12 @@ export const killKernelSchema = z.object({
44
44
  export const restartKernelSchema = z.object({
45
45
  ...notebookSelectorFields
46
46
  }).strict();
47
+ export const createNotebookSchema = z.object({
48
+ title: z.string().min(1).describe("Window title for the new notebook")
49
+ }).strict();
50
+ export const openNotebookSchema = z.object({
51
+ path: z.string().min(1).describe("Absolute path to an existing .nb file")
52
+ }).strict();
47
53
  export const getCellOutputSchema = z.object({
48
54
  ...notebookSelectorFields,
49
55
  cellId: z.string().min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliceshimada/mica",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Local MCP bridge for controlling live Wolfram Desktop / Mathematica notebooks.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,3 +1,5 @@
1
+ (* ::Package:: *)
2
+
1
3
  BeginPackage["MMAAgentBridge`"];
2
4
 
3
5
  StartMMAAgentPalette::usage = "StartMMAAgentPalette[] opens the MMA Agent Bridge palette.";
@@ -15,10 +17,10 @@ PollCancellations::usage = "PollCancellations[] polls palette-originated cancell
15
17
 
16
18
  Begin["`Private`"];
17
19
 
18
- $DefaultBridgeBaseURL = "http://127.0.0.1:19791";
19
- $BridgeBaseURL = $DefaultBridgeBaseURL;
20
- $BridgeAuthToken = None;
21
- $BridgeSessionFile = Automatic;
20
+ $DefaultBridgeBaseURL = "http://127.0.0.1:19791";
21
+ $BridgeBaseURL = $DefaultBridgeBaseURL;
22
+ $BridgeAuthToken = None;
23
+ $BridgeSessionFile = Automatic;
22
24
  $BridgeNotebooks = <||>;
23
25
  $ActiveNotebookId = None;
24
26
  $PaletteId = CreateUUID["palette-"];
@@ -33,24 +35,24 @@ $RunningNotebookId = None;
33
35
  $RunningNotebookObject = None;
34
36
  $RunningCellObject = None;
35
37
  $RunningCellOriginalEpilogRule = HoldComplete[CellEpilog -> Inherited];
36
- $RunningCellRestoreEpilogRule = HoldComplete[CellEpilog -> Inherited];
37
- $RunningStartedAt = None;
38
- $RunningStatus = None;
39
- $RunningStatusGraceSeconds = 2.0;
40
- $RunningTimeoutAt = None;
41
- $AbortRequestedAt = None;
42
- (* Reserved for future late-result surfacing; backend queue already rejects late timeout/cancel results. *)
43
- $LastLateResult = None;
44
- $LastRunStatusCellId = None;
38
+ $RunningCellRestoreEpilogRule = HoldComplete[CellEpilog -> Inherited];
39
+ $RunningStartedAt = None;
40
+ $RunningStatus = None;
41
+ $RunningStatusGraceSeconds = 2.0;
42
+ $RunningTimeoutAt = None;
43
+ $AbortRequestedAt = None;
44
+ (* Reserved for future late-result surfacing; backend queue already rejects late timeout/cancel results. *)
45
+ $LastLateResult = None;
46
+ $LastRunStatusCellId = None;
45
47
  $LastRunStatusNotebookId = None;
46
48
  $LastRunStatus = None;
47
49
  $LastStatus = <||>;
48
- $LastError = None;
49
- $PollingInProgress = False;
50
- $MaxArtifactScanCells = 20;
51
- $DefaultMaxCellPayloadBytes = 262144;
52
- $MaxCellPayloadBytes = 1024 * 1024;
53
- $BridgeHTTPTimeoutSeconds = 30;
50
+ $LastError = None;
51
+ $PollingInProgress = False;
52
+ $MaxArtifactScanCells = 20;
53
+ $DefaultMaxCellPayloadBytes = 262144;
54
+ $MaxCellPayloadBytes = 1024 * 1024;
55
+ $BridgeHTTPTimeoutSeconds = 30;
54
56
  $BridgeHTTPRetryCount = 3;
55
57
  $BridgeHTTPRetryDelaySeconds = 0.25;
56
58
  $BridgeInbox = {};
@@ -68,43 +70,43 @@ $MMAAgentBridgeSourceFile = If[StringQ[$InputFileName] && StringLength[$InputFil
68
70
  $ControlAgentNotebook = None;
69
71
  $ControlAgentEvaluatorName = "MMAAgentControl";
70
72
 
71
- UnixTimeMilliseconds[] := Round[1000 UnixTime[]];
72
-
73
- NonEmptyStringQ[value_] := StringQ[value] && StringLength[value] > 0;
74
-
75
- EnvironmentValue[name_String] := Module[{value},
76
- value = Quiet @ Check[Environment[name], ""];
77
- If[NonEmptyStringQ[value], value, ""]
78
- ];
79
-
80
- DefaultBridgeSessionFile[] := Module[{override, home},
81
- override = EnvironmentValue["MICA_SESSION_FILE"];
82
- If[NonEmptyStringQ[override], Return[override]];
83
- home = EnvironmentValue["HOME"];
84
- If[!NonEmptyStringQ[home], home = EnvironmentValue["USERPROFILE"]];
85
- If[!NonEmptyStringQ[home], home = Directory[]];
86
- FileNameJoin[{home, ".mica", "session.json"}]
87
- ];
88
-
89
- If[$BridgeSessionFile === Automatic, $BridgeSessionFile = DefaultBridgeSessionFile[]];
90
-
91
- LoadBridgeSession[] := Module[{sessionFile = $BridgeSessionFile, payload},
92
- If[!StringQ[sessionFile] || !FileExistsQ[sessionFile], Return[<||>]];
93
- payload = Quiet @ Check[Import[sessionFile, "RawJSON"], $Failed];
94
- If[AssociationQ[payload], payload, <||>]
95
- ];
96
-
97
- ConfigureBridgeFromSession[] := Module[{session, baseUrl, token},
98
- session = LoadBridgeSession[];
99
- baseUrl = Lookup[session, "baseUrl", None];
100
- If[NonEmptyStringQ[baseUrl],
101
- $BridgeBaseURL = baseUrl,
102
- $BridgeBaseURL = $DefaultBridgeBaseURL
103
- ];
104
- token = Lookup[session, "authToken", None];
105
- $BridgeAuthToken = If[NonEmptyStringQ[token], token, None];
106
- $BridgeBaseURL
107
- ];
73
+ UnixTimeMilliseconds[] := Round[1000 UnixTime[]];
74
+
75
+ NonEmptyStringQ[value_] := StringQ[value] && StringLength[value] > 0;
76
+
77
+ EnvironmentValue[name_String] := Module[{value},
78
+ value = Quiet @ Check[Environment[name], ""];
79
+ If[NonEmptyStringQ[value], value, ""]
80
+ ];
81
+
82
+ DefaultBridgeSessionFile[] := Module[{override, home},
83
+ override = EnvironmentValue["MICA_SESSION_FILE"];
84
+ If[NonEmptyStringQ[override], Return[override]];
85
+ home = EnvironmentValue["HOME"];
86
+ If[!NonEmptyStringQ[home], home = EnvironmentValue["USERPROFILE"]];
87
+ If[!NonEmptyStringQ[home], home = Directory[]];
88
+ FileNameJoin[{home, ".mica", "session.json"}]
89
+ ];
90
+
91
+ If[$BridgeSessionFile === Automatic, $BridgeSessionFile = DefaultBridgeSessionFile[]];
92
+
93
+ LoadBridgeSession[] := Module[{sessionFile = $BridgeSessionFile, payload},
94
+ If[!StringQ[sessionFile] || !FileExistsQ[sessionFile], Return[<||>]];
95
+ payload = Quiet @ Check[Import[sessionFile, "RawJSON"], $Failed];
96
+ If[AssociationQ[payload], payload, <||>]
97
+ ];
98
+
99
+ ConfigureBridgeFromSession[] := Module[{session, baseUrl, token},
100
+ session = LoadBridgeSession[];
101
+ baseUrl = Lookup[session, "baseUrl", None];
102
+ If[NonEmptyStringQ[baseUrl],
103
+ $BridgeBaseURL = baseUrl,
104
+ $BridgeBaseURL = $DefaultBridgeBaseURL
105
+ ];
106
+ token = Lookup[session, "authToken", None];
107
+ $BridgeAuthToken = If[NonEmptyStringQ[token], token, None];
108
+ $BridgeBaseURL
109
+ ];
108
110
 
109
111
  (* $BridgePermissions replaces the plan's $Permissions name because *)
110
112
  (* $Permissions is a protected Wolfram built-in symbol (Set::wrsym). *)
@@ -115,18 +117,40 @@ $DefaultBridgePermissions = <|
115
117
  "ModifyCell" -> False,
116
118
  "DeleteCell" -> False,
117
119
  "RunCell" -> False,
118
- "SaveNotebook" -> False
119
- |>;
120
+ "SaveNotebook" -> False,
121
+ "CreateNotebook" -> False,
122
+ "OpenNotebook" -> False
123
+ |>;
120
124
 
121
125
  If[!AssociationQ[Quiet @ Check[$BridgePermissions, None]],
122
126
  $BridgePermissions = $DefaultBridgePermissions
123
127
  ];
124
128
 
125
- PalettePermissionRow[label_String, key_String] := Row[{
126
- Checkbox[Dynamic[$BridgePermissions[key], ($BridgePermissions[key] = #; Quiet @ Check[PostPermissions[], Null]) &]],
127
- Spacer[8],
128
- Style[label, 11]
129
- }];
129
+ If[!AssociationQ[Quiet @ Check[$BridgeNotebookPermissions, None]],
130
+ $BridgeNotebookPermissions = <||>
131
+ ];
132
+
133
+ NotebookPermissions[notebookId_String] := Lookup[$BridgeNotebookPermissions, notebookId, $DefaultBridgePermissions];
134
+
135
+ SetNotebookPermissions[notebookId_String, perms_Association] := (
136
+ $BridgeNotebookPermissions[notebookId] = perms
137
+ );
138
+
139
+ PalettePermissionRow[label_String, key_String, notebookId_:None] := Module[{perms, setter},
140
+ perms = If[StringQ[notebookId] && StringLength[notebookId] > 0,
141
+ NotebookPermissions[notebookId],
142
+ $BridgePermissions
143
+ ];
144
+ setter = If[StringQ[notebookId] && StringLength[notebookId] > 0,
145
+ Function[val, SetNotebookPermissions[notebookId, Join[perms, <|key -> val|>]]],
146
+ Function[val, $BridgePermissions[key] = val; Quiet @ Check[PostPermissions[], Null]]
147
+ ];
148
+ Row[{
149
+ Checkbox[Dynamic[perms[key], (setter[#]) &]],
150
+ Spacer[8],
151
+ Style[label, 11]
152
+ }]
153
+ ];
130
154
 
131
155
  PaletteStatusSummary[] := Module[{server, paletteConnected, notebookAttached, pendingRequests, attachedNotebook, error},
132
156
  server = Lookup[$LastStatus, "server", "unknown"];
@@ -181,7 +205,7 @@ NotebookDisplayName[record_Association] := Module[{info, title, path, notebookId
181
205
  path = StringTrim[ToString[Lookup[info, "notebookPath", ""]]];
182
206
  If[title === "" && path === "",
183
207
  If[StringQ[notebookId] && StringLength[notebookId] > 0, notebookId, "Untitled notebook"],
184
- If[path === "", title, title <> " " <> FileNameTake[path]]
208
+ If[path === "", title, title <> " \[LongDash] " <> FileNameTake[path]]
185
209
  ]
186
210
  ];
187
211
 
@@ -291,7 +315,7 @@ RuntimeStatusCard[] := Module[{paletteConnected, transportMode, executorState, p
291
315
  Style["Running request: ", Bold],
292
316
  ToString[Lookup[runningRequest, "tool", "request"]],
293
317
  If[StringQ[Lookup[runningRequest, "requestId", ""]], " (" <> Lookup[runningRequest, "requestId", ""] <> ")", Nothing],
294
- If[NumberQ[elapsedSeconds], " " <> ToString[elapsedSeconds] <> "s", Nothing]
318
+ If[NumberQ[elapsedSeconds], " \[Bullet] " <> ToString[elapsedSeconds] <> "s", Nothing]
295
319
  },
296
320
  Nothing
297
321
  ]],
@@ -332,14 +356,16 @@ PermissionsPanel[] := Panel[
332
356
  Column[
333
357
  {
334
358
  Style["Permissions", Bold],
335
- Grid[
359
+ Dynamic @ Grid[
336
360
  {
337
- {PalettePermissionRow["Read notebook", "ReadNotebook"]},
338
- {PalettePermissionRow["Insert cell", "InsertCell"]},
339
- {PalettePermissionRow["Modify cell", "ModifyCell"]},
340
- {PalettePermissionRow["Delete cell", "DeleteCell"]},
341
- {PalettePermissionRow["Run cell", "RunCell"]},
342
- {PalettePermissionRow["Save notebook", "SaveNotebook"]}
361
+ {PalettePermissionRow["Read notebook", "ReadNotebook", $ActiveNotebookId]},
362
+ {PalettePermissionRow["Insert cell", "InsertCell", $ActiveNotebookId]},
363
+ {PalettePermissionRow["Modify cell", "ModifyCell", $ActiveNotebookId]},
364
+ {PalettePermissionRow["Delete cell", "DeleteCell", $ActiveNotebookId]},
365
+ {PalettePermissionRow["Run cell", "RunCell", $ActiveNotebookId]},
366
+ {PalettePermissionRow["Save notebook", "SaveNotebook", $ActiveNotebookId]},
367
+ {PalettePermissionRow["Create notebook", "CreateNotebook", $ActiveNotebookId]},
368
+ {PalettePermissionRow["Open notebook", "OpenNotebook", $ActiveNotebookId]}
343
369
  },
344
370
  Alignment -> Left,
345
371
  Spacings -> {1, 0.35}
@@ -499,14 +525,14 @@ PaletteView[] := DynamicModule[{},
499
525
  ]
500
526
  ];
501
527
 
502
- (* Re-read the small session file before each request so Wolfram follows MCP restarts and dynamic ports. *)
503
- BridgeURL[path_String] := ConfigureBridgeFromSession[] <> path;
504
-
505
- BridgeHeaders[] := If[
506
- StringQ[$BridgeAuthToken] && StringLength[$BridgeAuthToken] > 0,
507
- {"Authorization" -> "Bearer " <> $BridgeAuthToken},
508
- {}
509
- ];
528
+ (* Re-read the small session file before each request so Wolfram follows MCP restarts and dynamic ports. *)
529
+ BridgeURL[path_String] := ConfigureBridgeFromSession[] <> path;
530
+
531
+ BridgeHeaders[] := If[
532
+ StringQ[$BridgeAuthToken] && StringLength[$BridgeAuthToken] > 0,
533
+ {"Authorization" -> "Bearer " <> $BridgeAuthToken},
534
+ {}
535
+ ];
510
536
 
511
537
  URLComponentEncodeString[None] := "";
512
538
  URLComponentEncodeString[value_] := StringReplace[
@@ -564,8 +590,8 @@ JsonByteArrayToPayload[body_ByteArray] := Module[{text},
564
590
  Quiet @ Check[ImportString[text, "RawJSON"], $Failed]
565
591
  ];
566
592
 
567
- BridgeGet[path_String] := Module[{response},
568
- response = BridgeRequestWithRetries[HTTPRequest[BridgeURL[path], <|"Method" -> "GET", "Headers" -> BridgeHeaders[]|>]];
593
+ BridgeGet[path_String] := Module[{response},
594
+ response = BridgeRequestWithRetries[HTTPRequest[BridgeURL[path], <|"Method" -> "GET", "Headers" -> BridgeHeaders[]|>]];
569
595
  If[response === $Failed, Return[$Failed]];
570
596
  JsonByteArrayToPayload[response["BodyByteArray"]]
571
597
  ];
@@ -575,9 +601,9 @@ BridgePost[path_String, payload_Association] := Module[{response},
575
601
  HTTPRequest[
576
602
  BridgeURL[path],
577
603
  <|
578
- "Method" -> "POST",
579
- "Headers" -> BridgeHeaders[],
580
- "ContentType" -> "application/json; charset=utf-8",
604
+ "Method" -> "POST",
605
+ "Headers" -> BridgeHeaders[],
606
+ "ContentType" -> "application/json; charset=utf-8",
581
607
  "Body" -> PayloadToJsonBytes[payload]
582
608
  |>
583
609
  ]
@@ -590,10 +616,16 @@ PostPermissions[] := Module[{payload = <|"permissions" -> $BridgePermissions|>},
590
616
  Quiet @ Check[BridgePost["/permissions", payload], $Failed]
591
617
  ];
592
618
 
593
- NeedsConfirmationQ[action_String] := Not @ TrueQ[$BridgePermissions[action]];
619
+ NeedsConfirmationQ[action_String, notebookId_:None] := Module[{perms},
620
+ perms = If[StringQ[notebookId] && StringLength[notebookId] > 0,
621
+ NotebookPermissions[notebookId],
622
+ $BridgePermissions
623
+ ];
624
+ Not @ TrueQ[perms[action]]
625
+ ];
594
626
 
595
- ConfirmAction[action_String, message_String] := If[
596
- NeedsConfirmationQ[action],
627
+ ConfirmAction[action_String, message_String, notebookId_:None] := If[
628
+ NeedsConfirmationQ[action, notebookId],
597
629
  ChoiceDialog[message, {"Allow" -> True, "Deny" -> False}],
598
630
  True
599
631
  ];
@@ -606,8 +638,8 @@ FailedRequestCode[failure_Failure] := Module[{code = failure[[1]]},
606
638
 
607
639
  FailedRequestMessage[failure_Failure] := Lookup[failure[[2]], "Message", Lookup[failure[[2]], "message", "The Wolfram bridge rejected the request."]];
608
640
 
609
- RequireReadPermission[] := If[
610
- Not @ ConfirmAction["ReadNotebook", "AI requests reading the notebook. Allow?"],
641
+ RequireReadPermission[notebookId_:None] := If[
642
+ Not @ ConfirmAction["ReadNotebook", "AI requests reading the notebook. Allow?", notebookId],
611
643
  $Canceled,
612
644
  True
613
645
  ];
@@ -628,7 +660,7 @@ NotebookInfo[nb_NotebookObject, notebookId_String] := <|
628
660
  "wolframVersion" -> ToString @ $VersionNumber,
629
661
  "platform" -> $OperatingSystem,
630
662
  "paletteId" -> $PaletteId,
631
- "permissions" -> $BridgePermissions
663
+ "permissions" -> NotebookPermissions[notebookId]
632
664
  |>;
633
665
 
634
666
  NotebookRecord[notebookId_String] := Lookup[$BridgeNotebooks, notebookId, <||>];
@@ -799,141 +831,141 @@ CellPayload[cell_CellObject, id_String, index_Integer] := <|
799
831
  "tags" -> CellTagsList[cell]
800
832
  |>;
801
833
 
802
- CellGeneratedBoundaryQ[style_String] := MemberQ[{"Input", "Code", "Text", "Section", "Subsection", "Subsubsection", "Title", "Chapter"}, style];
803
-
804
- CellArtifactStyleQ[style_String] := MemberQ[{"Output", "Print", "Message"}, style];
805
-
806
- CellPayloadMaxBytes[args_Association] := Module[{value = Lookup[args, "maxBytes", $DefaultMaxCellPayloadBytes]},
807
- If[IntegerQ[value] && value > 0 && value <= $MaxCellPayloadBytes, value, $DefaultMaxCellPayloadBytes]
808
- ];
809
-
810
- Utf8LeadByteLength[byte_Integer] := Which[
811
- byte < 128, 1,
812
- byte < 224, 2,
813
- byte < 240, 3,
814
- byte < 248, 4,
815
- True, 1
816
- ];
817
-
818
- Utf8PrefixByteLength[bytes_List, maxBytes_Integer] := Module[{safeLength = Min[maxBytes, Length[bytes]], start, lead, expected},
819
- If[safeLength <= 0, Return[0]];
820
- start = safeLength;
821
- While[start > 1 && bytes[[start]] >= 128 && bytes[[start]] <= 191, start--];
822
- lead = bytes[[start]];
823
- expected = Utf8LeadByteLength[lead];
824
- If[safeLength - start + 1 >= expected, safeLength, Max[0, start - 1]]
825
- ];
826
-
827
- TruncateStringToUtf8Bytes[text_String, maxBytes_Integer] := Module[{originalBytes, originalByteLength, safeLength, returnedText},
828
- originalBytes = ToCharacterCode[text, "UTF8"];
829
- originalByteLength = Length[originalBytes];
830
- If[maxBytes <= 0,
831
- Return[<|"value" -> "", "truncated" -> (originalByteLength > 0), "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0|>]
832
- ];
833
- If[originalByteLength <= maxBytes,
834
- Return[<|"value" -> text, "truncated" -> False, "originalByteLength" -> originalByteLength, "returnedByteLength" -> originalByteLength|>]
835
- ];
836
- safeLength = Utf8PrefixByteLength[originalBytes, maxBytes];
837
- returnedText = If[safeLength > 0, ByteArrayToString[ByteArray[Take[originalBytes, safeLength]], "UTF8"], ""];
838
- <|"value" -> returnedText, "truncated" -> True, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength|>
839
- ];
840
-
841
- TruncatePayloadFields[content_String, outputs_List, messages_List, maxBytes_Integer, includeContentQ_] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, truncatedContent = "", truncatedOutputs = {}, truncatedMessages = {}, processString, output, message},
842
- processString[text_String] := Module[{item = TruncateStringToUtf8Bytes[text, remainingByteLength]},
843
- totalOriginalByteLength += item["originalByteLength"];
844
- totalReturnedByteLength += item["returnedByteLength"];
845
- anyTruncated = anyTruncated || TrueQ[item["truncated"]];
846
- remainingByteLength = Max[0, remainingByteLength - item["returnedByteLength"]];
847
- item["value"]
848
- ];
849
- If[TrueQ[includeContentQ], truncatedContent = processString[content]];
850
- Do[
851
- AppendTo[truncatedOutputs, processString[If[StringQ[output], output, ToString[output, InputForm]]]],
852
- {output, outputs}
853
- ];
854
- Do[
855
- AppendTo[truncatedMessages, processString[If[StringQ[message], message, ToString[message, InputForm]]]],
856
- {message, messages}
857
- ];
858
- Join[
859
- If[TrueQ[includeContentQ], <|"content" -> truncatedContent|>, <||>],
860
- <|
861
- "outputs" -> truncatedOutputs,
862
- "messages" -> truncatedMessages,
863
- "truncated" -> anyTruncated,
864
- "originalByteLength" -> totalOriginalByteLength,
865
- "returnedByteLength" -> totalReturnedByteLength
866
- |>
867
- ]
868
- ];
869
-
870
- ArtifactId[cellId_String, kind_String, index_Integer] := cellId <> ":" <> kind <> ":" <> ToString[index];
871
-
872
- ArtifactDescriptor[cellId_String, kind_String, index_Integer, text_String, maxBytes_Integer] := Module[{byteLength, preview},
873
- byteLength = Length[ToCharacterCode[text, "UTF8"]];
874
- preview = TruncateStringToUtf8Bytes[text, maxBytes];
875
- <|
876
- "artifactId" -> ArtifactId[cellId, kind, index],
877
- "type" -> kind,
878
- "index" -> index,
879
- "byteLength" -> byteLength,
880
- "preview" -> preview["value"],
881
- "previewByteLength" -> preview["returnedByteLength"],
882
- "truncated" -> True
883
- |>
884
- ];
885
-
886
- ArtifactPayloadFields[cellId_String, outputs_List, messages_List, maxBytes_Integer] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, processedOutputs = {}, processedMessages = {}, processString, output, message},
887
- processString[text_, kind_String, index_Integer] := Module[{value = If[StringQ[text], text, ToString[text, InputForm]], byteLength, descriptor},
888
- byteLength = Length[ToCharacterCode[value, "UTF8"]];
889
- totalOriginalByteLength += byteLength;
890
- If[byteLength <= remainingByteLength,
891
- totalReturnedByteLength += byteLength;
892
- remainingByteLength = Max[0, remainingByteLength - byteLength];
893
- value,
894
- anyTruncated = True;
895
- descriptor = ArtifactDescriptor[cellId, kind, index, value, remainingByteLength];
896
- totalReturnedByteLength += descriptor["previewByteLength"];
897
- remainingByteLength = Max[0, remainingByteLength - descriptor["previewByteLength"]];
898
- descriptor
899
- ]
900
- ];
901
- Do[
902
- AppendTo[processedOutputs, processString[outputs[[i]], "output", i - 1]],
903
- {i, Length[outputs]}
904
- ];
905
- Do[
906
- AppendTo[processedMessages, processString[messages[[i]], "message", i - 1]],
907
- {i, Length[messages]}
908
- ];
909
- <|
910
- "outputs" -> processedOutputs,
911
- "messages" -> processedMessages,
912
- "truncated" -> anyTruncated,
913
- "originalByteLength" -> totalOriginalByteLength,
914
- "returnedByteLength" -> totalReturnedByteLength
915
- |>
916
- ];
917
-
918
- Utf8SliceStringToBytes[text_String, offset_Integer, limit_Integer] := Module[{bytes, originalByteLength, start, available, safeLength, sliceBytes, value},
919
- bytes = ToCharacterCode[text, "UTF8"];
920
- originalByteLength = Length[bytes];
921
- If[limit <= 0 || offset >= originalByteLength,
922
- Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
923
- ];
924
- start = Max[1, offset + 1];
925
- While[start <= originalByteLength && bytes[[start]] >= 128 && bytes[[start]] <= 191, start++];
926
- available = originalByteLength - start + 1;
927
- If[available <= 0,
928
- Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
929
- ];
930
- safeLength = Utf8PrefixByteLength[Take[bytes, {start, originalByteLength}], Min[limit, available]];
931
- sliceBytes = If[safeLength > 0, Take[bytes, {start, start + safeLength - 1}], {}];
932
- value = If[safeLength > 0, ByteArrayToString[ByteArray[sliceBytes], "UTF8"], ""];
933
- <|"value" -> value, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength, "nextOffset" -> (start - 1 + safeLength)|>
934
- ];
935
-
936
- CellEvaluationTaggingPath[cellId_String] := {TaggingRules, "MMAAgentBridge", "evaluations", cellId, "complete"};
834
+ CellGeneratedBoundaryQ[style_String] := MemberQ[{"Input", "Code", "Text", "Section", "Subsection", "Subsubsection", "Title", "Chapter"}, style];
835
+
836
+ CellArtifactStyleQ[style_String] := MemberQ[{"Output", "Print", "Message"}, style];
837
+
838
+ CellPayloadMaxBytes[args_Association] := Module[{value = Lookup[args, "maxBytes", $DefaultMaxCellPayloadBytes]},
839
+ If[IntegerQ[value] && value > 0 && value <= $MaxCellPayloadBytes, value, $DefaultMaxCellPayloadBytes]
840
+ ];
841
+
842
+ Utf8LeadByteLength[byte_Integer] := Which[
843
+ byte < 128, 1,
844
+ byte < 224, 2,
845
+ byte < 240, 3,
846
+ byte < 248, 4,
847
+ True, 1
848
+ ];
849
+
850
+ Utf8PrefixByteLength[bytes_List, maxBytes_Integer] := Module[{safeLength = Min[maxBytes, Length[bytes]], start, lead, expected},
851
+ If[safeLength <= 0, Return[0]];
852
+ start = safeLength;
853
+ While[start > 1 && bytes[[start]] >= 128 && bytes[[start]] <= 191, start--];
854
+ lead = bytes[[start]];
855
+ expected = Utf8LeadByteLength[lead];
856
+ If[safeLength - start + 1 >= expected, safeLength, Max[0, start - 1]]
857
+ ];
858
+
859
+ TruncateStringToUtf8Bytes[text_String, maxBytes_Integer] := Module[{originalBytes, originalByteLength, safeLength, returnedText},
860
+ originalBytes = ToCharacterCode[text, "UTF8"];
861
+ originalByteLength = Length[originalBytes];
862
+ If[maxBytes <= 0,
863
+ Return[<|"value" -> "", "truncated" -> (originalByteLength > 0), "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0|>]
864
+ ];
865
+ If[originalByteLength <= maxBytes,
866
+ Return[<|"value" -> text, "truncated" -> False, "originalByteLength" -> originalByteLength, "returnedByteLength" -> originalByteLength|>]
867
+ ];
868
+ safeLength = Utf8PrefixByteLength[originalBytes, maxBytes];
869
+ returnedText = If[safeLength > 0, ByteArrayToString[ByteArray[Take[originalBytes, safeLength]], "UTF8"], ""];
870
+ <|"value" -> returnedText, "truncated" -> True, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength|>
871
+ ];
872
+
873
+ TruncatePayloadFields[content_String, outputs_List, messages_List, maxBytes_Integer, includeContentQ_] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, truncatedContent = "", truncatedOutputs = {}, truncatedMessages = {}, processString, output, message},
874
+ processString[text_String] := Module[{item = TruncateStringToUtf8Bytes[text, remainingByteLength]},
875
+ totalOriginalByteLength += item["originalByteLength"];
876
+ totalReturnedByteLength += item["returnedByteLength"];
877
+ anyTruncated = anyTruncated || TrueQ[item["truncated"]];
878
+ remainingByteLength = Max[0, remainingByteLength - item["returnedByteLength"]];
879
+ item["value"]
880
+ ];
881
+ If[TrueQ[includeContentQ], truncatedContent = processString[content]];
882
+ Do[
883
+ AppendTo[truncatedOutputs, processString[If[StringQ[output], output, ToString[output, InputForm]]]],
884
+ {output, outputs}
885
+ ];
886
+ Do[
887
+ AppendTo[truncatedMessages, processString[If[StringQ[message], message, ToString[message, InputForm]]]],
888
+ {message, messages}
889
+ ];
890
+ Join[
891
+ If[TrueQ[includeContentQ], <|"content" -> truncatedContent|>, <||>],
892
+ <|
893
+ "outputs" -> truncatedOutputs,
894
+ "messages" -> truncatedMessages,
895
+ "truncated" -> anyTruncated,
896
+ "originalByteLength" -> totalOriginalByteLength,
897
+ "returnedByteLength" -> totalReturnedByteLength
898
+ |>
899
+ ]
900
+ ];
901
+
902
+ ArtifactId[cellId_String, kind_String, index_Integer] := cellId <> ":" <> kind <> ":" <> ToString[index];
903
+
904
+ ArtifactDescriptor[cellId_String, kind_String, index_Integer, text_String, maxBytes_Integer] := Module[{byteLength, preview},
905
+ byteLength = Length[ToCharacterCode[text, "UTF8"]];
906
+ preview = TruncateStringToUtf8Bytes[text, maxBytes];
907
+ <|
908
+ "artifactId" -> ArtifactId[cellId, kind, index],
909
+ "type" -> kind,
910
+ "index" -> index,
911
+ "byteLength" -> byteLength,
912
+ "preview" -> preview["value"],
913
+ "previewByteLength" -> preview["returnedByteLength"],
914
+ "truncated" -> True
915
+ |>
916
+ ];
917
+
918
+ ArtifactPayloadFields[cellId_String, outputs_List, messages_List, maxBytes_Integer] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, processedOutputs = {}, processedMessages = {}, processString, output, message},
919
+ processString[text_, kind_String, index_Integer] := Module[{value = If[StringQ[text], text, ToString[text, InputForm]], byteLength, descriptor},
920
+ byteLength = Length[ToCharacterCode[value, "UTF8"]];
921
+ totalOriginalByteLength += byteLength;
922
+ If[byteLength <= remainingByteLength,
923
+ totalReturnedByteLength += byteLength;
924
+ remainingByteLength = Max[0, remainingByteLength - byteLength];
925
+ value,
926
+ anyTruncated = True;
927
+ descriptor = ArtifactDescriptor[cellId, kind, index, value, remainingByteLength];
928
+ totalReturnedByteLength += descriptor["previewByteLength"];
929
+ remainingByteLength = Max[0, remainingByteLength - descriptor["previewByteLength"]];
930
+ descriptor
931
+ ]
932
+ ];
933
+ Do[
934
+ AppendTo[processedOutputs, processString[outputs[[i]], "output", i - 1]],
935
+ {i, Length[outputs]}
936
+ ];
937
+ Do[
938
+ AppendTo[processedMessages, processString[messages[[i]], "message", i - 1]],
939
+ {i, Length[messages]}
940
+ ];
941
+ <|
942
+ "outputs" -> processedOutputs,
943
+ "messages" -> processedMessages,
944
+ "truncated" -> anyTruncated,
945
+ "originalByteLength" -> totalOriginalByteLength,
946
+ "returnedByteLength" -> totalReturnedByteLength
947
+ |>
948
+ ];
949
+
950
+ Utf8SliceStringToBytes[text_String, offset_Integer, limit_Integer] := Module[{bytes, originalByteLength, start, available, safeLength, sliceBytes, value},
951
+ bytes = ToCharacterCode[text, "UTF8"];
952
+ originalByteLength = Length[bytes];
953
+ If[limit <= 0 || offset >= originalByteLength,
954
+ Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
955
+ ];
956
+ start = Max[1, offset + 1];
957
+ While[start <= originalByteLength && bytes[[start]] >= 128 && bytes[[start]] <= 191, start++];
958
+ available = originalByteLength - start + 1;
959
+ If[available <= 0,
960
+ Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
961
+ ];
962
+ safeLength = Utf8PrefixByteLength[Take[bytes, {start, originalByteLength}], Min[limit, available]];
963
+ sliceBytes = If[safeLength > 0, Take[bytes, {start, start + safeLength - 1}], {}];
964
+ value = If[safeLength > 0, ByteArrayToString[ByteArray[sliceBytes], "UTF8"], ""];
965
+ <|"value" -> value, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength, "nextOffset" -> (start - 1 + safeLength)|>
966
+ ];
967
+
968
+ CellEvaluationTaggingPath[cellId_String] := {TaggingRules, "MMAAgentBridge", "evaluations", cellId, "complete"};
937
969
 
938
970
  MarkCellEvaluationComplete[cellId_String] := Quiet @ Check[
939
971
  CurrentValue[EvaluationNotebook[], CellEvaluationTaggingPath[cellId]] = True,
@@ -1011,23 +1043,23 @@ RestoreRunningCellEpilog[] := Module[{},
1011
1043
  ClearRunningEvaluationState[] := Module[{cellId = $RunningCellId, notebook = $RunningNotebookObject},
1012
1044
  RestoreRunningCellEpilog[];
1013
1045
  If[Head[notebook] === NotebookObject && StringQ[cellId], ClearCellEvaluationComplete[notebook, cellId]];
1014
- $RunningCellId = None;
1015
- $RunningRequestId = None;
1016
- $RunningNotebookId = None;
1017
- $RunningNotebookObject = None;
1018
- $RunningStartedAt = None;
1019
- $RunningStatus = None;
1020
- $AbortRequestedAt = None;
1021
- $RunningTimeoutAt = None
1022
- ];
1023
-
1024
- FinishRunningCell[status_String] := Module[{cellId = $RunningCellId, notebookId = $RunningNotebookId},
1025
- ClearRunningEvaluationState[];
1026
- $LastRunStatusCellId = cellId;
1027
- $LastRunStatusNotebookId = notebookId;
1028
- $RunningStatus = status;
1029
- $LastRunStatus = status
1030
- ];
1046
+ $RunningCellId = None;
1047
+ $RunningRequestId = None;
1048
+ $RunningNotebookId = None;
1049
+ $RunningNotebookObject = None;
1050
+ $RunningStartedAt = None;
1051
+ $RunningStatus = None;
1052
+ $AbortRequestedAt = None;
1053
+ $RunningTimeoutAt = None
1054
+ ];
1055
+
1056
+ FinishRunningCell[status_String] := Module[{cellId = $RunningCellId, notebookId = $RunningNotebookId},
1057
+ ClearRunningEvaluationState[];
1058
+ $LastRunStatusCellId = cellId;
1059
+ $LastRunStatusNotebookId = notebookId;
1060
+ $RunningStatus = status;
1061
+ $LastRunStatus = status
1062
+ ];
1031
1063
 
1032
1064
  CellGeneratedArtifactQ[cell_CellObject] := TrueQ[Quiet @ Check[CurrentValue[cell, GeneratedCell], False]] && CellArtifactStyleQ[CellStyleName[cell]];
1033
1065
 
@@ -1087,14 +1119,14 @@ CellArtifactScan[cell_CellObject, cellId_String:"", notebook_:Automatic] := Modu
1087
1119
  "timeout",
1088
1120
  StringQ[cellId] && StringQ[$LastRunStatusCellId] && cellId === $LastRunStatusCellId && StringQ[$LastRunStatusNotebookId] && NotebookIdForObject[nb] === $LastRunStatusNotebookId && $LastRunStatus === "finished",
1089
1121
  "finished",
1090
- StringQ[cellId] && StringQ[$LastRunStatusCellId] && cellId === $LastRunStatusCellId && StringQ[$LastRunStatusNotebookId] && NotebookIdForObject[nb] === $LastRunStatusNotebookId && $LastRunStatus === "aborted",
1091
- "aborted",
1092
- sameRunningCellQ && NumberQ[$AbortRequestedAt] && !evaluationCompleteQ,
1093
- "abort_requested",
1094
- sameRunningCellQ && NumberQ[$AbortRequestedAt] && evaluationCompleteQ,
1095
- (FinishRunningCell["aborted"]; "aborted"),
1096
- sameRunningCellQ && NumberQ[$RunningStartedAt] && (AbsoluteTime[] - $RunningStartedAt < $RunningStatusGraceSeconds),
1097
- "running",
1122
+ StringQ[cellId] && StringQ[$LastRunStatusCellId] && cellId === $LastRunStatusCellId && StringQ[$LastRunStatusNotebookId] && NotebookIdForObject[nb] === $LastRunStatusNotebookId && $LastRunStatus === "aborted",
1123
+ "aborted",
1124
+ sameRunningCellQ && NumberQ[$AbortRequestedAt] && !evaluationCompleteQ,
1125
+ "abort_requested",
1126
+ sameRunningCellQ && NumberQ[$AbortRequestedAt] && evaluationCompleteQ,
1127
+ (FinishRunningCell["aborted"]; "aborted"),
1128
+ sameRunningCellQ && NumberQ[$RunningStartedAt] && (AbsoluteTime[] - $RunningStartedAt < $RunningStatusGraceSeconds),
1129
+ "running",
1098
1130
  sameRunningCellQ && (evaluationCompleteQ || hasFinalOutputQ),
1099
1131
  (FinishRunningCell["finished"]; "finished"),
1100
1132
  sameRunningCellQ,
@@ -1137,86 +1169,86 @@ RefreshCellMap[notebookId_String] := Module[{record, nb, cells, idByCell, previo
1137
1169
  payload
1138
1170
  ];
1139
1171
 
1140
- ReadCellById[args_Association] := Module[{notebookId, record, cellId, cell, maxBytes, payload},
1141
- If[RequireReadPermission[] === $Canceled, Return[$Canceled]];
1142
- notebookId = TargetNotebookId[args];
1143
- If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1144
- record = NotebookRecord[notebookId];
1145
- If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1146
- cellId = Lookup[args, "cellId", ""];
1147
- If[StringLength[cellId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "cellId is required."|>]]];
1148
- cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1149
- If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1150
- maxBytes = CellPayloadMaxBytes[args];
1151
- With[{artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]]},
1152
- payload = TruncatePayloadFields[CellContentString[cell], artifacts["outputs"], artifacts["messages"], maxBytes, True];
1153
- Join[
1154
- <|"cellId" -> cellId, "style" -> CellStyleName[cell]|>,
1155
- payload,
1156
- <|"status" -> artifacts["status"]|>
1157
- ]
1158
- ]
1159
- ];
1160
-
1161
- GetCellOutputById[args_Association] := Module[{notebookId, record, cellId, cell, artifacts, maxBytes, payload},
1162
- If[RequireReadPermission[] === $Canceled, Return[$Canceled]];
1163
- notebookId = TargetNotebookId[args];
1164
- If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1165
- record = NotebookRecord[notebookId];
1166
- If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1172
+ ReadCellById[args_Association] := Module[{notebookId, record, cellId, cell, maxBytes, payload},
1173
+ notebookId = TargetNotebookId[args];
1174
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1175
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1176
+ record = NotebookRecord[notebookId];
1177
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1178
+ cellId = Lookup[args, "cellId", ""];
1179
+ If[StringLength[cellId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "cellId is required."|>]]];
1180
+ cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1181
+ If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1182
+ maxBytes = CellPayloadMaxBytes[args];
1183
+ With[{artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]]},
1184
+ payload = TruncatePayloadFields[CellContentString[cell], artifacts["outputs"], artifacts["messages"], maxBytes, True];
1185
+ Join[
1186
+ <|"cellId" -> cellId, "style" -> CellStyleName[cell]|>,
1187
+ payload,
1188
+ <|"status" -> artifacts["status"]|>
1189
+ ]
1190
+ ]
1191
+ ];
1192
+
1193
+ GetCellOutputById[args_Association] := Module[{notebookId, record, cellId, cell, artifacts, maxBytes, payload},
1194
+ notebookId = TargetNotebookId[args];
1195
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1196
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1197
+ record = NotebookRecord[notebookId];
1198
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1167
1199
  cellId = Lookup[args, "cellId", ""];
1168
1200
  If[StringLength[cellId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "cellId is required."|>]]];
1169
- cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1170
- If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1171
- maxBytes = CellPayloadMaxBytes[args];
1172
- artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1173
- payload = ArtifactPayloadFields[cellId, artifacts["outputs"], artifacts["messages"], maxBytes];
1174
- Join[
1175
- <|"cellId" -> cellId|>,
1176
- payload,
1177
- <|"status" -> artifacts["status"]|>
1178
- ]
1179
- ];
1180
-
1181
- ReadArtifactById[args_Association] := Module[{notebookId, record, artifactId, parts, cellId, kind, indexText, index, cell, artifacts, artifactList, text, offset, limit, page, nextOffset, done},
1182
- If[RequireReadPermission[] === $Canceled, Return[$Canceled]];
1183
- notebookId = TargetNotebookId[args];
1184
- If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1185
- record = NotebookRecord[notebookId];
1186
- If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1187
- artifactId = Lookup[args, "artifactId", ""];
1188
- If[!StringQ[artifactId] || StringLength[artifactId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId is required."|>]]];
1189
- parts = StringSplit[artifactId, ":"];
1190
- If[Length[parts] =!= 3, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId must have the form cellId:type:index."|>]]];
1191
- {cellId, kind, indexText} = parts;
1192
- If[!MemberQ[{"output", "message"}, kind], Return[Failure["BAD_REQUEST", <|"message" -> "artifact type must be output or message."|>]]];
1193
- If[!StringMatchQ[indexText, DigitCharacter..], Return[Failure["BAD_REQUEST", <|"message" -> "artifact index must be a non-negative integer."|>]]];
1194
- index = ToExpression[indexText];
1195
- offset = Lookup[args, "offset", 0];
1196
- limit = Lookup[args, "limit", 65536];
1197
- If[!IntegerQ[offset] || offset < 0, Return[Failure["BAD_REQUEST", <|"message" -> "offset must be a non-negative integer."|>]]];
1198
- If[!IntegerQ[limit] || limit <= 0 || limit > $MaxCellPayloadBytes, Return[Failure["BAD_REQUEST", <|"message" -> "limit must be a positive integer up to 1 MiB."|>]]];
1199
- cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1200
- If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1201
- artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1202
- artifactList = If[kind === "output", artifacts["outputs"], artifacts["messages"]];
1203
- If[index >= Length[artifactList], Return[Failure["BAD_REQUEST", <|"message" -> "Requested artifact was not found."|>]]];
1204
- text = artifactList[[index + 1]];
1205
- page = Utf8SliceStringToBytes[text, offset, limit];
1206
- nextOffset = page["nextOffset"];
1207
- done = nextOffset >= page["originalByteLength"];
1208
- <|
1209
- "artifactId" -> artifactId,
1210
- "offset" -> offset,
1211
- "limit" -> limit,
1212
- "data" -> page["value"],
1213
- "nextOffset" -> nextOffset,
1214
- "done" -> done,
1215
- "byteLength" -> page["originalByteLength"]
1216
- |>
1217
- ];
1218
-
1219
- MakeCellExpression[content_String, style_String] := Module[{cellStyle = If[StringQ[style] && StringLength[style] > 0, style, "Input"]},
1201
+ cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1202
+ If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1203
+ maxBytes = CellPayloadMaxBytes[args];
1204
+ artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1205
+ payload = ArtifactPayloadFields[cellId, artifacts["outputs"], artifacts["messages"], maxBytes];
1206
+ Join[
1207
+ <|"cellId" -> cellId|>,
1208
+ payload,
1209
+ <|"status" -> artifacts["status"]|>
1210
+ ]
1211
+ ];
1212
+
1213
+ ReadArtifactById[args_Association] := Module[{notebookId, record, artifactId, parts, cellId, kind, indexText, index, cell, artifacts, artifactList, text, offset, limit, page, nextOffset, done},
1214
+ notebookId = TargetNotebookId[args];
1215
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1216
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1217
+ record = NotebookRecord[notebookId];
1218
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1219
+ artifactId = Lookup[args, "artifactId", ""];
1220
+ If[!StringQ[artifactId] || StringLength[artifactId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId is required."|>]]];
1221
+ parts = StringSplit[artifactId, ":"];
1222
+ If[Length[parts] =!= 3, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId must have the form cellId:type:index."|>]]];
1223
+ {cellId, kind, indexText} = parts;
1224
+ If[!MemberQ[{"output", "message"}, kind], Return[Failure["BAD_REQUEST", <|"message" -> "artifact type must be output or message."|>]]];
1225
+ If[!StringMatchQ[indexText, DigitCharacter..], Return[Failure["BAD_REQUEST", <|"message" -> "artifact index must be a non-negative integer."|>]]];
1226
+ index = ToExpression[indexText];
1227
+ offset = Lookup[args, "offset", 0];
1228
+ limit = Lookup[args, "limit", 65536];
1229
+ If[!IntegerQ[offset] || offset < 0, Return[Failure["BAD_REQUEST", <|"message" -> "offset must be a non-negative integer."|>]]];
1230
+ If[!IntegerQ[limit] || limit <= 0 || limit > $MaxCellPayloadBytes, Return[Failure["BAD_REQUEST", <|"message" -> "limit must be a positive integer up to 1 MiB."|>]]];
1231
+ cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1232
+ If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1233
+ artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1234
+ artifactList = If[kind === "output", artifacts["outputs"], artifacts["messages"]];
1235
+ If[index >= Length[artifactList], Return[Failure["BAD_REQUEST", <|"message" -> "Requested artifact was not found."|>]]];
1236
+ text = artifactList[[index + 1]];
1237
+ page = Utf8SliceStringToBytes[text, offset, limit];
1238
+ nextOffset = page["nextOffset"];
1239
+ done = nextOffset >= page["originalByteLength"];
1240
+ <|
1241
+ "artifactId" -> artifactId,
1242
+ "offset" -> offset,
1243
+ "limit" -> limit,
1244
+ "data" -> page["value"],
1245
+ "nextOffset" -> nextOffset,
1246
+ "done" -> done,
1247
+ "byteLength" -> page["originalByteLength"]
1248
+ |>
1249
+ ];
1250
+
1251
+ MakeCellExpression[content_String, style_String] := Module[{cellStyle = If[StringQ[style] && StringLength[style] > 0, style, "Input"]},
1220
1252
  If[cellStyle === "Input",
1221
1253
  Cell[BoxData[content], "Input", CellTags -> {"AI-Generated"}],
1222
1254
  Cell[content, cellStyle, CellTags -> {"AI-Generated"}]
@@ -1255,8 +1287,8 @@ InsertCellAtBeginning[notebook_NotebookObject, newCell_] := Module[{beforeCount,
1255
1287
  ];
1256
1288
 
1257
1289
  InsertCellRequest[args_Association] := Module[{notebookId, record, afterId, style, content, newCell, notebook, anchor, cells, inserted, refreshed},
1258
- If[Not @ ConfirmAction["InsertCell", "AI requests inserting 1 cell. Allow?"], Return[$Canceled]];
1259
1290
  notebookId = TargetNotebookId[args];
1291
+ If[Not @ ConfirmAction["InsertCell", "AI requests inserting 1 cell. Allow?", notebookId], Return[$Canceled]];
1260
1292
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1261
1293
  record = NotebookRecord[notebookId];
1262
1294
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1291,8 +1323,8 @@ InsertCellRequest[args_Association] := Module[{notebookId, record, afterId, styl
1291
1323
  ];
1292
1324
 
1293
1325
  ModifyCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cellMap, cell, content, style, newCell, cells, cellIndex, updatedCells, updatedCell, writeResult},
1294
- If[Not @ ConfirmAction["ModifyCell", "AI requests modifying 1 cell. Allow?"], Return[$Canceled]];
1295
1326
  notebookId = TargetNotebookId[args];
1327
+ If[Not @ ConfirmAction["ModifyCell", "AI requests modifying 1 cell. Allow?", notebookId], Return[$Canceled]];
1296
1328
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1297
1329
  record = NotebookRecord[notebookId];
1298
1330
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1322,8 +1354,8 @@ ModifyCellRequest[args_Association] := Module[{notebookId, record, notebook, cel
1322
1354
  ];
1323
1355
 
1324
1356
  DeleteCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cell, cellMap, artifacts, artifactIds, newCellMap},
1325
- If[Not @ ConfirmAction["DeleteCell", "AI requests deleting 1 cell. Allow?"], Return[$Canceled]];
1326
1357
  notebookId = TargetNotebookId[args];
1358
+ If[Not @ ConfirmAction["DeleteCell", "AI requests deleting 1 cell. Allow?", notebookId], Return[$Canceled]];
1327
1359
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1328
1360
  record = NotebookRecord[notebookId];
1329
1361
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1345,8 +1377,8 @@ DeleteCellRequest[args_Association] := Module[{notebookId, record, notebook, cel
1345
1377
  ];
1346
1378
 
1347
1379
  RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cell, timeoutSec = Lookup[args, "timeoutSec", 120], installedEpilog, evaluateResult},
1348
- If[Not @ ConfirmAction["RunCell", "AI requests running 1 cell. Allow?"], Return[$Canceled]];
1349
1380
  notebookId = TargetNotebookId[args];
1381
+ If[Not @ ConfirmAction["RunCell", "AI requests running 1 cell. Allow?", notebookId], Return[$Canceled]];
1350
1382
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1351
1383
  record = NotebookRecord[notebookId];
1352
1384
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1358,14 +1390,14 @@ RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId
1358
1390
  If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1359
1391
  ClearCellEvaluationComplete[notebook, cellId];
1360
1392
  installedEpilog = InstallRunningCellEpilog[cell, cellId];
1361
- If[MatchQ[installedEpilog, _Failure], Return[installedEpilog]];
1362
- $LastRunStatusCellId = None;
1363
- $LastRunStatusNotebookId = None;
1364
- $LastRunStatus = None;
1365
- $RunningStatus = "running";
1366
- $AbortRequestedAt = None;
1367
- $LastLateResult = None;
1368
- $RunningRequestId = Lookup[args, "requestId", $CurrentRequestId];
1393
+ If[MatchQ[installedEpilog, _Failure], Return[installedEpilog]];
1394
+ $LastRunStatusCellId = None;
1395
+ $LastRunStatusNotebookId = None;
1396
+ $LastRunStatus = None;
1397
+ $RunningStatus = "running";
1398
+ $AbortRequestedAt = None;
1399
+ $LastLateResult = None;
1400
+ $RunningRequestId = Lookup[args, "requestId", $CurrentRequestId];
1369
1401
  $RunningCellId = cellId;
1370
1402
  $RunningNotebookId = notebookId;
1371
1403
  $RunningNotebookObject = notebook;
@@ -1384,8 +1416,8 @@ RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId
1384
1416
  ];
1385
1417
 
1386
1418
  AbortEvaluationRequest[args_Association] := Module[{notebookId, record, notebook, runningCellId, runningRequestId, wasRunning},
1387
- If[Not @ ConfirmAction["RunCell", "AI requests aborting the running evaluation. Allow?"], Return[$Canceled]];
1388
1419
  notebookId = TargetNotebookId[args];
1420
+ If[Not @ ConfirmAction["RunCell", "AI requests aborting the running evaluation. Allow?", notebookId], Return[$Canceled]];
1389
1421
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1390
1422
  record = NotebookRecord[notebookId];
1391
1423
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1398,15 +1430,15 @@ AbortEvaluationRequest[args_Association] := Module[{notebookId, record, notebook
1398
1430
  FinishRunningCell["finished"];
1399
1431
  Return[<|"status" -> "finished", "cellId" -> runningCellId, "requestId" -> runningRequestId|>]
1400
1432
  ];
1401
- If[wasRunning,
1402
- $AbortRequestedAt = AbsoluteTime[];
1403
- $RunningStatus = "abort_requested"
1404
- ];
1405
- Quiet @ Check[FrontEndTokenExecute[notebook, "EvaluatorAbort"], Null];
1406
- If[wasRunning,
1407
- <|"status" -> "abort_requested", "cellId" -> runningCellId, "requestId" -> runningRequestId|>,
1408
- <|"status" -> "idle"|>
1409
- ]
1433
+ If[wasRunning,
1434
+ $AbortRequestedAt = AbsoluteTime[];
1435
+ $RunningStatus = "abort_requested"
1436
+ ];
1437
+ Quiet @ Check[FrontEndTokenExecute[notebook, "EvaluatorAbort"], Null];
1438
+ If[wasRunning,
1439
+ <|"status" -> "abort_requested", "cellId" -> runningCellId, "requestId" -> runningRequestId|>,
1440
+ <|"status" -> "idle"|>
1441
+ ]
1410
1442
  ];
1411
1443
 
1412
1444
  KillKernelRequest[args_Association] := Module[{notebookId, record, notebook, evaluatorName},
@@ -1435,13 +1467,48 @@ RestartKernelRequest[args_Association] := Module[{notebookId, record, notebook,
1435
1467
  If[evaluatorName === $ControlAgentEvaluatorName,
1436
1468
  Return[Failure["PROTECTED_EVALUATOR", <|"message" -> "Cannot restart the MICA control agent evaluator."|>]]
1437
1469
  ];
1470
+ Quiet @ Check[NotebookEvaluate[notebook, "Quit[]", InsertResults -> False], Null];
1471
+ Pause[0.5];
1438
1472
  Quiet @ Check[NotebookEvaluate[notebook, "Null", InsertResults -> False], Null];
1439
1473
  <|"status" -> "restarted", "notebookId" -> notebookId|>
1440
1474
  ];
1441
1475
 
1442
- SaveNotebookRequest[args_Association] := Module[{notebookId, record, notebook},
1443
- If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?"], Return[$Canceled]];
1476
+ CreateNotebookRequest[args_Association] := Module[{title, nb, notebookId, payload, response},
1477
+ title = Lookup[args, "title", "Untitled"];
1478
+ If[!StringQ[title] || StringLength[StringTrim[title]] == 0, title = "Untitled"];
1479
+ nb = Quiet @ Check[CreateDocument[{}, WindowTitle -> title, Visible -> True], $Failed];
1480
+ If[Head[nb] =!= NotebookObject, Return[Failure["CREATE_FAILED", <|"message" -> "The FrontEnd failed to create the notebook."|>]]];
1481
+ payload = NotebookHeartbeatPayload[nb];
1482
+ response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1483
+ notebookId = If[AssociationQ[response],
1484
+ Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]],
1485
+ None
1486
+ ];
1487
+ <|"status" -> "created", "notebookId" -> notebookId, "title" -> title|>
1488
+ ];
1489
+
1490
+ OpenNotebookRequest[args_Association] := Module[{path, nb, notebookId, payload, response},
1491
+ path = Lookup[args, "path", ""];
1492
+ If[!StringQ[path] || StringLength[StringTrim[path]] == 0,
1493
+ Return[Failure["BAD_REQUEST", <|"message" -> "path is required."|>]]
1494
+ ];
1495
+ If[!FileExistsQ[path],
1496
+ Return[Failure["NOT_FOUND", <|"message" -> "File not found: " <> path|>]]
1497
+ ];
1498
+ nb = Quiet @ Check[NotebookOpen[path, Visible -> True], $Failed];
1499
+ If[Head[nb] =!= NotebookObject, Return[Failure["OPEN_FAILED", <|"message" -> "The FrontEnd failed to open the notebook."|>]]];
1500
+ payload = NotebookHeartbeatPayload[nb];
1501
+ response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1502
+ notebookId = If[AssociationQ[response],
1503
+ Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]],
1504
+ None
1505
+ ];
1506
+ <|"status" -> "opened", "notebookId" -> notebookId, "path" -> path|>
1507
+ ];
1508
+
1509
+ SaveNotebookRequest[args_Association] := Module[{notebookId, record, notebook},
1444
1510
  notebookId = TargetNotebookId[args];
1511
+ If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?", notebookId], Return[$Canceled]];
1445
1512
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1446
1513
  record = NotebookRecord[notebookId];
1447
1514
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1496,11 +1563,15 @@ AgentHeartbeat[] := BridgePost[
1496
1563
  |>
1497
1564
  ];
1498
1565
 
1499
- NotebookHeartbeatPayload[nb_NotebookObject] := Module[{savedPath, windowTitle, displayName, frontendObjectKey},
1566
+ NotebookHeartbeatPayload[nb_NotebookObject, notebookId_:None] := Module[{savedPath, windowTitle, displayName, frontendObjectKey, perms},
1500
1567
  savedPath = Quiet @ Check[ToString[Replace[NotebookFileName[nb], $Failed -> ""]], ""];
1501
1568
  frontendObjectKey = FrontendObjectKey[nb];
1502
1569
  windowTitle = NotebookWindowTitle[nb];
1503
1570
  displayName = NotebookDisplayNameForHeartbeat[nb, savedPath, frontendObjectKey];
1571
+ perms = If[StringQ[notebookId] && StringLength[notebookId] > 0,
1572
+ NotebookPermissions[notebookId],
1573
+ $DefaultBridgePermissions
1574
+ ];
1504
1575
  <|
1505
1576
  "agentSessionId" -> $AgentSessionId,
1506
1577
  "frontendObjectKey" -> frontendObjectKey,
@@ -1510,7 +1581,7 @@ NotebookHeartbeatPayload[nb_NotebookObject] := Module[{savedPath, windowTitle, d
1510
1581
  "savedPath" -> savedPath,
1511
1582
  "wolframVersion" -> ToString[$VersionNumber],
1512
1583
  "platform" -> $OperatingSystem,
1513
- "permissions" -> $BridgePermissions,
1584
+ "permissions" -> perms,
1514
1585
  "seenAt" -> UnixTimeMilliseconds[]
1515
1586
  |>
1516
1587
  ];
@@ -1544,7 +1615,8 @@ AgentHeartbeatNotebookClosure[notebookId_String] := Module[{record = Lookup[$Bri
1544
1615
  response = Quiet @ Check[BridgePost["/notebooks/" <> URLComponentEncodeString[notebookId] <> "/closed", <|"agentSessionId" -> $AgentSessionId|>], $Failed];
1545
1616
  If[AssociationQ[record] && AssociationQ[response] && TrueQ[Lookup[response, "ok", False]],
1546
1617
  If[!TrueQ[Lookup[record, "closed", False]],
1547
- $BridgeNotebooks[notebookId] = Join[record, <|"closed" -> True|>]
1618
+ $BridgeNotebooks[notebookId] = Join[record, <|"closed" -> True|>];
1619
+ KeyDropFrom[$BridgeNotebookPermissions, notebookId]
1548
1620
  ]
1549
1621
  ];
1550
1622
  response
@@ -1568,7 +1640,7 @@ HeartbeatNotebooks[] := Module[{notebooks = AgentVisibleNotebooks[], visibleNote
1568
1640
  None
1569
1641
  ];
1570
1642
  If[StringQ[localNotebookId] && StringLength[localNotebookId] > 0, AppendTo[visibleNotebookIds, localNotebookId]];
1571
- payload = NotebookHeartbeatPayload[nb];
1643
+ payload = NotebookHeartbeatPayload[nb, localNotebookId];
1572
1644
  response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1573
1645
  If[AssociationQ[response],
1574
1646
  notebookId = Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]];
@@ -1801,18 +1873,20 @@ ExecuteRequest[request_Association] := Module[{requestId, tool, args, result},
1801
1873
  !AssociationQ[args], Failure["BAD_REQUEST", <|"Message" -> "Request arguments must be an association."|>],
1802
1874
  True,
1803
1875
  Switch[tool,
1804
- "mma_list_cells", If[RequireReadPermission[] === $Canceled, $Canceled, Module[{notebookId = TargetNotebookId[args], refresh}, If[!StringQ[notebookId] || StringLength[notebookId] == 0, Failure["BAD_REQUEST", <|"Message" -> "No notebook is selected."|>], refresh = RefreshCellMap[notebookId]; If[MatchQ[refresh, _Failure], refresh, <|"cells" -> refresh|>]]]],
1876
+ "mma_list_cells", If[RequireReadPermission[TargetNotebookId[args]] === $Canceled, $Canceled, Module[{notebookId = TargetNotebookId[args], refresh}, If[!StringQ[notebookId] || StringLength[notebookId] == 0, Failure["BAD_REQUEST", <|"Message" -> "No notebook is selected."|>], refresh = RefreshCellMap[notebookId]; If[MatchQ[refresh, _Failure], refresh, <|"cells" -> refresh|>]]]],
1805
1877
  "mma_read_cell", ReadCellById[args],
1806
1878
  "mma_insert_cell", InsertCellRequest[args],
1807
1879
  "mma_modify_cell", ModifyCellRequest[args],
1808
1880
  "mma_delete_cell", DeleteCellRequest[args],
1809
- "mma_run_cell", RunCellRequest[args],
1881
+ "mma_run_cell", RunCellRequest[args],
1810
1882
  "mma_abort_evaluation", AbortEvaluationRequest[args],
1811
1883
  "mma_kill_kernel", KillKernelRequest[args],
1812
- "mma_restart_kernel", RestartKernelRequest[args],
1813
- "mma_get_cell_output", GetCellOutputById[args],
1814
- "mma_read_artifact", ReadArtifactById[args],
1815
- "mma_save_notebook", SaveNotebookRequest[args],
1884
+ "mma_restart_kernel", RestartKernelRequest[args],
1885
+ "mma_create_notebook", CreateNotebookRequest[args],
1886
+ "mma_open_notebook", OpenNotebookRequest[args],
1887
+ "mma_get_cell_output", GetCellOutputById[args],
1888
+ "mma_read_artifact", ReadArtifactById[args],
1889
+ "mma_save_notebook", SaveNotebookRequest[args],
1816
1890
  "mma_select_notebook", SelectNotebookRequest[args],
1817
1891
  "mma_symbol_lookup", SymbolLookup[Lookup[args, "query", ""]],
1818
1892
  _, Failure["BAD_REQUEST", <|"Message" -> StringTemplate["Unknown tool ``."][tool]|>]