@eclipse-docks/extension-ai-system 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +2 -2
  2. package/dist/api.js +1 -1
  3. package/dist/{cognitive-extension-CG_d39K5.js → cognitive-extension-7_ZMUCGD.js} +358 -92
  4. package/dist/cognitive-extension-7_ZMUCGD.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/runtime/abort.d.ts +5 -0
  7. package/dist/runtime/abort.d.ts.map +1 -0
  8. package/dist/runtime/attachment-paths.d.ts +17 -0
  9. package/dist/runtime/attachment-paths.d.ts.map +1 -0
  10. package/dist/runtime/attachments.d.ts +5 -5
  11. package/dist/runtime/attachments.d.ts.map +1 -1
  12. package/dist/runtime/cognitive-runtime.d.ts +7 -3
  13. package/dist/runtime/cognitive-runtime.d.ts.map +1 -1
  14. package/dist/runtime/index.d.ts +2 -0
  15. package/dist/runtime/index.d.ts.map +1 -1
  16. package/dist/runtime/models/openai-compatible.d.ts.map +1 -1
  17. package/dist/runtime/session-workspace.d.ts +20 -0
  18. package/dist/runtime/session-workspace.d.ts.map +1 -0
  19. package/dist/runtime/specialists/index.d.ts.map +1 -1
  20. package/dist/runtime/specialists/loop.d.ts.map +1 -1
  21. package/dist/runtime/tools/command-adapter.d.ts.map +1 -1
  22. package/dist/runtime/tools/index.d.ts +0 -1
  23. package/dist/runtime/tools/index.d.ts.map +1 -1
  24. package/dist/runtime/tools/workspace-adapter.d.ts +1 -1
  25. package/dist/runtime/tools/workspace-adapter.d.ts.map +1 -1
  26. package/dist/runtime/tools/workspace-path-qualify.d.ts +22 -0
  27. package/dist/runtime/tools/workspace-path-qualify.d.ts.map +1 -0
  28. package/dist/runtime/tools/workspace-path.d.ts +17 -0
  29. package/dist/runtime/tools/workspace-path.d.ts.map +1 -0
  30. package/dist/{runtime-DfNGzEjU.js → runtime-By5PP2BC.js} +696 -384
  31. package/dist/runtime-By5PP2BC.js.map +1 -0
  32. package/dist/view/busy-status.d.ts +12 -0
  33. package/dist/view/busy-status.d.ts.map +1 -0
  34. package/dist/view/cognitive-settings-editor.d.ts +0 -1
  35. package/dist/view/cognitive-settings-editor.d.ts.map +1 -1
  36. package/dist/view/cognitive-view.d.ts +21 -0
  37. package/dist/view/cognitive-view.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/cognitive-extension-CG_d39K5.js.map +0 -1
  40. package/dist/runtime/tools/attachment-adapter.d.ts +0 -6
  41. package/dist/runtime/tools/attachment-adapter.d.ts.map +0 -1
  42. package/dist/runtime-DfNGzEjU.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Directory, File, commandRegistry, persistenceService, workspaceService } from "@eclipse-docks/core";
1
+ import { Directory, File, IN_MEMORY_ROOT_NAME, commandRegistry, filebrowserDialog, getInMemoryRoot, persistenceService, setInMemoryMountProjection, workspaceService } from "@eclipse-docks/core";
2
2
  import { Models } from "@opencode-ai/models";
3
3
  //#region src/runtime/events.ts
4
4
  /** Tiny in-process bus so runtime stays UI-agnostic. */
@@ -548,6 +548,17 @@ function normalizeProvidersSettings(raw) {
548
548
  };
549
549
  }
550
550
  //#endregion
551
+ //#region src/runtime/abort.ts
552
+ /** True when an AbortSignal has already been aborted. */
553
+ function isAborted(signal) {
554
+ return Boolean(signal?.aborted);
555
+ }
556
+ /** True for DOMException / Error abort failures from fetch and friends. */
557
+ function isAbortError(err) {
558
+ if (!err || typeof err !== "object") return false;
559
+ return err.name === "AbortError";
560
+ }
561
+ //#endregion
551
562
  //#region src/runtime/models/openai-compatible.ts
552
563
  function wireContent(content) {
553
564
  if (typeof content === "string") return content;
@@ -665,6 +676,10 @@ var OpenAICompatibleProvider = class {
665
676
  signal: params.signal
666
677
  });
667
678
  } catch (err) {
679
+ if (params.signal?.aborted || isAbortError(err)) {
680
+ yield { type: "done" };
681
+ return;
682
+ }
668
683
  yield {
669
684
  type: "error",
670
685
  error: err instanceof Error ? err.message : String(err)
@@ -683,7 +698,21 @@ var OpenAICompatibleProvider = class {
683
698
  let buffer = "";
684
699
  const toolAcc = /* @__PURE__ */ new Map();
685
700
  while (true) {
686
- const { done, value } = await reader.read();
701
+ let done;
702
+ let value;
703
+ try {
704
+ ({done, value} = await reader.read());
705
+ } catch (err) {
706
+ if (params.signal?.aborted || isAbortError(err)) {
707
+ yield { type: "done" };
708
+ return;
709
+ }
710
+ yield {
711
+ type: "error",
712
+ error: err instanceof Error ? err.message : String(err)
713
+ };
714
+ return;
715
+ }
687
716
  if (done) break;
688
717
  buffer += decoder.decode(value, { stream: true });
689
718
  const lines = buffer.split("\n");
@@ -955,227 +984,40 @@ function isMutatingName(name) {
955
984
  return /^(write|edit|delete|mkdir|rm|mv|save|apply)/i.test(name) || name.includes("write") || name.includes("delete");
956
985
  }
957
986
  //#endregion
958
- //#region src/runtime/tools/workspace-adapter.ts
959
- function asRecord(value) {
960
- return value && typeof value === "object" ? value : {};
961
- }
962
- /** Registers built-in workspace file tools on the broker. */
963
- function registerWorkspaceTools(broker) {
964
- broker.register({
965
- name: "workspace_list",
966
- description: "List children of a workspace directory path",
967
- parameters: {
968
- type: "object",
969
- properties: { path: {
970
- type: "string",
971
- description: "Directory path relative to workspace"
972
- } },
973
- required: ["path"]
974
- }
975
- }, async (call) => {
976
- try {
977
- const path = String(call.arguments.path ?? "");
978
- const root = await workspaceService.getWorkspace();
979
- if (!root) return {
980
- id: call.id,
981
- name: call.name,
982
- ok: false,
983
- output: null,
984
- error: "No workspace"
985
- };
986
- const resource = await root.getResource(path.endsWith("/") ? path : `${path}/`);
987
- if (!(resource instanceof Directory)) return {
988
- id: call.id,
989
- name: call.name,
990
- ok: false,
991
- output: null,
992
- error: "Not a directory"
993
- };
994
- const children = await resource.listChildren(true);
995
- return {
996
- id: call.id,
997
- name: call.name,
998
- ok: true,
999
- output: children.map((c) => ({
1000
- name: c.getName(),
1001
- path: c.getWorkspacePath()
1002
- }))
1003
- };
1004
- } catch (err) {
1005
- return {
1006
- id: call.id,
1007
- name: call.name,
1008
- ok: false,
1009
- output: null,
1010
- error: err instanceof Error ? err.message : String(err)
1011
- };
1012
- }
1013
- });
1014
- broker.register({
1015
- name: "workspace_read",
1016
- description: "Read a workspace file as text",
1017
- parameters: {
1018
- type: "object",
1019
- properties: { path: { type: "string" } },
1020
- required: ["path"]
1021
- }
1022
- }, async (call) => {
1023
- try {
1024
- const path = String(call.arguments.path ?? "");
1025
- const root = await workspaceService.getWorkspace();
1026
- if (!root) return {
1027
- id: call.id,
1028
- name: call.name,
1029
- ok: false,
1030
- output: null,
1031
- error: "No workspace"
1032
- };
1033
- const resource = await root.getResource(path);
1034
- if (!(resource instanceof File)) return {
1035
- id: call.id,
1036
- name: call.name,
1037
- ok: false,
1038
- output: null,
1039
- error: "Not a file"
1040
- };
1041
- const contents = await resource.getContents();
1042
- return {
1043
- id: call.id,
1044
- name: call.name,
1045
- ok: true,
1046
- output: contents
1047
- };
1048
- } catch (err) {
1049
- return {
1050
- id: call.id,
1051
- name: call.name,
1052
- ok: false,
1053
- output: null,
1054
- error: err instanceof Error ? err.message : String(err)
1055
- };
1056
- }
1057
- });
1058
- broker.register({
1059
- name: "workspace_write",
1060
- description: "Write text contents to a workspace file (creates parents as needed)",
1061
- parameters: {
1062
- type: "object",
1063
- properties: {
1064
- path: { type: "string" },
1065
- contents: { type: "string" }
1066
- },
1067
- required: ["path", "contents"]
1068
- }
1069
- }, async (call) => {
1070
- try {
1071
- const args = asRecord(call.arguments);
1072
- const path = String(args.path ?? "");
1073
- const contents = String(args.contents ?? "");
1074
- const root = await workspaceService.getWorkspace();
1075
- if (!root) return {
1076
- id: call.id,
1077
- name: call.name,
1078
- ok: false,
1079
- output: null,
1080
- error: "No workspace",
1081
- mutating: true
1082
- };
1083
- const resource = await root.getResource(path, { create: true });
1084
- if (!(resource instanceof File)) return {
1085
- id: call.id,
1086
- name: call.name,
1087
- ok: false,
1088
- output: null,
1089
- error: "Not a file",
1090
- mutating: true
1091
- };
1092
- await resource.saveContents(contents);
1093
- return {
1094
- id: call.id,
1095
- name: call.name,
1096
- ok: true,
1097
- output: { path },
1098
- mutating: true
1099
- };
1100
- } catch (err) {
1101
- return {
1102
- id: call.id,
1103
- name: call.name,
1104
- ok: false,
1105
- output: null,
1106
- error: err instanceof Error ? err.message : String(err),
1107
- mutating: true
1108
- };
1109
- }
1110
- });
987
+ //#region src/runtime/attachment-paths.ts
988
+ /** Mirrors core `IN_MEMORY_ROOT_NAME` without importing the filesys graph. */
989
+ var IN_MEMORY_ROOT_NAME$1 = "in-memory";
990
+ /** URI scheme for UI references (`in-memory://<sessionId>/<file>`). */
991
+ var IN_MEMORY_URI_SCHEME = "in-memory://";
992
+ /** Sanitize a single path segment (no slashes). Prefer id-prefixed names for uniqueness. */
993
+ function safeAttachmentFileName(name, id) {
994
+ return `${id}-${(name || "attachment").replace(/[/\\]/g, "_").trim() || "attachment"}`;
995
+ }
996
+ /** Workspace path used by workspace_* tools (no protocol). */
997
+ function attachmentWorkspacePath(sessionId, stored) {
998
+ return `${IN_MEMORY_ROOT_NAME$1}/${sessionId}/${safeAttachmentFileName(stored.name, stored.id)}`;
999
+ }
1000
+ /** UI / catalog URI form (`in-memory://<sessionId>/<file>`). */
1001
+ function attachmentUri(sessionId, stored) {
1002
+ return `${IN_MEMORY_URI_SCHEME}${sessionId}/${safeAttachmentFileName(stored.name, stored.id)}`;
1111
1003
  }
1112
- //#endregion
1113
- //#region src/runtime/tools/command-adapter.ts
1114
- var SKIP = /* @__PURE__ */ new Set(["open_cognitive_ai_settings", "open_ai_config"]);
1115
- /** Built-in tools that must survive command refresh (not command-backed). */
1116
- var KEEP_TOOLS = /* @__PURE__ */ new Set([
1117
- "workspace_list",
1118
- "workspace_read",
1119
- "workspace_write",
1120
- "attachment_list",
1121
- "attachment_read"
1122
- ]);
1123
- function sanitizeName(id) {
1124
- return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
1125
- }
1126
- /** Registers Docks commands as first-class LLM tools (tool name = command id). */
1127
- function refreshCommandTools(broker, role) {
1128
- for (const def of broker.listDefinitions("build")) if (!KEEP_TOOLS.has(def.name)) broker.unregister(def.name);
1129
- const commands = commandRegistry.listCommands(commandRegistry.createExecutionContext());
1130
- const entries = Object.values(commands);
1131
- for (const command of entries) {
1132
- if (!command?.id || SKIP.has(command.id)) continue;
1133
- if (!command.description) continue;
1134
- if ((role === "plan" || role === "explore") && /delete|write|save|rm|mv/i.test(command.id)) continue;
1135
- const toolName = sanitizeName(command.id);
1136
- if (KEEP_TOOLS.has(toolName)) continue;
1137
- const properties = {};
1138
- const required = [];
1139
- for (const p of command.parameters ?? []) {
1140
- properties[p.name] = {
1141
- type: p.type ?? "string",
1142
- description: p.description,
1143
- ...p.allowedValues ? { enum: p.allowedValues } : {}
1144
- };
1145
- if (p.required) required.push(p.name);
1146
- }
1147
- broker.register({
1148
- name: toolName,
1149
- description: `${command.name}: ${command.description}`,
1150
- parameters: {
1151
- type: "object",
1152
- properties,
1153
- required
1154
- }
1155
- }, async (call, _sessionId) => {
1156
- try {
1157
- const result = await commandRegistry.execute(command.id, { params: call.arguments });
1158
- return {
1159
- id: call.id,
1160
- name: call.name,
1161
- ok: true,
1162
- output: result
1163
- };
1164
- } catch (err) {
1165
- return {
1166
- id: call.id,
1167
- name: call.name,
1168
- ok: false,
1169
- output: null,
1170
- error: err instanceof Error ? err.message : String(err)
1171
- };
1172
- }
1173
- });
1004
+ /**
1005
+ * Map `in-memory://…` references to composite workspace paths.
1006
+ * Real workspace paths are returned unchanged.
1007
+ */
1008
+ function toWorkspaceToolPath(path) {
1009
+ const trimmed = path.trim();
1010
+ const lower = trimmed.toLowerCase();
1011
+ if (lower === "in-memory:" || lower === "in-memory://" || lower === "in-memory") return IN_MEMORY_ROOT_NAME$1;
1012
+ if (lower.startsWith("in-memory://")) {
1013
+ const rest = trimmed.slice(12).replace(/^\/+/, "");
1014
+ return rest ? `${IN_MEMORY_ROOT_NAME$1}/${rest}` : IN_MEMORY_ROOT_NAME$1;
1174
1015
  }
1016
+ return trimmed;
1175
1017
  }
1176
1018
  //#endregion
1177
1019
  //#region src/runtime/attachments.ts
1178
- /** Soft cap per attached text file when stored / returned by attachment_read. */
1020
+ /** Soft cap per attached text file when stored / returned via workspace_read. */
1179
1021
  var MAX_ATTACHMENT_CHARS = 1e5;
1180
1022
  /** Soft cap for visual media attachments (bytes). */
1181
1023
  var MAX_IMAGE_BYTES = 4e6;
@@ -1384,7 +1226,7 @@ function toSessionAttachment(attachment, opts = {}) {
1384
1226
  messageId: opts.messageId
1385
1227
  };
1386
1228
  }
1387
- /** Lightweight history/UI reference — no payload. */
1229
+ /** Lightweight history/UI reference — no payload (path is an in-memory workspace path/URI). */
1388
1230
  function toFileRefPart(stored) {
1389
1231
  return {
1390
1232
  type: "file",
@@ -1398,14 +1240,15 @@ function toFileRefPart(stored) {
1398
1240
  }
1399
1241
  /** One-line catalog entry published into the user turn for the model. */
1400
1242
  function formatAttachmentRefForModel(part) {
1401
- const label = part.path?.trim() || part.name;
1243
+ const display = part.path || part.name;
1244
+ const toolPath = toWorkspaceToolPath(display);
1402
1245
  const kind = part.kind ?? "file";
1403
1246
  const bytes = part.byteLength != null ? ` bytes=${part.byteLength}` : "";
1404
- return `[Attachment published: id=${part.id} name="${label}" kind=${kind}${part.mime ? ` mime=${part.mime}` : ""}${bytes}] → use attachment_read with id="${part.id}"`;
1247
+ return `[Attachment published: ${display} kind=${kind}${part.mime ? ` mime=${part.mime}` : ""}${bytes}] → workspace_read path="${toolPath}"`;
1405
1248
  }
1406
1249
  /**
1407
1250
  * Build provider user content from history parts.
1408
- * File parts are references only — payloads stay in the session store.
1251
+ * File parts are references only — payloads live under the in-memory workspace root.
1409
1252
  */
1410
1253
  function userContentFromParts(parts) {
1411
1254
  const texts = [];
@@ -1416,71 +1259,10 @@ function userContentFromParts(parts) {
1416
1259
  }
1417
1260
  const body = [...texts, ...refs].join("\n").trim();
1418
1261
  if (!refs.length) return body;
1419
- return [body, "Attachments live in this session store only. Call attachment_read (or attachment_list) before answering about them. Do not use canvas/workspace/editor commands to locate attachments."].filter(Boolean).join("\n");
1420
- }
1421
- function attachmentMeta(stored) {
1422
- return {
1423
- id: stored.id,
1424
- name: stored.name,
1425
- path: stored.path,
1426
- mime: stored.mime,
1427
- kind: stored.kind,
1428
- byteLength: stored.byteLength,
1429
- truncated: stored.truncated
1430
- };
1431
- }
1432
- function readTextAttachment(stored, opts = {}) {
1433
- if (stored.kind !== "text" && !(stored.kind === "image" && stored.text)) return {
1434
- ok: false,
1435
- error: `${stored.name} is not a text attachment`
1436
- };
1437
- const full = stored.text ?? "";
1438
- const offset = Math.max(0, opts.offset ?? 0);
1439
- const limit = Math.max(1, opts.limit ?? 4e4);
1440
- const slice = full.slice(offset, offset + limit);
1441
- return {
1442
- ok: true,
1443
- output: {
1444
- ...attachmentMeta(stored),
1445
- offset,
1446
- length: slice.length,
1447
- totalLength: full.length,
1448
- truncated: offset + slice.length < full.length,
1449
- content: slice
1450
- }
1451
- };
1262
+ return [body, "Session attachments are files under the in-memory workspace root. List with workspace_list, read with workspace_read. Do not connect storage backends to find them."].filter(Boolean).join("\n");
1452
1263
  }
1453
1264
  /** Marker field: specialist loop presents image bytes in a follow-up multimodal message. */
1454
1265
  var IMAGE_PRESENT_KEY = "_presentImage";
1455
- function readImageAttachment(stored, opts = {}) {
1456
- if (stored.kind !== "image") return {
1457
- ok: false,
1458
- error: `${stored.name} is not an image attachment`
1459
- };
1460
- if (stored.text != null && isSvgAttachment(stored)) return readTextAttachment(stored, opts);
1461
- if (!stored.dataUrl) return {
1462
- ok: false,
1463
- error: `${stored.name} has no image payload`
1464
- };
1465
- const present = resolveMediaType({
1466
- name: stored.name,
1467
- mime: stored.mime
1468
- })?.vision !== false;
1469
- return {
1470
- ok: true,
1471
- output: {
1472
- ...attachmentMeta(stored),
1473
- ...present ? {
1474
- [IMAGE_PRESENT_KEY]: true,
1475
- dataUrl: stored.dataUrl,
1476
- note: "Image bytes will be provided to the model in the next message for vision inspection."
1477
- } : {
1478
- dataUrlOmitted: true,
1479
- note: "This media type is stored for preview; vision presentation is skipped. Ask for a raster export if needed."
1480
- }
1481
- }
1482
- };
1483
- }
1484
1266
  function stripAttachmentToolOutput(output) {
1485
1267
  if (!output || typeof output !== "object") return output;
1486
1268
  const copy = { ...output };
@@ -1611,105 +1393,451 @@ async function readPromptAttachmentFromFile(file) {
1611
1393
  };
1612
1394
  }
1613
1395
  //#endregion
1614
- //#region src/runtime/tools/attachment-adapter.ts
1615
- /** Registers read-only session attachment tools. */
1616
- function registerAttachmentTools(broker, lookup) {
1396
+ //#region src/runtime/tools/workspace-path-qualify.ts
1397
+ function isInMemoryToolPath(path) {
1398
+ return path === "in-memory" || path.startsWith(`in-memory/`);
1399
+ }
1400
+ function isWorkspaceRootPath(path) {
1401
+ const trimmed = path.trim();
1402
+ return !trimmed || trimmed === "." || trimmed === "/";
1403
+ }
1404
+ /** First path segment — composite roots are keyed by this name. */
1405
+ function firstPathSegment(path) {
1406
+ return path.split("/").filter(Boolean)[0] ?? "";
1407
+ }
1408
+ function pathHasKnownRoot(path, rootNames) {
1409
+ const first = firstPathSegment(path);
1410
+ if (!first) return false;
1411
+ return (rootNames instanceof Set ? rootNames : new Set(rootNames)).has(first);
1412
+ }
1413
+ function joinUnderRoot(rootPath, relativePath) {
1414
+ const root = rootPath.replace(/\/+$/, "");
1415
+ const relative = relativePath.replace(/^\/+/, "");
1416
+ if (!root) return relative;
1417
+ if (!relative) return root;
1418
+ return `${root}/${relative}`;
1419
+ }
1420
+ /**
1421
+ * Decide how to qualify a workspace tool path that is missing a connected-root prefix.
1422
+ */
1423
+ function decideRootQualification(path, durableRoots, mode) {
1424
+ if (isInMemoryToolPath(path) || isWorkspaceRootPath(path)) return { action: "keep" };
1425
+ if (pathHasKnownRoot(path, durableRoots)) return { action: "keep" };
1426
+ if (durableRoots.length === 1) return {
1427
+ action: "prefix",
1428
+ root: durableRoots[0]
1429
+ };
1430
+ if (durableRoots.length === 0) return {
1431
+ action: "error",
1432
+ error: mode === "write" ? "No durable workspace folder connected. Connect a folder before writing, or use an in-memory/… path." : "No workspace folder connected."
1433
+ };
1434
+ if (mode === "write") return { action: "pick" };
1435
+ return {
1436
+ action: "error",
1437
+ error: `Path must start with a workspace root (${durableRoots.join(", ")}). Example: ${durableRoots[0]}/${path}`
1438
+ };
1439
+ }
1440
+ //#endregion
1441
+ //#region src/runtime/tools/workspace-path.ts
1442
+ /**
1443
+ * Ensure tool paths are under a composite workspace root.
1444
+ * - Already rooted / in-memory / empty → unchanged
1445
+ * - One durable root → auto-prefix
1446
+ * - Several durable roots (writes) → folder picker dialog
1447
+ */
1448
+ async function qualifyWorkspaceToolPath(rawPath, mode = "write") {
1449
+ const path = toWorkspaceToolPath(String(rawPath ?? ""));
1450
+ if (isInMemoryToolPath(path) || isWorkspaceRootPath(path)) return {
1451
+ ok: true,
1452
+ path
1453
+ };
1454
+ const folders = await workspaceService.getFolders();
1455
+ const allNames = folders.map((f) => f.name);
1456
+ if (pathHasKnownRoot(path, allNames)) return {
1457
+ ok: true,
1458
+ path
1459
+ };
1460
+ const durableRoots = folders.filter((f) => f.type !== "in-memory").map((f) => f.name);
1461
+ const decision = decideRootQualification(path, mode === "write" ? durableRoots : allNames.length ? allNames : durableRoots, mode);
1462
+ if (decision.action === "keep") return {
1463
+ ok: true,
1464
+ path
1465
+ };
1466
+ if (decision.action === "prefix") return {
1467
+ ok: true,
1468
+ path: joinUnderRoot(decision.root, path),
1469
+ resolvedFrom: path
1470
+ };
1471
+ if (decision.action === "error") return {
1472
+ ok: false,
1473
+ error: decision.error
1474
+ };
1475
+ const chosen = await filebrowserDialog("directory");
1476
+ if (!chosen) return {
1477
+ ok: false,
1478
+ error: "Write cancelled: no workspace folder selected."
1479
+ };
1480
+ return {
1481
+ ok: true,
1482
+ path: joinUnderRoot(chosen, path),
1483
+ resolvedFrom: path
1484
+ };
1485
+ }
1486
+ //#endregion
1487
+ //#region src/runtime/tools/workspace-adapter.ts
1488
+ function asRecord(value) {
1489
+ return value && typeof value === "object" ? value : {};
1490
+ }
1491
+ /** Some File backends (e.g. in-memory) expose mime; the abstract File does not. */
1492
+ function optionalMimeType(resource) {
1493
+ const getMimeType = resource.getMimeType;
1494
+ return typeof getMimeType === "function" ? getMimeType.call(resource) : void 0;
1495
+ }
1496
+ function fileNameFromPath(path) {
1497
+ const parts = path.split("/").filter(Boolean);
1498
+ return parts[parts.length - 1] || path;
1499
+ }
1500
+ /** Resolve a tool path against the shared in-memory root or the mounted workspace. */
1501
+ async function resolveResource(path, options) {
1502
+ if (isInMemoryToolPath(path)) {
1503
+ const root = getInMemoryRoot();
1504
+ const relative = path === IN_MEMORY_ROOT_NAME ? "" : path.slice(`${IN_MEMORY_ROOT_NAME}/`.length);
1505
+ if (!relative) return {
1506
+ resource: root,
1507
+ path: IN_MEMORY_ROOT_NAME
1508
+ };
1509
+ const lookup = options?.directory && !relative.endsWith("/") ? `${relative}/` : relative;
1510
+ return {
1511
+ resource: await root.getResource(lookup, options?.create ? { create: true } : void 0),
1512
+ path
1513
+ };
1514
+ }
1515
+ const workspace = await workspaceService.getWorkspace();
1516
+ if (!workspace) return {
1517
+ resource: null,
1518
+ path
1519
+ };
1520
+ if (isWorkspaceRootPath(path)) return {
1521
+ resource: workspace,
1522
+ path: ""
1523
+ };
1524
+ const lookup = options?.directory && !path.endsWith("/") ? `${path}/` : path;
1525
+ return {
1526
+ resource: await workspace.getResource(lookup, options?.create ? { create: true } : void 0),
1527
+ path
1528
+ };
1529
+ }
1530
+ /**
1531
+ * Present image-like workspace files for vision (data URLs or known image extensions).
1532
+ * Returns null when the file should be treated as ordinary text.
1533
+ */
1534
+ function visionToolOutput(path, contents, mimeHint) {
1535
+ const name = fileNameFromPath(path);
1536
+ const dataUrl = typeof contents === "string" && contents.startsWith("data:image/") ? contents : void 0;
1537
+ const mime = mimeHint || (dataUrl ? dataUrl.slice(5, dataUrl.indexOf(";")) : void 0) || resolveMediaType({
1538
+ name,
1539
+ mime: mimeHint
1540
+ })?.mime;
1541
+ if (isSvgAttachment({
1542
+ name,
1543
+ mime,
1544
+ text: dataUrl ? void 0 : contents
1545
+ })) return null;
1546
+ const media = resolveMediaType({
1547
+ name,
1548
+ mime
1549
+ });
1550
+ if (!dataUrl && media?.mode !== "raster") return null;
1551
+ if (!dataUrl) return null;
1552
+ return {
1553
+ path,
1554
+ name,
1555
+ mime,
1556
+ kind: "image",
1557
+ ...media?.vision !== false ? {
1558
+ [IMAGE_PRESENT_KEY]: true,
1559
+ dataUrl,
1560
+ note: "Image bytes will be provided to the model in the next message for vision inspection."
1561
+ } : {
1562
+ dataUrlOmitted: true,
1563
+ note: "This media type is stored for preview; vision presentation is skipped."
1564
+ }
1565
+ };
1566
+ }
1567
+ /** Registers built-in workspace file tools (paths may use in-memory:// URI form). */
1568
+ function registerWorkspaceTools(broker) {
1617
1569
  broker.register({
1618
- name: "attachment_list",
1619
- description: "List chat-session attachments (id, name, kind, mime, size). Use this for files the user attached in chat — not workspace/canvas files. Payloads are omitted; call attachment_read to inspect one.",
1570
+ name: "workspace_list",
1571
+ description: "List children of a workspace directory. Omit path (or use \"\" / \"/\") for connected roots (use those path prefixes for writes). in-memory/… works without mounting.",
1620
1572
  parameters: {
1621
1573
  type: "object",
1622
- properties: {}
1574
+ properties: { path: {
1575
+ type: "string",
1576
+ description: "Directory path under a connected root (empty = list roots). in-memory://… URIs are accepted."
1577
+ } },
1578
+ required: []
1579
+ }
1580
+ }, async (call) => {
1581
+ try {
1582
+ const path = toWorkspaceToolPath(String(call.arguments.path ?? ""));
1583
+ const { resource } = await resolveResource(path, { directory: true });
1584
+ if (!(resource instanceof Directory)) return {
1585
+ id: call.id,
1586
+ name: call.name,
1587
+ ok: false,
1588
+ output: null,
1589
+ error: isInMemoryToolPath(path) ? "Not a directory" : "No workspace / not a directory"
1590
+ };
1591
+ const children = await resource.listChildren(true);
1592
+ return {
1593
+ id: call.id,
1594
+ name: call.name,
1595
+ ok: true,
1596
+ output: children.map((c) => ({
1597
+ name: c.getName(),
1598
+ path: c.getWorkspacePath(),
1599
+ kind: c instanceof Directory ? "directory" : "file"
1600
+ }))
1601
+ };
1602
+ } catch (err) {
1603
+ return {
1604
+ id: call.id,
1605
+ name: call.name,
1606
+ ok: false,
1607
+ output: null,
1608
+ error: err instanceof Error ? err.message : String(err)
1609
+ };
1623
1610
  }
1624
- }, async (call, sessionId) => {
1625
- const items = lookup(sessionId).map(attachmentMeta);
1626
- return {
1627
- id: call.id,
1628
- name: call.name,
1629
- ok: true,
1630
- output: {
1631
- count: items.length,
1632
- attachments: items
1633
- }
1634
- };
1635
1611
  });
1636
1612
  broker.register({
1637
- name: "attachment_read",
1638
- description: "Inspect a chat-session attachment by id from the user message or attachment_list. Prefer this whenever the user asks about an attached screenshot/file. Text returns content (optional offset/limit). Images are presented for vision inspection.",
1613
+ name: "workspace_read",
1614
+ description: "Read a workspace file as text. Paths should be root/… from workspace_list; a bare name is auto-rooted when only one folder is connected. Image data-URL files are presented for vision.",
1639
1615
  parameters: {
1640
1616
  type: "object",
1641
1617
  properties: {
1642
- id: {
1618
+ path: {
1643
1619
  type: "string",
1644
- description: "Attachment id from attachment_list or the user message"
1620
+ description: "File path under a connected root (e.g. tests/readme.md)."
1645
1621
  },
1646
1622
  offset: {
1647
1623
  type: "number",
1648
- description: "Text start offset (characters)"
1624
+ description: "Text start offset"
1649
1625
  },
1650
1626
  limit: {
1651
1627
  type: "number",
1652
1628
  description: "Max text characters to return"
1653
1629
  }
1654
1630
  },
1655
- required: ["id"]
1631
+ required: ["path"]
1656
1632
  }
1657
- }, async (call, sessionId) => {
1658
- const id = String(call.arguments.id ?? "").trim();
1659
- if (!id) return {
1660
- id: call.id,
1661
- name: call.name,
1662
- ok: false,
1663
- output: null,
1664
- error: "id is required"
1665
- };
1666
- const stored = lookup(sessionId, id)[0];
1667
- if (!stored) return {
1668
- id: call.id,
1669
- name: call.name,
1670
- ok: false,
1671
- output: null,
1672
- error: `Unknown attachment: ${id}`
1673
- };
1674
- if (stored.kind === "image") {
1675
- const result = readImageAttachment(stored, {
1676
- offset: typeof call.arguments.offset === "number" ? call.arguments.offset : void 0,
1677
- limit: typeof call.arguments.limit === "number" ? call.arguments.limit : void 0
1678
- });
1679
- if (!result.ok) return {
1633
+ }, async (call) => {
1634
+ try {
1635
+ const qualified = await qualifyWorkspaceToolPath(String(call.arguments.path ?? ""), "read");
1636
+ if (!qualified.ok) return {
1680
1637
  id: call.id,
1681
1638
  name: call.name,
1682
1639
  ok: false,
1683
1640
  output: null,
1684
- error: result.error
1641
+ error: qualified.error
1685
1642
  };
1643
+ const path = qualified.path;
1644
+ const { resource } = await resolveResource(path);
1645
+ if (!(resource instanceof File)) return {
1646
+ id: call.id,
1647
+ name: call.name,
1648
+ ok: false,
1649
+ output: null,
1650
+ error: isInMemoryToolPath(path) ? "Not a file" : "No workspace / not a file"
1651
+ };
1652
+ const contents = await resource.getContents();
1653
+ const text = typeof contents === "string" ? contents : String(contents ?? "");
1654
+ const vision = visionToolOutput(path, text, optionalMimeType(resource));
1655
+ if (vision) return {
1656
+ id: call.id,
1657
+ name: call.name,
1658
+ ok: true,
1659
+ output: vision
1660
+ };
1661
+ const offset = typeof call.arguments.offset === "number" ? Math.max(0, call.arguments.offset) : 0;
1662
+ const limit = typeof call.arguments.limit === "number" ? Math.max(1, call.arguments.limit) : void 0;
1663
+ if (offset || limit != null) {
1664
+ const slice = text.slice(offset, limit != null ? offset + limit : void 0);
1665
+ return {
1666
+ id: call.id,
1667
+ name: call.name,
1668
+ ok: true,
1669
+ output: {
1670
+ path,
1671
+ offset,
1672
+ length: slice.length,
1673
+ totalLength: text.length,
1674
+ truncated: offset + slice.length < text.length,
1675
+ content: slice
1676
+ }
1677
+ };
1678
+ }
1686
1679
  return {
1687
1680
  id: call.id,
1688
1681
  name: call.name,
1689
1682
  ok: true,
1690
- output: result.output
1683
+ output: text
1684
+ };
1685
+ } catch (err) {
1686
+ return {
1687
+ id: call.id,
1688
+ name: call.name,
1689
+ ok: false,
1690
+ output: null,
1691
+ error: err instanceof Error ? err.message : String(err)
1692
+ };
1693
+ }
1694
+ });
1695
+ broker.register({
1696
+ name: "workspace_write",
1697
+ description: "Write text to a workspace file (creates parents as needed). Prefer root/… paths from workspace_list. A bare filename uses the only durable root, or opens a folder picker when several roots are connected.",
1698
+ parameters: {
1699
+ type: "object",
1700
+ properties: {
1701
+ path: {
1702
+ type: "string",
1703
+ description: "File path under a connected root (e.g. tests/demo.canvas.js)."
1704
+ },
1705
+ contents: { type: "string" }
1706
+ },
1707
+ required: ["path", "contents"]
1708
+ }
1709
+ }, async (call) => {
1710
+ try {
1711
+ const args = asRecord(call.arguments);
1712
+ const qualified = await qualifyWorkspaceToolPath(String(args.path ?? ""), "write");
1713
+ if (!qualified.ok) return {
1714
+ id: call.id,
1715
+ name: call.name,
1716
+ ok: false,
1717
+ output: null,
1718
+ error: qualified.error,
1719
+ mutating: true
1720
+ };
1721
+ const path = qualified.path;
1722
+ const contents = String(args.contents ?? "");
1723
+ const { resource } = await resolveResource(path, { create: true });
1724
+ if (!(resource instanceof File)) return {
1725
+ id: call.id,
1726
+ name: call.name,
1727
+ ok: false,
1728
+ output: null,
1729
+ error: isInMemoryToolPath(path) ? "Not a file" : "No workspace / not a file",
1730
+ mutating: true
1731
+ };
1732
+ await resource.saveContents(contents);
1733
+ return {
1734
+ id: call.id,
1735
+ name: call.name,
1736
+ ok: true,
1737
+ output: {
1738
+ path,
1739
+ ...qualified.resolvedFrom ? { resolvedFrom: qualified.resolvedFrom } : {}
1740
+ },
1741
+ mutating: true
1742
+ };
1743
+ } catch (err) {
1744
+ return {
1745
+ id: call.id,
1746
+ name: call.name,
1747
+ ok: false,
1748
+ output: null,
1749
+ error: err instanceof Error ? err.message : String(err),
1750
+ mutating: true
1691
1751
  };
1692
1752
  }
1693
- const result = readTextAttachment(stored, {
1694
- offset: typeof call.arguments.offset === "number" ? call.arguments.offset : void 0,
1695
- limit: typeof call.arguments.limit === "number" ? call.arguments.limit : void 0
1696
- });
1697
- if (!result.ok) return {
1698
- id: call.id,
1699
- name: call.name,
1700
- ok: false,
1701
- output: null,
1702
- error: result.error
1703
- };
1704
- return {
1705
- id: call.id,
1706
- name: call.name,
1707
- ok: true,
1708
- output: result.output
1709
- };
1710
1753
  });
1711
1754
  }
1712
1755
  //#endregion
1756
+ //#region src/runtime/tools/command-adapter.ts
1757
+ /** Commands that must never be exposed as LLM tools (pickers, storage boots, settings). */
1758
+ var SKIP = /* @__PURE__ */ new Set([
1759
+ "open_cognitive_ai_settings",
1760
+ "open_ai_config",
1761
+ "load_workspace",
1762
+ "connect_opfs",
1763
+ "connect_indexeddb",
1764
+ "disconnect_workspace",
1765
+ "disconnect_folder",
1766
+ "open_editor",
1767
+ "list_extensions",
1768
+ "enable_extension",
1769
+ "disable_extension"
1770
+ ]);
1771
+ var SKIP_PREFIXES = [
1772
+ "connect_",
1773
+ "disconnect_",
1774
+ "install_",
1775
+ "uninstall_"
1776
+ ];
1777
+ /** Built-in tools that must survive command refresh (not command-backed). */
1778
+ var KEEP_TOOLS = /* @__PURE__ */ new Set([
1779
+ "workspace_list",
1780
+ "workspace_read",
1781
+ "workspace_write"
1782
+ ]);
1783
+ function sanitizeName(id) {
1784
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
1785
+ }
1786
+ function shouldSkipCommand(id) {
1787
+ if (SKIP.has(id)) return true;
1788
+ return SKIP_PREFIXES.some((prefix) => id.startsWith(prefix));
1789
+ }
1790
+ /** Registers Docks commands as first-class LLM tools (tool name = command id). */
1791
+ function refreshCommandTools(broker, role) {
1792
+ for (const def of broker.listDefinitions("build")) if (!KEEP_TOOLS.has(def.name)) broker.unregister(def.name);
1793
+ const commands = commandRegistry.listCommands(commandRegistry.createExecutionContext());
1794
+ const entries = Object.values(commands);
1795
+ for (const command of entries) {
1796
+ if (!command?.id || shouldSkipCommand(command.id)) continue;
1797
+ if (!command.description) continue;
1798
+ if ((role === "plan" || role === "explore") && /delete|write|save|rm|mv/i.test(command.id)) continue;
1799
+ const toolName = sanitizeName(command.id);
1800
+ if (KEEP_TOOLS.has(toolName)) continue;
1801
+ const properties = {};
1802
+ const required = [];
1803
+ for (const p of command.parameters ?? []) {
1804
+ properties[p.name] = {
1805
+ type: p.type ?? "string",
1806
+ description: p.description,
1807
+ ...p.allowedValues ? { enum: p.allowedValues } : {}
1808
+ };
1809
+ if (p.required) required.push(p.name);
1810
+ }
1811
+ broker.register({
1812
+ name: toolName,
1813
+ description: `${command.name}: ${command.description}`,
1814
+ parameters: {
1815
+ type: "object",
1816
+ properties,
1817
+ required
1818
+ }
1819
+ }, async (call, _sessionId) => {
1820
+ try {
1821
+ const result = await commandRegistry.execute(command.id, { params: call.arguments });
1822
+ return {
1823
+ id: call.id,
1824
+ name: call.name,
1825
+ ok: true,
1826
+ output: result
1827
+ };
1828
+ } catch (err) {
1829
+ return {
1830
+ id: call.id,
1831
+ name: call.name,
1832
+ ok: false,
1833
+ output: null,
1834
+ error: err instanceof Error ? err.message : String(err)
1835
+ };
1836
+ }
1837
+ });
1838
+ }
1839
+ }
1840
+ //#endregion
1713
1841
  //#region src/runtime/compact/compaction.ts
1714
1842
  var COMPACT_SYSTEM = `You compress older chat turns into a durable summary for future assistant turns.
1715
1843
  Preserve: user goals, decisions, constraints, important file/paths, tool outcomes that still matter, and open questions.
@@ -2163,7 +2291,21 @@ async function runSpecialistLoop(input) {
2163
2291
  };
2164
2292
  const messages = historyToProviderMessages(input.systemPrompt, input.history, input.compactionSummary);
2165
2293
  let success = true;
2294
+ let cancelled = false;
2295
+ const markCancelled = () => {
2296
+ if (cancelled) return;
2297
+ cancelled = true;
2298
+ success = false;
2299
+ if (!parts.some((p) => p.type === "text")) emit({
2300
+ type: "text",
2301
+ text: "Cancelled."
2302
+ });
2303
+ };
2166
2304
  for (let iter = 0; iter < MAX_TOOL_ITERATIONS; iter++) {
2305
+ if (isAborted(input.signal)) {
2306
+ markCancelled();
2307
+ break;
2308
+ }
2167
2309
  input.refreshTools?.();
2168
2310
  const tools = input.broker.listDefinitions(input.role);
2169
2311
  let pendingTools = [];
@@ -2175,6 +2317,7 @@ async function runSpecialistLoop(input) {
2175
2317
  providerOptions: input.resolved.providerOptions,
2176
2318
  signal: input.signal
2177
2319
  })) {
2320
+ if (isAborted(input.signal)) break;
2178
2321
  if (chunk.type === "error") {
2179
2322
  emit({
2180
2323
  type: "text",
@@ -2203,6 +2346,10 @@ async function runSpecialistLoop(input) {
2203
2346
  } else pendingTools.push({ ...chunk.toolCall });
2204
2347
  }
2205
2348
  }
2349
+ if (isAborted(input.signal)) {
2350
+ markCancelled();
2351
+ break;
2352
+ }
2206
2353
  if (!pendingTools.length) {
2207
2354
  if (assistantText && !parts.some((p) => p.type === "text")) emit({
2208
2355
  type: "text",
@@ -2229,7 +2376,12 @@ async function runSpecialistLoop(input) {
2229
2376
  })),
2230
2377
  signal: input.signal
2231
2378
  }) : null;
2379
+ const pendingImages = [];
2232
2380
  for (const tool of pendingTools) {
2381
+ if (isAborted(input.signal)) {
2382
+ markCancelled();
2383
+ break;
2384
+ }
2233
2385
  let args = {};
2234
2386
  try {
2235
2387
  args = tool.arguments ? JSON.parse(tool.arguments) : {};
@@ -2267,26 +2419,30 @@ async function runSpecialistLoop(input) {
2267
2419
  name: tool.name,
2268
2420
  content: JSON.stringify(presentImage ? {
2269
2421
  ...safeOutput ?? {},
2270
- note: "Image bytes follow in the next user message for vision inspection."
2422
+ note: "Image bytes follow after all tool results for vision inspection."
2271
2423
  } : safeOutput)
2272
2424
  });
2273
- if (presentImage) {
2274
- const dataUrl = String(rawOutput.dataUrl);
2275
- const name = String(rawOutput.name ?? "image");
2276
- const id = String(rawOutput.id ?? "");
2277
- messages.push({
2278
- role: "user",
2279
- content: [{
2280
- type: "text",
2281
- text: `Attachment image for inspection: id=${id} name="${name}"`
2282
- }, {
2283
- type: "image_url",
2284
- image_url: { url: dataUrl }
2285
- }]
2286
- });
2287
- }
2425
+ if (presentImage) pendingImages.push({
2426
+ id: String(rawOutput.id ?? ""),
2427
+ name: String(rawOutput.name ?? "image"),
2428
+ dataUrl: String(rawOutput.dataUrl)
2429
+ });
2288
2430
  if (!result.ok) success = false;
2289
2431
  }
2432
+ if (cancelled || isAborted(input.signal)) {
2433
+ markCancelled();
2434
+ break;
2435
+ }
2436
+ if (pendingImages.length) messages.push({
2437
+ role: "user",
2438
+ content: [{
2439
+ type: "text",
2440
+ text: pendingImages.length === 1 ? `Attachment image for inspection: id=${pendingImages[0].id} name="${pendingImages[0].name}"` : `Attachment images for inspection:\n${pendingImages.map((img) => `- id=${img.id} name="${img.name}"`).join("\n")}`
2441
+ }, ...pendingImages.map((img) => ({
2442
+ type: "image_url",
2443
+ image_url: { url: img.dataUrl }
2444
+ }))]
2445
+ });
2290
2446
  }
2291
2447
  return {
2292
2448
  message: {
@@ -2325,7 +2481,14 @@ function coalesceParts(parts) {
2325
2481
  //#endregion
2326
2482
  //#region src/runtime/specialists/index.ts
2327
2483
  function systemPromptFor(role, goal, opts = {}) {
2328
- const preamble = `You are a specialist in the Eclipse Docks AI runtime. Prefer tools for file and command actions. Be concise. ${opts.hasAttachments ? "This turn publishes session attachments. To inspect them, call attachment_read with the attachment id from the user message (attachment_list if you need ids). Do not open canvases, editors, workspaces, or unrelated commands to find attachment contents — they are not workspace files." : "When the user publishes attachments, use attachment_list / attachment_read to inspect them — do not assume their contents are already in the transcript."}`;
2484
+ const preamble = `You are a specialist in the Eclipse Docks AI runtime. Prefer tools for file and command actions. Be concise. After tools finish, write a short human-friendly final reply (what you did and the outcome). Do not paste tool arguments, stdout, JSON, or command logs — those already appear in the UI details panel. ${opts.hasAttachments ? [
2485
+ "This turn publishes session attachments under the shared in-memory root (in-memory/<sessionId>/… or in-memory://<sessionId>/…).",
2486
+ "Read them with workspace_read / workspace_list — in-memory paths work without mounting that root into the workspace tree.",
2487
+ "Do not call load_workspace / connect_opfs / connect_indexeddb to find attachments.",
2488
+ "Canvas from a screenshot: workspace_read the attachment path → canvas.help → workspace_write a .canvas.js (into a durable workspace folder) → canvas.open / canvas.apply_source.",
2489
+ "Qualify durable writes as <root>/<file> using a root from workspace_list (e.g. tests/demo.canvas.js); bare filenames are auto-rooted when a single durable folder is connected, otherwise a folder picker appears.",
2490
+ "If no durable workspace folder is connected for writing canvases, say so and stop."
2491
+ ].join(" ") : "Session attachments live under the shared in-memory root and are readable via workspace_read / workspace_list."} Workspace paths use a connected root prefix from workspace_list (never write bare names at the composite root unless a single durable folder can auto-qualify them).`;
2329
2492
  switch (role) {
2330
2493
  case "plan": return `${preamble}\nMode: PLAN (read-only). Analyze and propose a plan for: ${goal}`;
2331
2494
  case "explore": return `${preamble}\nMode: EXPLORE (read-only). Search and report findings for: ${goal}`;
@@ -2453,6 +2616,82 @@ function formatBucketDetail(bucket) {
2453
2616
  return `${bucket.count} · ${tokens}`;
2454
2617
  }
2455
2618
  //#endregion
2619
+ //#region src/runtime/session-workspace.ts
2620
+ function attachmentContents(stored) {
2621
+ if (stored.kind === "image") {
2622
+ if (stored.dataUrl) return stored.dataUrl;
2623
+ if (stored.text) return stored.text;
2624
+ }
2625
+ return stored.text ?? "";
2626
+ }
2627
+ /** Build live-file callbacks that always re-read via the caller-provided resolver. */
2628
+ function attachmentLiveContents(resolve) {
2629
+ return {
2630
+ get mimeType() {
2631
+ return resolve()?.mime;
2632
+ },
2633
+ getContents() {
2634
+ const stored = resolve();
2635
+ if (!stored) throw new Error("Attachment payload is no longer available");
2636
+ return attachmentContents(stored);
2637
+ },
2638
+ size() {
2639
+ const stored = resolve();
2640
+ if (!stored) return null;
2641
+ if (stored.byteLength != null) return stored.byteLength;
2642
+ return attachmentContents(stored).length;
2643
+ }
2644
+ };
2645
+ }
2646
+ /** Relative path under the shared in-memory root (`<sessionId>/<file>`). */
2647
+ function relativeAttachmentPath(sessionId, stored) {
2648
+ return attachmentWorkspacePath(sessionId, stored).slice(`${IN_MEMORY_ROOT_NAME}/`.length);
2649
+ }
2650
+ /**
2651
+ * Publish a live file node. Payload is resolved only through `resolve` — nothing is copied.
2652
+ */
2653
+ async function syncAttachmentToWorkspace(sessionId, stored, resolve) {
2654
+ const root = getInMemoryRoot();
2655
+ const relative = relativeAttachmentPath(sessionId, stored);
2656
+ root.writeLiveFile(relative, attachmentLiveContents(resolve));
2657
+ return attachmentWorkspacePath(sessionId, stored);
2658
+ }
2659
+ async function syncAttachmentsToWorkspace(sessionId, attachments, resolveById) {
2660
+ for (const attachment of attachments) await syncAttachmentToWorkspace(sessionId, attachment, () => resolveById(attachment.id));
2661
+ }
2662
+ async function removeAttachmentFromWorkspace(sessionId, stored) {
2663
+ const root = getInMemoryRoot();
2664
+ const relative = relativeAttachmentPath(sessionId, stored);
2665
+ const resource = await root.getResource(relative);
2666
+ if (resource) await resource.delete();
2667
+ }
2668
+ async function removeSessionWorkspaceFolder(sessionId) {
2669
+ const dir = await getInMemoryRoot().getResource(sessionId);
2670
+ if (dir instanceof Directory) await dir.delete();
2671
+ }
2672
+ /**
2673
+ * Restrict the workspace-mounted in-memory view to a single folder name (active session).
2674
+ * Pass `null` to show the full tree again.
2675
+ */
2676
+ function projectInMemoryMountToFolder(folderName) {
2677
+ if (!folderName) {
2678
+ setInMemoryMountProjection(null);
2679
+ return;
2680
+ }
2681
+ const name = folderName;
2682
+ setInMemoryMountProjection({
2683
+ async listChildren(root) {
2684
+ return [root.ensureDirectoryEntry(name)];
2685
+ },
2686
+ async getResource(root, path, options) {
2687
+ const segments = path.split("/").filter((s) => s.trim());
2688
+ if (segments[0] !== name) return null;
2689
+ if (segments.length === 1 && (path.endsWith("/") || path === name)) return root.ensureDirectoryEntry(name);
2690
+ return root.getResource(path, options);
2691
+ }
2692
+ });
2693
+ }
2694
+ //#endregion
2456
2695
  //#region src/runtime/cognitive-runtime.ts
2457
2696
  var PERSIST_KEY = "cognitive/v1/state";
2458
2697
  /** Auto-compact after a turn when live history exceeds this many messages. */
@@ -2487,12 +2726,6 @@ var CognitiveRuntime = class {
2487
2726
  emit: false
2488
2727
  });
2489
2728
  registerWorkspaceTools(this.tools);
2490
- registerAttachmentTools(this.tools, (sessionId, attachmentId) => {
2491
- const list = this.sessions.get(sessionId)?.attachments ?? [];
2492
- if (!attachmentId) return list;
2493
- const found = list.find((a) => a.id === attachmentId);
2494
- return found ? [found] : [];
2495
- });
2496
2729
  this.tools.setApprovalMode(this.approvalMode);
2497
2730
  this.tools.setPermissionHandler(async (req) => this.requestPermission(req));
2498
2731
  this.ready = this.hydrateAll();
@@ -2554,7 +2787,10 @@ var CognitiveRuntime = class {
2554
2787
  return catalog.find((e) => e.providerId === this.providersSettings.defaultProviderId)?.id ?? catalog[0]?.id ?? "openai/gpt-4o-mini";
2555
2788
  }
2556
2789
  setModelPolicy(policy) {
2557
- this.policy = policy;
2790
+ const next = normalizeModelPolicy(policy) ?? this.policy;
2791
+ if (sameModelPolicy(this.policy, next)) return;
2792
+ this.policy = next;
2793
+ this.persist();
2558
2794
  }
2559
2795
  getModelPolicy() {
2560
2796
  return this.policy;
@@ -2616,28 +2852,46 @@ var CognitiveRuntime = class {
2616
2852
  if (focused) return focused.id;
2617
2853
  return this.sessions.createSession().id;
2618
2854
  }
2619
- /** Store attachment payloads on the session (not inlined into history until prompt). */
2620
- addAttachments(sessionId, attachments) {
2855
+ /** Store attachment payloads on the session and mirror them into in-memory/<sessionId>/. */
2856
+ async addAttachments(sessionId, attachments) {
2621
2857
  const id = this.ensureSessionId(sessionId);
2622
2858
  const session = this.sessions.get(id);
2623
2859
  if (!session || !attachments.length) return [];
2624
2860
  const room = Math.max(0, 8 - (session.attachments?.length ?? 0));
2625
- const accepted = attachments.slice(0, room).map((attachment) => toSessionAttachment(attachment));
2861
+ const accepted = attachments.slice(0, room).map((attachment) => {
2862
+ const stored = toSessionAttachment(attachment);
2863
+ return {
2864
+ ...stored,
2865
+ path: attachmentUri(id, stored)
2866
+ };
2867
+ });
2626
2868
  if (!accepted.length) return [];
2627
2869
  this.sessions.update(id, { attachments: [...session.attachments ?? [], ...accepted] });
2628
2870
  this.commitSessions();
2871
+ await this.mirrorAttachments(id, accepted).catch(() => void 0);
2629
2872
  return accepted;
2630
2873
  }
2631
- /** Remove an attachment payload from the session store. */
2874
+ /** Remove an attachment payload from the session store and in-memory workspace. */
2632
2875
  removeAttachment(sessionId, attachmentId) {
2633
2876
  const session = this.sessions.get(sessionId);
2634
2877
  if (!session) return false;
2878
+ const removed = (session.attachments ?? []).find((a) => a.id === attachmentId);
2635
2879
  const next = (session.attachments ?? []).filter((a) => a.id !== attachmentId);
2636
2880
  if (next.length === (session.attachments?.length ?? 0)) return false;
2637
2881
  this.sessions.update(sessionId, { attachments: next });
2638
2882
  this.commitSessions();
2883
+ if (removed) removeAttachmentFromWorkspace(sessionId, removed);
2639
2884
  return true;
2640
2885
  }
2886
+ /** Drop a session and its in-memory attachment folder. */
2887
+ removeSession(sessionId) {
2888
+ const ok = this.sessions.remove(sessionId);
2889
+ if (ok) {
2890
+ removeSessionWorkspaceFolder(sessionId);
2891
+ this.commitSessions();
2892
+ }
2893
+ return ok;
2894
+ }
2641
2895
  /**
2642
2896
  * Compact a session: drop older turns, store a summary (LLM when possible),
2643
2897
  * and persist. The summary is injected into later model calls.
@@ -2681,8 +2935,17 @@ var CognitiveRuntime = class {
2681
2935
  const session = (input.sessionId ? this.sessions.get(input.sessionId) : this.sessions.focused()) ?? this.sessions.createSession();
2682
2936
  const text = input.text.trim();
2683
2937
  const messageId = `msg-${Date.now()}-u`;
2684
- const fresh = (input.attachments ?? []).slice(0, 8).map((attachment) => toSessionAttachment(attachment, { messageId }));
2685
- if (fresh.length) this.sessions.update(session.id, { attachments: [...this.sessions.get(session.id)?.attachments ?? [], ...fresh] });
2938
+ const fresh = (input.attachments ?? []).slice(0, 8).map((attachment) => {
2939
+ const stored = toSessionAttachment(attachment, { messageId });
2940
+ return {
2941
+ ...stored,
2942
+ path: attachmentUri(session.id, stored)
2943
+ };
2944
+ });
2945
+ if (fresh.length) {
2946
+ this.sessions.update(session.id, { attachments: [...this.sessions.get(session.id)?.attachments ?? [], ...fresh] });
2947
+ await this.mirrorAttachments(session.id, fresh);
2948
+ }
2686
2949
  const existingIds = (input.attachmentIds ?? []).filter(Boolean);
2687
2950
  const stored = this.sessions.get(session.id)?.attachments ?? [];
2688
2951
  const byId = new Map(stored.map((a) => [a.id, a]));
@@ -2693,12 +2956,16 @@ var CognitiveRuntime = class {
2693
2956
  if (found && !published.some((p) => p.id === found.id)) published.push(found);
2694
2957
  }
2695
2958
  if (!text && !published.length) throw new Error("Prompt requires text or at least one attachment");
2959
+ if (published.length) await this.mirrorAttachments(session.id, published).catch(() => void 0);
2696
2960
  const parts = [];
2697
2961
  if (text) parts.push({
2698
2962
  type: "text",
2699
2963
  text
2700
2964
  });
2701
- for (const item of published) parts.push(toFileRefPart(item));
2965
+ for (const item of published) parts.push(toFileRefPart({
2966
+ ...item,
2967
+ path: item.path || attachmentUri(session.id, item)
2968
+ }));
2702
2969
  const promptSummary = summarizePrompt(text, published.map((a) => ({
2703
2970
  name: a.name,
2704
2971
  kind: a.kind
@@ -2802,7 +3069,7 @@ var CognitiveRuntime = class {
2802
3069
  sessionId: session.id,
2803
3070
  activation: .7
2804
3071
  });
2805
- if ((this.sessions.get(session.id)?.history.length ?? 0) > AUTO_COMPACT_AFTER) await this.compactSession(session.id, COMPACT_KEEP_COUNT, input.signal);
3072
+ if (!Boolean(input.signal?.aborted) && (this.sessions.get(session.id)?.history.length ?? 0) > AUTO_COMPACT_AFTER) await this.compactSession(session.id, COMPACT_KEEP_COUNT, input.signal);
2806
3073
  this.sessions.update(session.id, { status: "idle" });
2807
3074
  this.events.publish(TOPIC.messageDone, {
2808
3075
  sessionId: session.id,
@@ -2863,11 +3130,17 @@ var CognitiveRuntime = class {
2863
3130
  });
2864
3131
  }
2865
3132
  emitSessions() {
3133
+ const focusedId = this.sessions.focused()?.id ?? null;
3134
+ projectInMemoryMountToFolder(focusedId);
2866
3135
  this.events.publish(TOPIC.sessionChanged, {
2867
3136
  sessions: this.sessions.list(),
2868
- focusedId: this.sessions.focused()?.id ?? null
3137
+ focusedId
2869
3138
  });
2870
3139
  }
3140
+ /** Live in-memory file refs that resolve payloads from the session store. */
3141
+ mirrorAttachments(sessionId, attachments) {
3142
+ return syncAttachmentsToWorkspace(sessionId, attachments, (attachmentId) => this.sessions.get(sessionId)?.attachments?.find((a) => a.id === attachmentId));
3143
+ }
2871
3144
  async hydrateAll() {
2872
3145
  await loadModelsCatalog().catch(() => void 0);
2873
3146
  await this.hydrateProviders();
@@ -2879,6 +3152,16 @@ var CognitiveRuntime = class {
2879
3152
  if (!this.sessions.list().length) this.sessions.createSession({ title: "Chat" });
2880
3153
  const mostRecent = this.sessions.list()[0];
2881
3154
  if (mostRecent) this.sessions.focus(mostRecent.id);
3155
+ for (const session of this.sessions.list()) {
3156
+ const list = session.attachments ?? [];
3157
+ if (!list.length) continue;
3158
+ const stamped = list.map((a) => a.path ? a : {
3159
+ ...a,
3160
+ path: attachmentUri(session.id, a)
3161
+ });
3162
+ if (stamped.some((a, i) => a.path !== list[i]?.path)) this.sessions.update(session.id, { attachments: stamped });
3163
+ await this.mirrorAttachments(session.id, stamped).catch(() => void 0);
3164
+ }
2882
3165
  this.emitSessions();
2883
3166
  }
2884
3167
  async hydrateProviders() {
@@ -2906,6 +3189,8 @@ var CognitiveRuntime = class {
2906
3189
  this.approvalMode = saved.approvalMode;
2907
3190
  this.tools.setApprovalMode(saved.approvalMode);
2908
3191
  }
3192
+ const policy = normalizeModelPolicy(saved.modelPolicy);
3193
+ if (policy) this.policy = policy;
2909
3194
  } catch {}
2910
3195
  }
2911
3196
  async persist() {
@@ -2913,7 +3198,8 @@ var CognitiveRuntime = class {
2913
3198
  await persistenceService.persistObject(PERSIST_KEY, {
2914
3199
  sessions: this.sessions.snapshot(),
2915
3200
  memory: this.memory.snapshot(),
2916
- approvalMode: this.approvalMode
3201
+ approvalMode: this.approvalMode,
3202
+ modelPolicy: this.policy
2917
3203
  });
2918
3204
  } catch {}
2919
3205
  }
@@ -2921,6 +3207,32 @@ var CognitiveRuntime = class {
2921
3207
  function isToolApprovalMode(value) {
2922
3208
  return value === "manual" || value === "auto_safe" || value === "auto_all";
2923
3209
  }
3210
+ function isOptimizeFor(value) {
3211
+ return value === "cost" || value === "balance" || value === "intelligence";
3212
+ }
3213
+ function isEffortOrAuto(value) {
3214
+ return value === "auto" || value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
3215
+ }
3216
+ function normalizeModelPolicy(value) {
3217
+ if (!value || typeof value !== "object") return null;
3218
+ const raw = value;
3219
+ if (raw.mode === "auto" && isOptimizeFor(raw.optimizeFor)) return {
3220
+ mode: "auto",
3221
+ optimizeFor: raw.optimizeFor
3222
+ };
3223
+ if (raw.mode === "manual" && typeof raw.model === "string" && raw.model.trim()) return {
3224
+ mode: "manual",
3225
+ model: raw.model.trim(),
3226
+ ...isEffortOrAuto(raw.effort) ? { effort: raw.effort } : {}
3227
+ };
3228
+ return null;
3229
+ }
3230
+ function sameModelPolicy(a, b) {
3231
+ if (a.mode !== b.mode) return false;
3232
+ if (a.mode === "auto" && b.mode === "auto") return a.optimizeFor === b.optimizeFor;
3233
+ if (a.mode === "manual" && b.mode === "manual") return a.model === b.model && (a.effort ?? "auto") === (b.effort ?? "auto");
3234
+ return false;
3235
+ }
2924
3236
  function externalCatalogEntry(id, providerId) {
2925
3237
  return {
2926
3238
  id,
@@ -2949,4 +3261,4 @@ function summarizePrompt(text, attachments) {
2949
3261
  //#endregion
2950
3262
  export { AI_SETTINGS_KEY as _, formatContextPercent as a, TOPIC as b, readPromptAttachmentFromFile as c, defaultProvidersSettings as d, listConfiguredModelIds as f, AI_SETTINGS_EDITOR_ID as g, loadModelsCatalog as h, formatBucketDetail as i, OpenAICompatibleProvider as l, detectEffortCapability as m, cognitiveRuntime as n, formatTokenCount as o, parseModelIds as p, estimateSessionContextUsage as r, ATTACHMENT_FILE_ACCEPT as s, CognitiveRuntime as t, fetchOpenAICompatibleModels as u, AI_VIEW_NAME as v, OPEN_AI_SETTINGS_COMMAND as y };
2951
3263
 
2952
- //# sourceMappingURL=runtime-DfNGzEjU.js.map
3264
+ //# sourceMappingURL=runtime-By5PP2BC.js.map