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

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 (66) hide show
  1. package/README.md +2 -2
  2. package/dist/api.d.ts +1 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +87 -2
  5. package/dist/api.js.map +1 -0
  6. package/dist/{cognitive-extension-CG_d39K5.js → cognitive-extension-BKGxwLYj.js} +524 -102
  7. package/dist/cognitive-extension-BKGxwLYj.js.map +1 -0
  8. package/dist/cognitive-extension.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/runtime/abort.d.ts +5 -0
  11. package/dist/runtime/abort.d.ts.map +1 -0
  12. package/dist/runtime/attachment-paths.d.ts +17 -0
  13. package/dist/runtime/attachment-paths.d.ts.map +1 -0
  14. package/dist/runtime/attachments.d.ts +5 -5
  15. package/dist/runtime/attachments.d.ts.map +1 -1
  16. package/dist/runtime/cognitive-runtime.d.ts +12 -3
  17. package/dist/runtime/cognitive-runtime.d.ts.map +1 -1
  18. package/dist/runtime/context/usage.d.ts +3 -0
  19. package/dist/runtime/context/usage.d.ts.map +1 -1
  20. package/dist/runtime/index.d.ts +4 -0
  21. package/dist/runtime/index.d.ts.map +1 -1
  22. package/dist/runtime/models/openai-compatible.d.ts.map +1 -1
  23. package/dist/runtime/models/types.d.ts +6 -1
  24. package/dist/runtime/models/types.d.ts.map +1 -1
  25. package/dist/runtime/session-workspace.d.ts +20 -0
  26. package/dist/runtime/session-workspace.d.ts.map +1 -0
  27. package/dist/runtime/skills-commands.d.ts +3 -0
  28. package/dist/runtime/skills-commands.d.ts.map +1 -0
  29. package/dist/runtime/skills-parse.d.ts +14 -0
  30. package/dist/runtime/skills-parse.d.ts.map +1 -0
  31. package/dist/runtime/skills-resolve.d.ts +9 -0
  32. package/dist/runtime/skills-resolve.d.ts.map +1 -0
  33. package/dist/runtime/skills-select.d.ts +9 -0
  34. package/dist/runtime/skills-select.d.ts.map +1 -0
  35. package/dist/runtime/skills-types.d.ts +47 -0
  36. package/dist/runtime/skills-types.d.ts.map +1 -0
  37. package/dist/runtime/skills.d.ts +13 -0
  38. package/dist/runtime/skills.d.ts.map +1 -0
  39. package/dist/runtime/specialists/index.d.ts.map +1 -1
  40. package/dist/runtime/specialists/loop.d.ts.map +1 -1
  41. package/dist/runtime/tools/command-adapter.d.ts.map +1 -1
  42. package/dist/runtime/tools/fetch-adapter.d.ts +4 -0
  43. package/dist/runtime/tools/fetch-adapter.d.ts.map +1 -0
  44. package/dist/runtime/tools/index.d.ts +1 -1
  45. package/dist/runtime/tools/index.d.ts.map +1 -1
  46. package/dist/runtime/tools/workspace-adapter.d.ts +1 -1
  47. package/dist/runtime/tools/workspace-adapter.d.ts.map +1 -1
  48. package/dist/runtime/tools/workspace-path-qualify.d.ts +22 -0
  49. package/dist/runtime/tools/workspace-path-qualify.d.ts.map +1 -0
  50. package/dist/runtime/tools/workspace-path.d.ts +17 -0
  51. package/dist/runtime/tools/workspace-path.d.ts.map +1 -0
  52. package/dist/runtime/types.d.ts +7 -0
  53. package/dist/runtime/types.d.ts.map +1 -1
  54. package/dist/{runtime-DfNGzEjU.js → runtime-B0IHjzQO.js} +1042 -401
  55. package/dist/runtime-B0IHjzQO.js.map +1 -0
  56. package/dist/view/busy-status.d.ts +12 -0
  57. package/dist/view/busy-status.d.ts.map +1 -0
  58. package/dist/view/cognitive-settings-editor.d.ts +0 -1
  59. package/dist/view/cognitive-settings-editor.d.ts.map +1 -1
  60. package/dist/view/cognitive-view.d.ts +22 -0
  61. package/dist/view/cognitive-view.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/dist/cognitive-extension-CG_d39K5.js.map +0 -1
  64. package/dist/runtime/tools/attachment-adapter.d.ts +0 -6
  65. package/dist/runtime/tools/attachment-adapter.d.ts.map +0 -1
  66. 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, contributionRegistry, filebrowserDialog, getInMemoryRoot, persistenceService, registerAll, 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;
@@ -613,11 +624,16 @@ async function fetchOpenAICompatibleModels(baseUrl, apiKey, signal) {
613
624
  function buildChatCompletionBody(params) {
614
625
  const model = params.model.includes("/") ? params.model.split("/").slice(1).join("/") : params.model;
615
626
  const { reasoning_effort: _a, reasoningEffort: _b, reasoning: _c, ...safeOptions } = params.providerOptions ?? {};
627
+ const { stream_options: userStreamOptions, ...restSafeOptions } = safeOptions;
616
628
  const body = {
617
629
  model,
618
630
  messages: toOpenAIMessages(params.messages),
619
631
  stream: true,
620
- ...safeOptions
632
+ ...restSafeOptions,
633
+ stream_options: {
634
+ ...userStreamOptions && typeof userStreamOptions === "object" ? userStreamOptions : {},
635
+ include_usage: true
636
+ }
621
637
  };
622
638
  const requestedEffort = params.providerOptions?.reasoning_effort ?? params.providerOptions?.reasoningEffort;
623
639
  const reasoningModel = detectEffortCapability(params.model).supported || requestedEffort != null;
@@ -665,6 +681,10 @@ var OpenAICompatibleProvider = class {
665
681
  signal: params.signal
666
682
  });
667
683
  } catch (err) {
684
+ if (params.signal?.aborted || isAbortError(err)) {
685
+ yield { type: "done" };
686
+ return;
687
+ }
668
688
  yield {
669
689
  type: "error",
670
690
  error: err instanceof Error ? err.message : String(err)
@@ -683,7 +703,21 @@ var OpenAICompatibleProvider = class {
683
703
  let buffer = "";
684
704
  const toolAcc = /* @__PURE__ */ new Map();
685
705
  while (true) {
686
- const { done, value } = await reader.read();
706
+ let done;
707
+ let value;
708
+ try {
709
+ ({done, value} = await reader.read());
710
+ } catch (err) {
711
+ if (params.signal?.aborted || isAbortError(err)) {
712
+ yield { type: "done" };
713
+ return;
714
+ }
715
+ yield {
716
+ type: "error",
717
+ error: err instanceof Error ? err.message : String(err)
718
+ };
719
+ return;
720
+ }
687
721
  if (done) break;
688
722
  buffer += decoder.decode(value, { stream: true });
689
723
  const lines = buffer.split("\n");
@@ -697,15 +731,22 @@ var OpenAICompatibleProvider = class {
697
731
  return;
698
732
  }
699
733
  try {
700
- const delta = JSON.parse(data).choices?.[0]?.delta;
734
+ const json = JSON.parse(data);
735
+ const usage = parseStreamUsage(json.usage);
736
+ if (usage) yield {
737
+ type: "usage",
738
+ usage
739
+ };
740
+ const delta = json.choices?.[0]?.delta;
701
741
  if (!delta) continue;
702
- if (delta.content) yield {
742
+ if (typeof delta.content === "string" && delta.content) yield {
703
743
  type: "text",
704
744
  text: delta.content
705
745
  };
706
- if (delta.reasoning_content || delta.reasoning) yield {
746
+ const reasoning = delta.reasoning_content ?? delta.reasoning;
747
+ if (reasoning) yield {
707
748
  type: "reasoning",
708
- text: String(delta.reasoning_content ?? delta.reasoning)
749
+ text: String(reasoning)
709
750
  };
710
751
  if (Array.isArray(delta.tool_calls)) for (const tc of delta.tool_calls) {
711
752
  const idx = tc.index ?? 0;
@@ -729,6 +770,17 @@ var OpenAICompatibleProvider = class {
729
770
  yield { type: "done" };
730
771
  }
731
772
  };
773
+ function parseStreamUsage(usage) {
774
+ if (!usage) return null;
775
+ const inputTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
776
+ const outputTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
777
+ if (!Number.isFinite(inputTokens) && !Number.isFinite(outputTokens)) return null;
778
+ if (inputTokens <= 0 && outputTokens <= 0) return null;
779
+ return {
780
+ inputTokens: Math.max(0, Math.round(inputTokens) || 0),
781
+ outputTokens: Math.max(0, Math.round(outputTokens) || 0)
782
+ };
783
+ }
732
784
  //#endregion
733
785
  //#region src/runtime/models/router.ts
734
786
  var EFFORT_ORDER = [
@@ -955,227 +1007,40 @@ function isMutatingName(name) {
955
1007
  return /^(write|edit|delete|mkdir|rm|mv|save|apply)/i.test(name) || name.includes("write") || name.includes("delete");
956
1008
  }
957
1009
  //#endregion
958
- //#region src/runtime/tools/workspace-adapter.ts
959
- function asRecord(value) {
960
- return value && typeof value === "object" ? value : {};
1010
+ //#region src/runtime/attachment-paths.ts
1011
+ /** Mirrors core `IN_MEMORY_ROOT_NAME` without importing the filesys graph. */
1012
+ var IN_MEMORY_ROOT_NAME$1 = "in-memory";
1013
+ /** URI scheme for UI references (`in-memory://<sessionId>/<file>`). */
1014
+ var IN_MEMORY_URI_SCHEME = "in-memory://";
1015
+ /** Sanitize a single path segment (no slashes). Prefer id-prefixed names for uniqueness. */
1016
+ function safeAttachmentFileName(name, id) {
1017
+ return `${id}-${(name || "attachment").replace(/[/\\]/g, "_").trim() || "attachment"}`;
1018
+ }
1019
+ /** Workspace path used by workspace_* tools (no protocol). */
1020
+ function attachmentWorkspacePath(sessionId, stored) {
1021
+ return `${IN_MEMORY_ROOT_NAME$1}/${sessionId}/${safeAttachmentFileName(stored.name, stored.id)}`;
1022
+ }
1023
+ /** UI / catalog URI form (`in-memory://<sessionId>/<file>`). */
1024
+ function attachmentUri(sessionId, stored) {
1025
+ return `${IN_MEMORY_URI_SCHEME}${sessionId}/${safeAttachmentFileName(stored.name, stored.id)}`;
961
1026
  }
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
- });
1111
- }
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
- });
1027
+ /**
1028
+ * Map `in-memory://…` references to composite workspace paths.
1029
+ * Real workspace paths are returned unchanged.
1030
+ */
1031
+ function toWorkspaceToolPath(path) {
1032
+ const trimmed = path.trim();
1033
+ const lower = trimmed.toLowerCase();
1034
+ if (lower === "in-memory:" || lower === "in-memory://" || lower === "in-memory") return IN_MEMORY_ROOT_NAME$1;
1035
+ if (lower.startsWith("in-memory://")) {
1036
+ const rest = trimmed.slice(12).replace(/^\/+/, "");
1037
+ return rest ? `${IN_MEMORY_ROOT_NAME$1}/${rest}` : IN_MEMORY_ROOT_NAME$1;
1174
1038
  }
1039
+ return trimmed;
1175
1040
  }
1176
1041
  //#endregion
1177
1042
  //#region src/runtime/attachments.ts
1178
- /** Soft cap per attached text file when stored / returned by attachment_read. */
1043
+ /** Soft cap per attached text file when stored / returned via workspace_read. */
1179
1044
  var MAX_ATTACHMENT_CHARS = 1e5;
1180
1045
  /** Soft cap for visual media attachments (bytes). */
1181
1046
  var MAX_IMAGE_BYTES = 4e6;
@@ -1384,7 +1249,7 @@ function toSessionAttachment(attachment, opts = {}) {
1384
1249
  messageId: opts.messageId
1385
1250
  };
1386
1251
  }
1387
- /** Lightweight history/UI reference — no payload. */
1252
+ /** Lightweight history/UI reference — no payload (path is an in-memory workspace path/URI). */
1388
1253
  function toFileRefPart(stored) {
1389
1254
  return {
1390
1255
  type: "file",
@@ -1398,14 +1263,15 @@ function toFileRefPart(stored) {
1398
1263
  }
1399
1264
  /** One-line catalog entry published into the user turn for the model. */
1400
1265
  function formatAttachmentRefForModel(part) {
1401
- const label = part.path?.trim() || part.name;
1266
+ const display = part.path || part.name;
1267
+ const toolPath = toWorkspaceToolPath(display);
1402
1268
  const kind = part.kind ?? "file";
1403
1269
  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}"`;
1270
+ return `[Attachment published: ${display} kind=${kind}${part.mime ? ` mime=${part.mime}` : ""}${bytes}] → workspace_read path="${toolPath}"`;
1405
1271
  }
1406
1272
  /**
1407
1273
  * Build provider user content from history parts.
1408
- * File parts are references only — payloads stay in the session store.
1274
+ * File parts are references only — payloads live under the in-memory workspace root.
1409
1275
  */
1410
1276
  function userContentFromParts(parts) {
1411
1277
  const texts = [];
@@ -1416,71 +1282,10 @@ function userContentFromParts(parts) {
1416
1282
  }
1417
1283
  const body = [...texts, ...refs].join("\n").trim();
1418
1284
  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
- };
1285
+ 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
1286
  }
1453
1287
  /** Marker field: specialist loop presents image bytes in a follow-up multimodal message. */
1454
1288
  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
1289
  function stripAttachmentToolOutput(output) {
1485
1290
  if (!output || typeof output !== "object") return output;
1486
1291
  const copy = { ...output };
@@ -1608,107 +1413,579 @@ async function readPromptAttachmentFromFile(file) {
1608
1413
  byteLength: file.size,
1609
1414
  truncated
1610
1415
  }
1611
- };
1416
+ };
1417
+ }
1418
+ //#endregion
1419
+ //#region src/runtime/tools/workspace-path-qualify.ts
1420
+ function isInMemoryToolPath(path) {
1421
+ return path === "in-memory" || path.startsWith(`in-memory/`);
1422
+ }
1423
+ function isWorkspaceRootPath(path) {
1424
+ const trimmed = path.trim();
1425
+ return !trimmed || trimmed === "." || trimmed === "/";
1426
+ }
1427
+ /** First path segment — composite roots are keyed by this name. */
1428
+ function firstPathSegment(path) {
1429
+ return path.split("/").filter(Boolean)[0] ?? "";
1430
+ }
1431
+ function pathHasKnownRoot(path, rootNames) {
1432
+ const first = firstPathSegment(path);
1433
+ if (!first) return false;
1434
+ return (rootNames instanceof Set ? rootNames : new Set(rootNames)).has(first);
1435
+ }
1436
+ function joinUnderRoot(rootPath, relativePath) {
1437
+ const root = rootPath.replace(/\/+$/, "");
1438
+ const relative = relativePath.replace(/^\/+/, "");
1439
+ if (!root) return relative;
1440
+ if (!relative) return root;
1441
+ return `${root}/${relative}`;
1442
+ }
1443
+ /**
1444
+ * Decide how to qualify a workspace tool path that is missing a connected-root prefix.
1445
+ */
1446
+ function decideRootQualification(path, durableRoots, mode) {
1447
+ if (isInMemoryToolPath(path) || isWorkspaceRootPath(path)) return { action: "keep" };
1448
+ if (pathHasKnownRoot(path, durableRoots)) return { action: "keep" };
1449
+ if (durableRoots.length === 1) return {
1450
+ action: "prefix",
1451
+ root: durableRoots[0]
1452
+ };
1453
+ if (durableRoots.length === 0) return {
1454
+ action: "error",
1455
+ error: mode === "write" ? "No durable workspace folder connected. Connect a folder before writing, or use an in-memory/… path." : "No workspace folder connected."
1456
+ };
1457
+ if (mode === "write") return { action: "pick" };
1458
+ return {
1459
+ action: "error",
1460
+ error: `Path must start with a workspace root (${durableRoots.join(", ")}). Example: ${durableRoots[0]}/${path}`
1461
+ };
1462
+ }
1463
+ //#endregion
1464
+ //#region src/runtime/tools/workspace-path.ts
1465
+ /**
1466
+ * Ensure tool paths are under a composite workspace root.
1467
+ * - Already rooted / in-memory / empty → unchanged
1468
+ * - One durable root → auto-prefix
1469
+ * - Several durable roots (writes) → folder picker dialog
1470
+ */
1471
+ async function qualifyWorkspaceToolPath(rawPath, mode = "write") {
1472
+ const path = toWorkspaceToolPath(String(rawPath ?? ""));
1473
+ if (isInMemoryToolPath(path) || isWorkspaceRootPath(path)) return {
1474
+ ok: true,
1475
+ path
1476
+ };
1477
+ const folders = await workspaceService.getFolders();
1478
+ const allNames = folders.map((f) => f.name);
1479
+ if (pathHasKnownRoot(path, allNames)) return {
1480
+ ok: true,
1481
+ path
1482
+ };
1483
+ const durableRoots = folders.filter((f) => f.type !== "in-memory").map((f) => f.name);
1484
+ const decision = decideRootQualification(path, mode === "write" ? durableRoots : allNames.length ? allNames : durableRoots, mode);
1485
+ if (decision.action === "keep") return {
1486
+ ok: true,
1487
+ path
1488
+ };
1489
+ if (decision.action === "prefix") return {
1490
+ ok: true,
1491
+ path: joinUnderRoot(decision.root, path),
1492
+ resolvedFrom: path
1493
+ };
1494
+ if (decision.action === "error") return {
1495
+ ok: false,
1496
+ error: decision.error
1497
+ };
1498
+ const chosen = await filebrowserDialog("directory");
1499
+ if (!chosen) return {
1500
+ ok: false,
1501
+ error: "Write cancelled: no workspace folder selected."
1502
+ };
1503
+ return {
1504
+ ok: true,
1505
+ path: joinUnderRoot(chosen, path),
1506
+ resolvedFrom: path
1507
+ };
1508
+ }
1509
+ //#endregion
1510
+ //#region src/runtime/tools/workspace-adapter.ts
1511
+ function asRecord$1(value) {
1512
+ return value && typeof value === "object" ? value : {};
1513
+ }
1514
+ /** Some File backends (e.g. in-memory) expose mime; the abstract File does not. */
1515
+ function optionalMimeType(resource) {
1516
+ const getMimeType = resource.getMimeType;
1517
+ return typeof getMimeType === "function" ? getMimeType.call(resource) : void 0;
1518
+ }
1519
+ function fileNameFromPath(path) {
1520
+ const parts = path.split("/").filter(Boolean);
1521
+ return parts[parts.length - 1] || path;
1522
+ }
1523
+ /** Resolve a tool path against the shared in-memory root or the mounted workspace. */
1524
+ async function resolveResource(path, options) {
1525
+ if (isInMemoryToolPath(path)) {
1526
+ const root = getInMemoryRoot();
1527
+ const relative = path === IN_MEMORY_ROOT_NAME ? "" : path.slice(`${IN_MEMORY_ROOT_NAME}/`.length);
1528
+ if (!relative) return {
1529
+ resource: root,
1530
+ path: IN_MEMORY_ROOT_NAME
1531
+ };
1532
+ const lookup = options?.directory && !relative.endsWith("/") ? `${relative}/` : relative;
1533
+ return {
1534
+ resource: await root.getResource(lookup, options?.create ? { create: true } : void 0),
1535
+ path
1536
+ };
1537
+ }
1538
+ const workspace = await workspaceService.getWorkspace();
1539
+ if (!workspace) return {
1540
+ resource: null,
1541
+ path
1542
+ };
1543
+ if (isWorkspaceRootPath(path)) return {
1544
+ resource: workspace,
1545
+ path: ""
1546
+ };
1547
+ const lookup = options?.directory && !path.endsWith("/") ? `${path}/` : path;
1548
+ return {
1549
+ resource: await workspace.getResource(lookup, options?.create ? { create: true } : void 0),
1550
+ path
1551
+ };
1552
+ }
1553
+ /**
1554
+ * Present image-like workspace files for vision (data URLs or known image extensions).
1555
+ * Returns null when the file should be treated as ordinary text.
1556
+ */
1557
+ function visionToolOutput(path, contents, mimeHint) {
1558
+ const name = fileNameFromPath(path);
1559
+ const dataUrl = typeof contents === "string" && contents.startsWith("data:image/") ? contents : void 0;
1560
+ const mime = mimeHint || (dataUrl ? dataUrl.slice(5, dataUrl.indexOf(";")) : void 0) || resolveMediaType({
1561
+ name,
1562
+ mime: mimeHint
1563
+ })?.mime;
1564
+ if (isSvgAttachment({
1565
+ name,
1566
+ mime,
1567
+ text: dataUrl ? void 0 : contents
1568
+ })) return null;
1569
+ const media = resolveMediaType({
1570
+ name,
1571
+ mime
1572
+ });
1573
+ if (!dataUrl && media?.mode !== "raster") return null;
1574
+ if (!dataUrl) return null;
1575
+ return {
1576
+ path,
1577
+ name,
1578
+ mime,
1579
+ kind: "image",
1580
+ ...media?.vision !== false ? {
1581
+ [IMAGE_PRESENT_KEY]: true,
1582
+ dataUrl,
1583
+ note: "Image bytes will be provided to the model in the next message for vision inspection."
1584
+ } : {
1585
+ dataUrlOmitted: true,
1586
+ note: "This media type is stored for preview; vision presentation is skipped."
1587
+ }
1588
+ };
1589
+ }
1590
+ /** Registers built-in workspace file tools (paths may use in-memory:// URI form). */
1591
+ function registerWorkspaceTools(broker) {
1592
+ broker.register({
1593
+ name: "workspace_list",
1594
+ 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.",
1595
+ parameters: {
1596
+ type: "object",
1597
+ properties: { path: {
1598
+ type: "string",
1599
+ description: "Directory path under a connected root (empty = list roots). in-memory://… URIs are accepted."
1600
+ } },
1601
+ required: []
1602
+ }
1603
+ }, async (call) => {
1604
+ try {
1605
+ const path = toWorkspaceToolPath(String(call.arguments.path ?? ""));
1606
+ const { resource } = await resolveResource(path, { directory: true });
1607
+ if (!(resource instanceof Directory)) return {
1608
+ id: call.id,
1609
+ name: call.name,
1610
+ ok: false,
1611
+ output: null,
1612
+ error: isInMemoryToolPath(path) ? "Not a directory" : "No workspace / not a directory"
1613
+ };
1614
+ const children = await resource.listChildren(true);
1615
+ return {
1616
+ id: call.id,
1617
+ name: call.name,
1618
+ ok: true,
1619
+ output: children.map((c) => ({
1620
+ name: c.getName(),
1621
+ path: c.getWorkspacePath(),
1622
+ kind: c instanceof Directory ? "directory" : "file"
1623
+ }))
1624
+ };
1625
+ } catch (err) {
1626
+ return {
1627
+ id: call.id,
1628
+ name: call.name,
1629
+ ok: false,
1630
+ output: null,
1631
+ error: err instanceof Error ? err.message : String(err)
1632
+ };
1633
+ }
1634
+ });
1635
+ broker.register({
1636
+ name: "workspace_read",
1637
+ 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.",
1638
+ parameters: {
1639
+ type: "object",
1640
+ properties: {
1641
+ path: {
1642
+ type: "string",
1643
+ description: "File path under a connected root (e.g. tests/readme.md)."
1644
+ },
1645
+ offset: {
1646
+ type: "number",
1647
+ description: "Text start offset"
1648
+ },
1649
+ limit: {
1650
+ type: "number",
1651
+ description: "Max text characters to return"
1652
+ }
1653
+ },
1654
+ required: ["path"]
1655
+ }
1656
+ }, async (call) => {
1657
+ try {
1658
+ const qualified = await qualifyWorkspaceToolPath(String(call.arguments.path ?? ""), "read");
1659
+ if (!qualified.ok) return {
1660
+ id: call.id,
1661
+ name: call.name,
1662
+ ok: false,
1663
+ output: null,
1664
+ error: qualified.error
1665
+ };
1666
+ const path = qualified.path;
1667
+ const { resource } = await resolveResource(path);
1668
+ if (!(resource instanceof File)) return {
1669
+ id: call.id,
1670
+ name: call.name,
1671
+ ok: false,
1672
+ output: null,
1673
+ error: isInMemoryToolPath(path) ? "Not a file" : "No workspace / not a file"
1674
+ };
1675
+ const contents = await resource.getContents();
1676
+ const text = typeof contents === "string" ? contents : String(contents ?? "");
1677
+ const vision = visionToolOutput(path, text, optionalMimeType(resource));
1678
+ if (vision) return {
1679
+ id: call.id,
1680
+ name: call.name,
1681
+ ok: true,
1682
+ output: vision
1683
+ };
1684
+ const offset = typeof call.arguments.offset === "number" ? Math.max(0, call.arguments.offset) : 0;
1685
+ const limit = typeof call.arguments.limit === "number" ? Math.max(1, call.arguments.limit) : void 0;
1686
+ if (offset || limit != null) {
1687
+ const slice = text.slice(offset, limit != null ? offset + limit : void 0);
1688
+ return {
1689
+ id: call.id,
1690
+ name: call.name,
1691
+ ok: true,
1692
+ output: {
1693
+ path,
1694
+ offset,
1695
+ length: slice.length,
1696
+ totalLength: text.length,
1697
+ truncated: offset + slice.length < text.length,
1698
+ content: slice
1699
+ }
1700
+ };
1701
+ }
1702
+ return {
1703
+ id: call.id,
1704
+ name: call.name,
1705
+ ok: true,
1706
+ output: text
1707
+ };
1708
+ } catch (err) {
1709
+ return {
1710
+ id: call.id,
1711
+ name: call.name,
1712
+ ok: false,
1713
+ output: null,
1714
+ error: err instanceof Error ? err.message : String(err)
1715
+ };
1716
+ }
1717
+ });
1718
+ broker.register({
1719
+ name: "workspace_write",
1720
+ 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.",
1721
+ parameters: {
1722
+ type: "object",
1723
+ properties: {
1724
+ path: {
1725
+ type: "string",
1726
+ description: "File path under a connected root (e.g. tests/demo.canvas.js)."
1727
+ },
1728
+ contents: { type: "string" }
1729
+ },
1730
+ required: ["path", "contents"]
1731
+ }
1732
+ }, async (call) => {
1733
+ try {
1734
+ const args = asRecord$1(call.arguments);
1735
+ const qualified = await qualifyWorkspaceToolPath(String(args.path ?? ""), "write");
1736
+ if (!qualified.ok) return {
1737
+ id: call.id,
1738
+ name: call.name,
1739
+ ok: false,
1740
+ output: null,
1741
+ error: qualified.error,
1742
+ mutating: true
1743
+ };
1744
+ const path = qualified.path;
1745
+ const contents = String(args.contents ?? "");
1746
+ const { resource } = await resolveResource(path, { create: true });
1747
+ if (!(resource instanceof File)) return {
1748
+ id: call.id,
1749
+ name: call.name,
1750
+ ok: false,
1751
+ output: null,
1752
+ error: isInMemoryToolPath(path) ? "Not a file" : "No workspace / not a file",
1753
+ mutating: true
1754
+ };
1755
+ await resource.saveContents(contents);
1756
+ return {
1757
+ id: call.id,
1758
+ name: call.name,
1759
+ ok: true,
1760
+ output: {
1761
+ path,
1762
+ ...qualified.resolvedFrom ? { resolvedFrom: qualified.resolvedFrom } : {}
1763
+ },
1764
+ mutating: true
1765
+ };
1766
+ } catch (err) {
1767
+ return {
1768
+ id: call.id,
1769
+ name: call.name,
1770
+ ok: false,
1771
+ output: null,
1772
+ error: err instanceof Error ? err.message : String(err),
1773
+ mutating: true
1774
+ };
1775
+ }
1776
+ });
1612
1777
  }
1613
1778
  //#endregion
1614
- //#region src/runtime/tools/attachment-adapter.ts
1615
- /** Registers read-only session attachment tools. */
1616
- function registerAttachmentTools(broker, lookup) {
1617
- 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.",
1620
- parameters: {
1621
- type: "object",
1622
- properties: {}
1623
- }
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
- });
1779
+ //#region src/runtime/tools/fetch-adapter.ts
1780
+ var DEFAULT_MAX_BYTES = 512e3;
1781
+ var DEFAULT_MAX_CHARS = 12e4;
1782
+ var DEFAULT_TIMEOUT_MS = 2e4;
1783
+ function asRecord(value) {
1784
+ return value && typeof value === "object" ? value : {};
1785
+ }
1786
+ function parseUrl(raw) {
1787
+ const trimmed = raw.trim();
1788
+ if (!trimmed) return { error: "Missing url" };
1789
+ let parsed;
1790
+ try {
1791
+ parsed = new URL(trimmed);
1792
+ } catch {
1793
+ return { error: `Invalid url: ${trimmed}` };
1794
+ }
1795
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return { error: `Only http(s) URLs are allowed (got ${parsed.protocol})` };
1796
+ return parsed;
1797
+ }
1798
+ function isTextualContentType(contentType) {
1799
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1800
+ if (!type) return true;
1801
+ if (type.startsWith("text/")) return true;
1802
+ return type.includes("json") || type.includes("xml") || type.includes("javascript") || type.includes("svg") || type.includes("markdown") || type.includes("yaml") || type.includes("csv");
1803
+ }
1804
+ /** Registers a read-only HTTP(S) fetch tool for remote documentation and text content. */
1805
+ function registerFetchTools(broker) {
1636
1806
  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.",
1807
+ name: "fetch",
1808
+ description: "Fetch a remote http(s) URL and return text content (HTML, Markdown, JSON, etc.). Use for docs and references (e.g. Web Awesome agent skills on jsDelivr). Binary responses return metadata only.",
1639
1809
  parameters: {
1640
1810
  type: "object",
1641
1811
  properties: {
1642
- id: {
1812
+ url: {
1643
1813
  type: "string",
1644
- description: "Attachment id from attachment_list or the user message"
1645
- },
1646
- offset: {
1647
- type: "number",
1648
- description: "Text start offset (characters)"
1814
+ description: "Absolute http(s) URL to fetch"
1649
1815
  },
1650
- limit: {
1816
+ max_chars: {
1651
1817
  type: "number",
1652
- description: "Max text characters to return"
1818
+ description: `Max characters of body text to return (default ${DEFAULT_MAX_CHARS})`
1653
1819
  }
1654
1820
  },
1655
- required: ["id"]
1821
+ required: ["url"]
1656
1822
  }
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 {
1823
+ }, async (call) => {
1824
+ const args = asRecord(call.arguments);
1825
+ const parsed = parseUrl(String(args.url ?? ""));
1826
+ if ("error" in parsed) return {
1668
1827
  id: call.id,
1669
1828
  name: call.name,
1670
1829
  ok: false,
1671
1830
  output: null,
1672
- error: `Unknown attachment: ${id}`
1831
+ error: parsed.error
1673
1832
  };
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
1833
+ const maxCharsRaw = Number(args.max_chars);
1834
+ const maxChars = Number.isFinite(maxCharsRaw) && maxCharsRaw > 0 ? Math.min(Math.floor(maxCharsRaw), DEFAULT_MAX_CHARS * 2) : DEFAULT_MAX_CHARS;
1835
+ const controller = new AbortController();
1836
+ const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
1837
+ try {
1838
+ const response = await fetch(parsed.href, {
1839
+ method: "GET",
1840
+ redirect: "follow",
1841
+ signal: controller.signal,
1842
+ headers: { Accept: "text/*, application/json, application/xml, */*;q=0.1" }
1678
1843
  });
1679
- if (!result.ok) return {
1844
+ const contentType = response.headers.get("content-type") ?? "";
1845
+ const buffer = new Uint8Array(await response.arrayBuffer());
1846
+ const truncatedBytes = buffer.byteLength > DEFAULT_MAX_BYTES;
1847
+ const slice = truncatedBytes ? buffer.subarray(0, DEFAULT_MAX_BYTES) : buffer;
1848
+ if (!isTextualContentType(contentType) && !looksLikeUtf8Text(slice)) return {
1680
1849
  id: call.id,
1681
1850
  name: call.name,
1682
- ok: false,
1683
- output: null,
1684
- error: result.error
1851
+ ok: response.ok,
1852
+ output: {
1853
+ url: response.url || parsed.href,
1854
+ status: response.status,
1855
+ contentType,
1856
+ byteLength: buffer.byteLength,
1857
+ binary: true,
1858
+ note: "Response is not text; body omitted. Prefer documentation or raw text URLs."
1859
+ },
1860
+ error: response.ok ? void 0 : `HTTP ${response.status}`
1685
1861
  };
1862
+ const decoded = new TextDecoder("utf-8", { fatal: false }).decode(slice);
1863
+ const truncatedChars = decoded.length > maxChars;
1864
+ const body = truncatedChars ? decoded.slice(0, maxChars) : decoded;
1686
1865
  return {
1687
1866
  id: call.id,
1688
1867
  name: call.name,
1689
- ok: true,
1690
- output: result.output
1868
+ ok: response.ok,
1869
+ output: {
1870
+ url: response.url || parsed.href,
1871
+ status: response.status,
1872
+ contentType,
1873
+ byteLength: buffer.byteLength,
1874
+ truncated: truncatedBytes || truncatedChars,
1875
+ body
1876
+ },
1877
+ error: response.ok ? void 0 : `HTTP ${response.status}`
1878
+ };
1879
+ } catch (err) {
1880
+ const message = err instanceof Error && err.name === "AbortError" ? `Timed out after ${DEFAULT_TIMEOUT_MS}ms` : err instanceof Error ? err.message : String(err);
1881
+ return {
1882
+ id: call.id,
1883
+ name: call.name,
1884
+ ok: false,
1885
+ output: null,
1886
+ error: message
1691
1887
  };
1888
+ } finally {
1889
+ clearTimeout(timer);
1692
1890
  }
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
1891
  });
1711
1892
  }
1893
+ function looksLikeUtf8Text(bytes) {
1894
+ if (!bytes.length) return true;
1895
+ const sample = bytes.subarray(0, Math.min(bytes.length, 512));
1896
+ let odd = 0;
1897
+ for (const b of sample) {
1898
+ if (b === 0) return false;
1899
+ if (b < 7 || b > 13 && b < 32) odd += 1;
1900
+ }
1901
+ return odd / sample.length < .1;
1902
+ }
1903
+ //#endregion
1904
+ //#region src/runtime/tools/command-adapter.ts
1905
+ /** Commands that must never be exposed as LLM tools (pickers, storage boots, settings). */
1906
+ var SKIP = /* @__PURE__ */ new Set([
1907
+ "open_cognitive_ai_settings",
1908
+ "open_ai_config",
1909
+ "load_workspace",
1910
+ "connect_opfs",
1911
+ "connect_indexeddb",
1912
+ "disconnect_workspace",
1913
+ "disconnect_folder",
1914
+ "open_editor",
1915
+ "list_extensions",
1916
+ "enable_extension",
1917
+ "disable_extension"
1918
+ ]);
1919
+ var SKIP_PREFIXES = [
1920
+ "connect_",
1921
+ "disconnect_",
1922
+ "install_",
1923
+ "uninstall_"
1924
+ ];
1925
+ /** Built-in tools that must survive command refresh (not command-backed). */
1926
+ var KEEP_TOOLS = /* @__PURE__ */ new Set([
1927
+ "workspace_list",
1928
+ "workspace_read",
1929
+ "workspace_write",
1930
+ "fetch"
1931
+ ]);
1932
+ function sanitizeName(id) {
1933
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
1934
+ }
1935
+ function shouldSkipCommand(id) {
1936
+ if (SKIP.has(id)) return true;
1937
+ return SKIP_PREFIXES.some((prefix) => id.startsWith(prefix));
1938
+ }
1939
+ /** Registers Docks commands as first-class LLM tools (tool name = command id). */
1940
+ function refreshCommandTools(broker, role) {
1941
+ for (const def of broker.listDefinitions("build")) if (!KEEP_TOOLS.has(def.name)) broker.unregister(def.name);
1942
+ const commands = commandRegistry.listCommands(commandRegistry.createExecutionContext());
1943
+ const entries = Object.values(commands);
1944
+ for (const command of entries) {
1945
+ if (!command?.id || shouldSkipCommand(command.id)) continue;
1946
+ if (!command.description) continue;
1947
+ if ((role === "plan" || role === "explore") && /delete|write|save|rm|mv/i.test(command.id)) continue;
1948
+ const toolName = sanitizeName(command.id);
1949
+ if (KEEP_TOOLS.has(toolName)) continue;
1950
+ const properties = {};
1951
+ const required = [];
1952
+ for (const p of command.parameters ?? []) {
1953
+ properties[p.name] = {
1954
+ type: p.type ?? "string",
1955
+ description: p.description,
1956
+ ...p.allowedValues ? { enum: p.allowedValues } : {}
1957
+ };
1958
+ if (p.required) required.push(p.name);
1959
+ }
1960
+ broker.register({
1961
+ name: toolName,
1962
+ description: `${command.name}: ${command.description}`,
1963
+ parameters: {
1964
+ type: "object",
1965
+ properties,
1966
+ required
1967
+ }
1968
+ }, async (call, _sessionId) => {
1969
+ try {
1970
+ const result = await commandRegistry.execute(command.id, { params: call.arguments });
1971
+ return {
1972
+ id: call.id,
1973
+ name: call.name,
1974
+ ok: true,
1975
+ output: result
1976
+ };
1977
+ } catch (err) {
1978
+ return {
1979
+ id: call.id,
1980
+ name: call.name,
1981
+ ok: false,
1982
+ output: null,
1983
+ error: err instanceof Error ? err.message : String(err)
1984
+ };
1985
+ }
1986
+ });
1987
+ }
1988
+ }
1712
1989
  //#endregion
1713
1990
  //#region src/runtime/compact/compaction.ts
1714
1991
  var COMPACT_SYSTEM = `You compress older chat turns into a durable summary for future assistant turns.
@@ -2026,7 +2303,7 @@ var SessionRuntime = class {
2026
2303
  //#region src/runtime/specialists/classify-risk.ts
2027
2304
  var CLASSIFY_SYSTEM = `You classify tool calls for user approval.
2028
2305
  Mark risk "critical" if the call can change state: write/edit/delete files, connect or load a workspace, run shell/commands with side effects, install, deploy, publish, push, or otherwise mutate the environment.
2029
- Mark risk "safe" only for read-only inspection (list, read, search, describe).
2306
+ Mark risk "safe" only for read-only inspection (list, read, search, describe, fetch).
2030
2307
  When unsure, choose "critical".
2031
2308
  Reply with ONLY a JSON array: [{"id":"<tool call id>","risk":"safe"|"critical"}]`;
2032
2309
  /** Ask the model whether pending tool calls are safe or critical. Unknown → critical. */
@@ -2163,7 +2440,25 @@ async function runSpecialistLoop(input) {
2163
2440
  };
2164
2441
  const messages = historyToProviderMessages(input.systemPrompt, input.history, input.compactionSummary);
2165
2442
  let success = true;
2443
+ let cancelled = false;
2444
+ const usage = {
2445
+ inputTokens: 0,
2446
+ outputTokens: 0
2447
+ };
2448
+ const markCancelled = () => {
2449
+ if (cancelled) return;
2450
+ cancelled = true;
2451
+ success = false;
2452
+ if (!parts.some((p) => p.type === "text")) emit({
2453
+ type: "text",
2454
+ text: "Cancelled."
2455
+ });
2456
+ };
2166
2457
  for (let iter = 0; iter < MAX_TOOL_ITERATIONS; iter++) {
2458
+ if (isAborted(input.signal)) {
2459
+ markCancelled();
2460
+ break;
2461
+ }
2167
2462
  input.refreshTools?.();
2168
2463
  const tools = input.broker.listDefinitions(input.role);
2169
2464
  let pendingTools = [];
@@ -2175,6 +2470,7 @@ async function runSpecialistLoop(input) {
2175
2470
  providerOptions: input.resolved.providerOptions,
2176
2471
  signal: input.signal
2177
2472
  })) {
2473
+ if (isAborted(input.signal)) break;
2178
2474
  if (chunk.type === "error") {
2179
2475
  emit({
2180
2476
  type: "text",
@@ -2183,6 +2479,10 @@ async function runSpecialistLoop(input) {
2183
2479
  success = false;
2184
2480
  break;
2185
2481
  }
2482
+ if (chunk.type === "usage" && chunk.usage) {
2483
+ usage.inputTokens += chunk.usage.inputTokens;
2484
+ usage.outputTokens += chunk.usage.outputTokens;
2485
+ }
2186
2486
  if (chunk.type === "reasoning" && chunk.text) emit({
2187
2487
  type: "reasoning",
2188
2488
  text: chunk.text,
@@ -2203,6 +2503,10 @@ async function runSpecialistLoop(input) {
2203
2503
  } else pendingTools.push({ ...chunk.toolCall });
2204
2504
  }
2205
2505
  }
2506
+ if (isAborted(input.signal)) {
2507
+ markCancelled();
2508
+ break;
2509
+ }
2206
2510
  if (!pendingTools.length) {
2207
2511
  if (assistantText && !parts.some((p) => p.type === "text")) emit({
2208
2512
  type: "text",
@@ -2229,7 +2533,12 @@ async function runSpecialistLoop(input) {
2229
2533
  })),
2230
2534
  signal: input.signal
2231
2535
  }) : null;
2536
+ const pendingImages = [];
2232
2537
  for (const tool of pendingTools) {
2538
+ if (isAborted(input.signal)) {
2539
+ markCancelled();
2540
+ break;
2541
+ }
2233
2542
  let args = {};
2234
2543
  try {
2235
2544
  args = tool.arguments ? JSON.parse(tool.arguments) : {};
@@ -2267,26 +2576,30 @@ async function runSpecialistLoop(input) {
2267
2576
  name: tool.name,
2268
2577
  content: JSON.stringify(presentImage ? {
2269
2578
  ...safeOutput ?? {},
2270
- note: "Image bytes follow in the next user message for vision inspection."
2579
+ note: "Image bytes follow after all tool results for vision inspection."
2271
2580
  } : safeOutput)
2272
2581
  });
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
- }
2582
+ if (presentImage) pendingImages.push({
2583
+ id: String(rawOutput.id ?? ""),
2584
+ name: String(rawOutput.name ?? "image"),
2585
+ dataUrl: String(rawOutput.dataUrl)
2586
+ });
2288
2587
  if (!result.ok) success = false;
2289
2588
  }
2589
+ if (cancelled || isAborted(input.signal)) {
2590
+ markCancelled();
2591
+ break;
2592
+ }
2593
+ if (pendingImages.length) messages.push({
2594
+ role: "user",
2595
+ content: [{
2596
+ type: "text",
2597
+ 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")}`
2598
+ }, ...pendingImages.map((img) => ({
2599
+ type: "image_url",
2600
+ image_url: { url: img.dataUrl }
2601
+ }))]
2602
+ });
2290
2603
  }
2291
2604
  return {
2292
2605
  message: {
@@ -2295,7 +2608,8 @@ async function runSpecialistLoop(input) {
2295
2608
  parts: coalesceParts(parts),
2296
2609
  createdAt: Date.now(),
2297
2610
  routedModel: input.resolved.model,
2298
- effort: input.resolved.effort
2611
+ effort: input.resolved.effort,
2612
+ ...usage.inputTokens > 0 || usage.outputTokens > 0 ? { usage } : {}
2299
2613
  },
2300
2614
  success
2301
2615
  };
@@ -2325,7 +2639,14 @@ function coalesceParts(parts) {
2325
2639
  //#endregion
2326
2640
  //#region src/runtime/specialists/index.ts
2327
2641
  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."}`;
2642
+ 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 ? [
2643
+ "This turn publishes session attachments under the shared in-memory root (in-memory/<sessionId>/… or in-memory://<sessionId>/…).",
2644
+ "Read them with workspace_read / workspace_list — in-memory paths work without mounting that root into the workspace tree.",
2645
+ "Do not call load_workspace / connect_opfs / connect_indexeddb to find attachments.",
2646
+ "Canvas from a screenshot: skills.load eclipse-docks-canvas → workspace_read the attachment path → workspace_write a .canvas.js (into a durable workspace folder) → canvas.open / canvas.apply_source.",
2647
+ "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.",
2648
+ "If no durable workspace folder is connected for writing canvases, say so and stop."
2649
+ ].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). For specialized workflows (e.g. canvas.js), call skills.list to discover registered skills, then skills.load with the matching name before following those instructions.`;
2329
2650
  switch (role) {
2330
2651
  case "plan": return `${preamble}\nMode: PLAN (read-only). Analyze and propose a plan for: ${goal}`;
2331
2652
  case "explore": return `${preamble}\nMode: EXPLORE (read-only). Search and report findings for: ${goal}`;
@@ -2417,6 +2738,13 @@ function estimateSessionContextUsage(session, modelRef) {
2417
2738
  }
2418
2739
  const used = buckets.reduce((sum, b) => sum + b.tokens, 0);
2419
2740
  const percent = limit > 0 ? Math.min(100, used / limit * 100) : 0;
2741
+ let inputTokens = 0;
2742
+ let outputTokens = 0;
2743
+ for (const message of session?.history ?? []) {
2744
+ if (!message.usage) continue;
2745
+ inputTokens += message.usage.inputTokens;
2746
+ outputTokens += message.usage.outputTokens;
2747
+ }
2420
2748
  return {
2421
2749
  used,
2422
2750
  limit,
@@ -2424,7 +2752,9 @@ function estimateSessionContextUsage(session, modelRef) {
2424
2752
  remaining: Math.max(0, limit - used),
2425
2753
  level: contextUsageLevel(percent),
2426
2754
  buckets: buckets.filter((b) => b.tokens > 0),
2427
- modelRef
2755
+ modelRef,
2756
+ inputTokens,
2757
+ outputTokens
2428
2758
  };
2429
2759
  }
2430
2760
  function formatTokenCount(n) {
@@ -2453,6 +2783,184 @@ function formatBucketDetail(bucket) {
2453
2783
  return `${bucket.count} · ${tokens}`;
2454
2784
  }
2455
2785
  //#endregion
2786
+ //#region src/runtime/session-workspace.ts
2787
+ function attachmentContents(stored) {
2788
+ if (stored.kind === "image") {
2789
+ if (stored.dataUrl) return stored.dataUrl;
2790
+ if (stored.text) return stored.text;
2791
+ }
2792
+ return stored.text ?? "";
2793
+ }
2794
+ /** Build live-file callbacks that always re-read via the caller-provided resolver. */
2795
+ function attachmentLiveContents(resolve) {
2796
+ return {
2797
+ get mimeType() {
2798
+ return resolve()?.mime;
2799
+ },
2800
+ getContents() {
2801
+ const stored = resolve();
2802
+ if (!stored) throw new Error("Attachment payload is no longer available");
2803
+ return attachmentContents(stored);
2804
+ },
2805
+ size() {
2806
+ const stored = resolve();
2807
+ if (!stored) return null;
2808
+ if (stored.byteLength != null) return stored.byteLength;
2809
+ return attachmentContents(stored).length;
2810
+ }
2811
+ };
2812
+ }
2813
+ /** Relative path under the shared in-memory root (`<sessionId>/<file>`). */
2814
+ function relativeAttachmentPath(sessionId, stored) {
2815
+ return attachmentWorkspacePath(sessionId, stored).slice(`${IN_MEMORY_ROOT_NAME}/`.length);
2816
+ }
2817
+ /**
2818
+ * Publish a live file node. Payload is resolved only through `resolve` — nothing is copied.
2819
+ */
2820
+ async function syncAttachmentToWorkspace(sessionId, stored, resolve) {
2821
+ const root = getInMemoryRoot();
2822
+ const relative = relativeAttachmentPath(sessionId, stored);
2823
+ root.writeLiveFile(relative, attachmentLiveContents(resolve));
2824
+ return attachmentWorkspacePath(sessionId, stored);
2825
+ }
2826
+ async function syncAttachmentsToWorkspace(sessionId, attachments, resolveById) {
2827
+ for (const attachment of attachments) await syncAttachmentToWorkspace(sessionId, attachment, () => resolveById(attachment.id));
2828
+ }
2829
+ async function removeAttachmentFromWorkspace(sessionId, stored) {
2830
+ const root = getInMemoryRoot();
2831
+ const relative = relativeAttachmentPath(sessionId, stored);
2832
+ const resource = await root.getResource(relative);
2833
+ if (resource) await resource.delete();
2834
+ }
2835
+ async function removeSessionWorkspaceFolder(sessionId) {
2836
+ const dir = await getInMemoryRoot().getResource(sessionId);
2837
+ if (dir instanceof Directory) await dir.delete();
2838
+ }
2839
+ /**
2840
+ * Restrict the workspace-mounted in-memory view to a single folder name (active session).
2841
+ * Pass `null` to show the full tree again.
2842
+ */
2843
+ function projectInMemoryMountToFolder(folderName) {
2844
+ if (!folderName) {
2845
+ setInMemoryMountProjection(null);
2846
+ return;
2847
+ }
2848
+ const name = folderName;
2849
+ setInMemoryMountProjection({
2850
+ async listChildren(root) {
2851
+ return [root.ensureDirectoryEntry(name)];
2852
+ },
2853
+ async getResource(root, path, options) {
2854
+ const segments = path.split("/").filter((s) => s.trim());
2855
+ if (segments[0] !== name) return null;
2856
+ if (segments.length === 1 && (path.endsWith("/") || path === name)) return root.ensureDirectoryEntry(name);
2857
+ return root.getResource(path, options);
2858
+ }
2859
+ });
2860
+ }
2861
+ //#endregion
2862
+ //#region src/runtime/skills-parse.ts
2863
+ /**
2864
+ * Parse a SKILL.md document (YAML frontmatter + markdown body).
2865
+ * Compatible with Cursor Agent Skills layout.
2866
+ */
2867
+ function parseSkillMarkdown(raw) {
2868
+ const text = raw.replace(/^\uFEFF/, "");
2869
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
2870
+ if (!match) return {
2871
+ name: "",
2872
+ description: "",
2873
+ body: text.trim()
2874
+ };
2875
+ const front = match[1] ?? "";
2876
+ const body = (match[2] ?? "").trim();
2877
+ return {
2878
+ name: readFrontmatterString(front, "name"),
2879
+ description: readFrontmatterString(front, "description"),
2880
+ body
2881
+ };
2882
+ }
2883
+ /** Short ambient prompt block (uses skill description). */
2884
+ function formatSkillForPrompt(skill) {
2885
+ const description = skill.description.trim();
2886
+ if (!description) return skill.body.trim();
2887
+ return description;
2888
+ }
2889
+ function readFrontmatterString(front, key) {
2890
+ const folded = front.match(new RegExp(`^${key}:\\s*>-\\s*\\r?\\n((?:[ \\t]+.*\\r?\\n?)*)`, "m"));
2891
+ if (folded?.[1]) return folded[1].split(/\r?\n/).map((line) => line.replace(/^[ \t]+/, "").trimEnd()).filter((line, index, all) => line.length > 0 || index < all.length - 1).join(" ").replace(/\s+/g, " ").trim();
2892
+ const quoted = front.match(new RegExp(`^${key}:\\s*[\"']([\\s\\S]*?)[\"']\\s*$`, "m"));
2893
+ if (quoted?.[1]) return quoted[1].replace(/\s+/g, " ").trim();
2894
+ return front.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim() ?? "";
2895
+ }
2896
+ //#endregion
2897
+ //#region src/runtime/skills-types.ts
2898
+ function isSkillContribution(value) {
2899
+ if (!value || typeof value !== "object") return false;
2900
+ const skill = value;
2901
+ const hasBody = typeof skill.body === "string";
2902
+ const hasLoad = typeof skill.load === "function";
2903
+ return typeof skill.name === "string" && skill.name.length > 0 && typeof skill.label === "string" && (hasBody || hasLoad);
2904
+ }
2905
+ //#endregion
2906
+ //#region src/runtime/skills-resolve.ts
2907
+ function rawFromLoadResult(result) {
2908
+ if (typeof result === "string") return result;
2909
+ return typeof result?.default === "string" ? result.default : "";
2910
+ }
2911
+ async function readSkillRaw(skill) {
2912
+ if (typeof skill.body === "string" && skill.body.trim()) return skill.body.replace(/^\uFEFF/, "");
2913
+ if (!skill.load) return "";
2914
+ return rawFromLoadResult(await skill.load()).replace(/^\uFEFF/, "");
2915
+ }
2916
+ /**
2917
+ * Resolve SKILL.md (eager `body` or lazy `load`) into name/description/body.
2918
+ * Frontmatter wins for description unless the contribution sets an explicit override.
2919
+ */
2920
+ async function resolveSkill(skill) {
2921
+ const raw = (await readSkillRaw(skill)).trim();
2922
+ const parsed = raw ? parseSkillMarkdown(raw) : {
2923
+ name: "",
2924
+ description: "",
2925
+ body: ""
2926
+ };
2927
+ return {
2928
+ name: skill.name || parsed.name,
2929
+ label: skill.label || parsed.name || skill.name,
2930
+ description: (skill.description ?? parsed.description).trim(),
2931
+ body: parsed.body.trim() || (raw && !parsed.description && !parsed.name ? raw : "")
2932
+ };
2933
+ }
2934
+ /** @deprecated Prefer {@link resolveSkill}; kept for callers that only need the body. */
2935
+ async function resolveSkillBody(skill) {
2936
+ return (await resolveSkill(skill)).body;
2937
+ }
2938
+ //#endregion
2939
+ //#region src/runtime/skills.ts
2940
+ /** Contribution slot for AI agent skills (Cursor-compatible SKILL.md content). */
2941
+ var SYSTEM_SKILLS = "system.skills";
2942
+ function listSkillContributions() {
2943
+ return contributionRegistry.getContributions(SYSTEM_SKILLS).filter(isSkillContribution);
2944
+ }
2945
+ /** Register a SKILL.md document on {@link SYSTEM_SKILLS}. Returns an unregister function. */
2946
+ function registerSkillMarkdown(raw, opts) {
2947
+ const parsed = parseSkillMarkdown(raw);
2948
+ const name = (opts?.name || parsed.name || `skill-${Date.now()}`).trim();
2949
+ const contribution = {
2950
+ name,
2951
+ label: opts?.label || parsed.name || name,
2952
+ body: (opts?.body || raw).trim(),
2953
+ ranking: opts?.ranking,
2954
+ icon: opts?.icon,
2955
+ relevant: opts?.relevant,
2956
+ load: opts?.load,
2957
+ description: opts?.description
2958
+ };
2959
+ if (!contribution.body && !contribution.load) return () => void 0;
2960
+ contributionRegistry.registerContribution(SYSTEM_SKILLS, contribution);
2961
+ return () => contributionRegistry.unregisterContribution(SYSTEM_SKILLS, name);
2962
+ }
2963
+ //#endregion
2456
2964
  //#region src/runtime/cognitive-runtime.ts
2457
2965
  var PERSIST_KEY = "cognitive/v1/state";
2458
2966
  /** Auto-compact after a turn when live history exceeds this many messages. */
@@ -2487,12 +2995,7 @@ var CognitiveRuntime = class {
2487
2995
  emit: false
2488
2996
  });
2489
2997
  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
- });
2998
+ registerFetchTools(this.tools);
2496
2999
  this.tools.setApprovalMode(this.approvalMode);
2497
3000
  this.tools.setPermissionHandler(async (req) => this.requestPermission(req));
2498
3001
  this.ready = this.hydrateAll();
@@ -2548,13 +3051,23 @@ var CognitiveRuntime = class {
2548
3051
  this.promptEnhancers = this.promptEnhancers.filter((e) => e !== enhancer);
2549
3052
  };
2550
3053
  }
3054
+ /**
3055
+ * Register a Cursor-compatible SKILL.md document on {@link SYSTEM_SKILLS}.
3056
+ * Relevant skills are selected into the specialist prompt each turn.
3057
+ */
3058
+ addSkillMarkdown(raw) {
3059
+ return registerSkillMarkdown(raw);
3060
+ }
2551
3061
  /** First catalog model for the default provider — used by Manual routing. */
2552
3062
  defaultModelId() {
2553
3063
  const catalog = catalogFromSettings(this.providersSettings);
2554
3064
  return catalog.find((e) => e.providerId === this.providersSettings.defaultProviderId)?.id ?? catalog[0]?.id ?? "openai/gpt-4o-mini";
2555
3065
  }
2556
3066
  setModelPolicy(policy) {
2557
- this.policy = policy;
3067
+ const next = normalizeModelPolicy(policy) ?? this.policy;
3068
+ if (sameModelPolicy(this.policy, next)) return;
3069
+ this.policy = next;
3070
+ this.persist();
2558
3071
  }
2559
3072
  getModelPolicy() {
2560
3073
  return this.policy;
@@ -2616,28 +3129,46 @@ var CognitiveRuntime = class {
2616
3129
  if (focused) return focused.id;
2617
3130
  return this.sessions.createSession().id;
2618
3131
  }
2619
- /** Store attachment payloads on the session (not inlined into history until prompt). */
2620
- addAttachments(sessionId, attachments) {
3132
+ /** Store attachment payloads on the session and mirror them into in-memory/<sessionId>/. */
3133
+ async addAttachments(sessionId, attachments) {
2621
3134
  const id = this.ensureSessionId(sessionId);
2622
3135
  const session = this.sessions.get(id);
2623
3136
  if (!session || !attachments.length) return [];
2624
3137
  const room = Math.max(0, 8 - (session.attachments?.length ?? 0));
2625
- const accepted = attachments.slice(0, room).map((attachment) => toSessionAttachment(attachment));
3138
+ const accepted = attachments.slice(0, room).map((attachment) => {
3139
+ const stored = toSessionAttachment(attachment);
3140
+ return {
3141
+ ...stored,
3142
+ path: attachmentUri(id, stored)
3143
+ };
3144
+ });
2626
3145
  if (!accepted.length) return [];
2627
3146
  this.sessions.update(id, { attachments: [...session.attachments ?? [], ...accepted] });
2628
3147
  this.commitSessions();
3148
+ await this.mirrorAttachments(id, accepted).catch(() => void 0);
2629
3149
  return accepted;
2630
3150
  }
2631
- /** Remove an attachment payload from the session store. */
3151
+ /** Remove an attachment payload from the session store and in-memory workspace. */
2632
3152
  removeAttachment(sessionId, attachmentId) {
2633
3153
  const session = this.sessions.get(sessionId);
2634
3154
  if (!session) return false;
3155
+ const removed = (session.attachments ?? []).find((a) => a.id === attachmentId);
2635
3156
  const next = (session.attachments ?? []).filter((a) => a.id !== attachmentId);
2636
3157
  if (next.length === (session.attachments?.length ?? 0)) return false;
2637
3158
  this.sessions.update(sessionId, { attachments: next });
2638
3159
  this.commitSessions();
3160
+ if (removed) removeAttachmentFromWorkspace(sessionId, removed);
2639
3161
  return true;
2640
3162
  }
3163
+ /** Drop a session and its in-memory attachment folder. */
3164
+ removeSession(sessionId) {
3165
+ const ok = this.sessions.remove(sessionId);
3166
+ if (ok) {
3167
+ removeSessionWorkspaceFolder(sessionId);
3168
+ this.commitSessions();
3169
+ }
3170
+ return ok;
3171
+ }
2641
3172
  /**
2642
3173
  * Compact a session: drop older turns, store a summary (LLM when possible),
2643
3174
  * and persist. The summary is injected into later model calls.
@@ -2681,8 +3212,17 @@ var CognitiveRuntime = class {
2681
3212
  const session = (input.sessionId ? this.sessions.get(input.sessionId) : this.sessions.focused()) ?? this.sessions.createSession();
2682
3213
  const text = input.text.trim();
2683
3214
  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] });
3215
+ const fresh = (input.attachments ?? []).slice(0, 8).map((attachment) => {
3216
+ const stored = toSessionAttachment(attachment, { messageId });
3217
+ return {
3218
+ ...stored,
3219
+ path: attachmentUri(session.id, stored)
3220
+ };
3221
+ });
3222
+ if (fresh.length) {
3223
+ this.sessions.update(session.id, { attachments: [...this.sessions.get(session.id)?.attachments ?? [], ...fresh] });
3224
+ await this.mirrorAttachments(session.id, fresh);
3225
+ }
2686
3226
  const existingIds = (input.attachmentIds ?? []).filter(Boolean);
2687
3227
  const stored = this.sessions.get(session.id)?.attachments ?? [];
2688
3228
  const byId = new Map(stored.map((a) => [a.id, a]));
@@ -2693,12 +3233,16 @@ var CognitiveRuntime = class {
2693
3233
  if (found && !published.some((p) => p.id === found.id)) published.push(found);
2694
3234
  }
2695
3235
  if (!text && !published.length) throw new Error("Prompt requires text or at least one attachment");
3236
+ if (published.length) await this.mirrorAttachments(session.id, published).catch(() => void 0);
2696
3237
  const parts = [];
2697
3238
  if (text) parts.push({
2698
3239
  type: "text",
2699
3240
  text
2700
3241
  });
2701
- for (const item of published) parts.push(toFileRefPart(item));
3242
+ for (const item of published) parts.push(toFileRefPart({
3243
+ ...item,
3244
+ path: item.path || attachmentUri(session.id, item)
3245
+ }));
2702
3246
  const promptSummary = summarizePrompt(text, published.map((a) => ({
2703
3247
  name: a.name,
2704
3248
  kind: a.kind
@@ -2736,6 +3280,7 @@ var CognitiveRuntime = class {
2736
3280
  buffers: tick.buffers,
2737
3281
  status: "running"
2738
3282
  });
3283
+ this.emitSessions();
2739
3284
  this.events.publish(TOPIC.coreTick, {
2740
3285
  valenceDelta: tick.valenceDelta,
2741
3286
  intent: tick.intent,
@@ -2802,7 +3347,7 @@ var CognitiveRuntime = class {
2802
3347
  sessionId: session.id,
2803
3348
  activation: .7
2804
3349
  });
2805
- if ((this.sessions.get(session.id)?.history.length ?? 0) > AUTO_COMPACT_AFTER) await this.compactSession(session.id, COMPACT_KEEP_COUNT, input.signal);
3350
+ 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
3351
  this.sessions.update(session.id, { status: "idle" });
2807
3352
  this.events.publish(TOPIC.messageDone, {
2808
3353
  sessionId: session.id,
@@ -2863,11 +3408,17 @@ var CognitiveRuntime = class {
2863
3408
  });
2864
3409
  }
2865
3410
  emitSessions() {
3411
+ const focusedId = this.sessions.focused()?.id ?? null;
3412
+ projectInMemoryMountToFolder(focusedId);
2866
3413
  this.events.publish(TOPIC.sessionChanged, {
2867
3414
  sessions: this.sessions.list(),
2868
- focusedId: this.sessions.focused()?.id ?? null
3415
+ focusedId
2869
3416
  });
2870
3417
  }
3418
+ /** Live in-memory file refs that resolve payloads from the session store. */
3419
+ mirrorAttachments(sessionId, attachments) {
3420
+ return syncAttachmentsToWorkspace(sessionId, attachments, (attachmentId) => this.sessions.get(sessionId)?.attachments?.find((a) => a.id === attachmentId));
3421
+ }
2871
3422
  async hydrateAll() {
2872
3423
  await loadModelsCatalog().catch(() => void 0);
2873
3424
  await this.hydrateProviders();
@@ -2879,6 +3430,16 @@ var CognitiveRuntime = class {
2879
3430
  if (!this.sessions.list().length) this.sessions.createSession({ title: "Chat" });
2880
3431
  const mostRecent = this.sessions.list()[0];
2881
3432
  if (mostRecent) this.sessions.focus(mostRecent.id);
3433
+ for (const session of this.sessions.list()) {
3434
+ const list = session.attachments ?? [];
3435
+ if (!list.length) continue;
3436
+ const stamped = list.map((a) => a.path ? a : {
3437
+ ...a,
3438
+ path: attachmentUri(session.id, a)
3439
+ });
3440
+ if (stamped.some((a, i) => a.path !== list[i]?.path)) this.sessions.update(session.id, { attachments: stamped });
3441
+ await this.mirrorAttachments(session.id, stamped).catch(() => void 0);
3442
+ }
2882
3443
  this.emitSessions();
2883
3444
  }
2884
3445
  async hydrateProviders() {
@@ -2906,6 +3467,8 @@ var CognitiveRuntime = class {
2906
3467
  this.approvalMode = saved.approvalMode;
2907
3468
  this.tools.setApprovalMode(saved.approvalMode);
2908
3469
  }
3470
+ const policy = normalizeModelPolicy(saved.modelPolicy);
3471
+ if (policy) this.policy = policy;
2909
3472
  } catch {}
2910
3473
  }
2911
3474
  async persist() {
@@ -2913,7 +3476,8 @@ var CognitiveRuntime = class {
2913
3476
  await persistenceService.persistObject(PERSIST_KEY, {
2914
3477
  sessions: this.sessions.snapshot(),
2915
3478
  memory: this.memory.snapshot(),
2916
- approvalMode: this.approvalMode
3479
+ approvalMode: this.approvalMode,
3480
+ modelPolicy: this.policy
2917
3481
  });
2918
3482
  } catch {}
2919
3483
  }
@@ -2921,6 +3485,32 @@ var CognitiveRuntime = class {
2921
3485
  function isToolApprovalMode(value) {
2922
3486
  return value === "manual" || value === "auto_safe" || value === "auto_all";
2923
3487
  }
3488
+ function isOptimizeFor(value) {
3489
+ return value === "cost" || value === "balance" || value === "intelligence";
3490
+ }
3491
+ function isEffortOrAuto(value) {
3492
+ return value === "auto" || value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
3493
+ }
3494
+ function normalizeModelPolicy(value) {
3495
+ if (!value || typeof value !== "object") return null;
3496
+ const raw = value;
3497
+ if (raw.mode === "auto" && isOptimizeFor(raw.optimizeFor)) return {
3498
+ mode: "auto",
3499
+ optimizeFor: raw.optimizeFor
3500
+ };
3501
+ if (raw.mode === "manual" && typeof raw.model === "string" && raw.model.trim()) return {
3502
+ mode: "manual",
3503
+ model: raw.model.trim(),
3504
+ ...isEffortOrAuto(raw.effort) ? { effort: raw.effort } : {}
3505
+ };
3506
+ return null;
3507
+ }
3508
+ function sameModelPolicy(a, b) {
3509
+ if (a.mode !== b.mode) return false;
3510
+ if (a.mode === "auto" && b.mode === "auto") return a.optimizeFor === b.optimizeFor;
3511
+ if (a.mode === "manual" && b.mode === "manual") return a.model === b.model && (a.effort ?? "auto") === (b.effort ?? "auto");
3512
+ return false;
3513
+ }
2924
3514
  function externalCatalogEntry(id, providerId) {
2925
3515
  return {
2926
3516
  id,
@@ -2947,6 +3537,57 @@ function summarizePrompt(text, attachments) {
2947
3537
  return `Attached ${attachments.map((a) => a.name).join(", ")}`;
2948
3538
  }
2949
3539
  //#endregion
2950
- 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 };
3540
+ //#region src/runtime/skills-commands.ts
3541
+ /** Register skills.list / skills.load as Docks commands (also exposed as LLM tools). */
3542
+ function registerSkillCommands() {
3543
+ registerAll({
3544
+ command: {
3545
+ id: "skills.list",
3546
+ name: "List skills",
3547
+ description: "List registered agent skills (name + description from SKILL.md). Call skills.load with a skill name to read the full instructions.",
3548
+ parameters: []
3549
+ },
3550
+ handler: { execute: async () => Promise.all(listSkillContributions().map(async (skill) => {
3551
+ const resolved = await resolveSkill(skill);
3552
+ return {
3553
+ name: resolved.name,
3554
+ label: resolved.label,
3555
+ description: resolved.description
3556
+ };
3557
+ })) }
3558
+ });
3559
+ registerAll({
3560
+ command: {
3561
+ id: "skills.load",
3562
+ name: "Load skill",
3563
+ description: "Load the full body of a registered agent skill by name. Prefer skills returned by skills.list.",
3564
+ parameters: [{
3565
+ name: "name",
3566
+ description: "Skill name (e.g. eclipse-docks-canvas)",
3567
+ required: true
3568
+ }]
3569
+ },
3570
+ handler: { execute: async (context) => {
3571
+ const name = String(context.params?.name ?? "").trim();
3572
+ if (!name) return {
3573
+ error: "Missing skill name",
3574
+ available: listSkillContributions().map((skill) => skill.name)
3575
+ };
3576
+ const skill = listSkillContributions().find((entry) => entry.name === name);
3577
+ if (!skill) return {
3578
+ error: `Unknown skill: ${name}`,
3579
+ available: listSkillContributions().map((entry) => entry.name)
3580
+ };
3581
+ const resolved = await resolveSkill(skill);
3582
+ if (!resolved.body) return {
3583
+ error: `Skill ${name} has no body`,
3584
+ name: resolved.name
3585
+ };
3586
+ return resolved;
3587
+ } }
3588
+ });
3589
+ }
3590
+ //#endregion
3591
+ export { detectEffortCapability as C, AI_VIEW_NAME as D, AI_SETTINGS_KEY as E, OPEN_AI_SETTINGS_COMMAND as O, parseModelIds as S, AI_SETTINGS_EDITOR_ID as T, readPromptAttachmentFromFile as _, listSkillContributions as a, defaultProvidersSettings as b, resolveSkillBody as c, parseSkillMarkdown as d, estimateSessionContextUsage as f, ATTACHMENT_FILE_ACCEPT as g, formatTokenCount as h, SYSTEM_SKILLS as i, TOPIC as k, isSkillContribution as l, formatContextPercent as m, CognitiveRuntime as n, registerSkillMarkdown as o, formatBucketDetail as p, cognitiveRuntime as r, resolveSkill as s, registerSkillCommands as t, formatSkillForPrompt as u, OpenAICompatibleProvider as v, loadModelsCatalog as w, listConfiguredModelIds as x, fetchOpenAICompatibleModels as y };
2951
3592
 
2952
- //# sourceMappingURL=runtime-DfNGzEjU.js.map
3593
+ //# sourceMappingURL=runtime-B0IHjzQO.js.map