@nextclaw/kernel 0.3.3 → 0.4.0-beta.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.
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { BUILTIN_PROVIDER_PLUGINS } from "@nextclaw/runtime";
15
15
  import { McpRegistryService, McpServerLifecycleManager } from "@nextclaw/mcp";
16
16
  import { McpNcpToolRegistryAdapter } from "@nextclaw/ncp-mcp";
17
17
  import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
18
+ import { createRuntimeChildEnv } from "@nextclaw/core/child-process-env";
18
19
  import { parse } from "yaml";
19
20
  import { HttpRuntimeConfigResolver, HttpRuntimeNcpAgentRuntime } from "@nextclaw/nextclaw-ncp-runtime-http-client";
20
21
  import { StdioRuntimeConfigResolver, StdioRuntimeNcpAgentRuntime, probeStdioRuntime } from "@nextclaw/nextclaw-ncp-runtime-stdio-client";
@@ -627,16 +628,6 @@ function readOptionalString$9(value) {
627
628
  if (typeof value !== "string") return;
628
629
  return value.trim() || void 0;
629
630
  }
630
- function toSessionMessageRequest(payload) {
631
- return {
632
- sessionId: payload.sessionId,
633
- message: {
634
- ...payload.message,
635
- sessionId: payload.sessionId
636
- },
637
- correlationId: payload.requestId
638
- };
639
- }
640
631
  function toRunHandle(accepted) {
641
632
  return {
642
633
  sessionId: accepted.sessionId,
@@ -684,7 +675,14 @@ var AgentRunRequestManager = class {
684
675
  };
685
676
  handleSessionMessageRequest = async (envelope) => {
686
677
  if (!envelope.payload) throw new Error("Invalid agent run session message request.");
687
- return toRunHandle(await this.send(toSessionMessageRequest(envelope.payload)));
678
+ return toRunHandle(await this.send({
679
+ sessionId: envelope.payload.sessionId,
680
+ message: {
681
+ ...envelope.payload.message,
682
+ sessionId: envelope.payload.sessionId
683
+ },
684
+ correlationId: envelope.payload.requestId
685
+ }));
688
686
  };
689
687
  send = async (request) => {
690
688
  const session = await this.sessionManager.getOrCreateAgentRunSession({
@@ -710,7 +708,26 @@ var AgentRunRequestManager = class {
710
708
  message
711
709
  };
712
710
  sessionRun.inbox.enqueue(message);
713
- const activeRun = sessionRun.beginRun();
711
+ let stopPublishingRunStatus = () => void 0;
712
+ stopPublishingRunStatus = sessionRun.onStatusChange((status) => {
713
+ this.eventBus.emit(eventKeys.sessionRunStatus, {
714
+ sessionKey: sessionRun.sessionId,
715
+ status
716
+ }, {
717
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
718
+ source: "agent-run-request"
719
+ });
720
+ if (status === "idle") stopPublishingRunStatus();
721
+ });
722
+ this.cleanups.push(stopPublishingRunStatus);
723
+ const activeRun = (() => {
724
+ try {
725
+ return sessionRun.beginRun();
726
+ } catch (error) {
727
+ stopPublishingRunStatus();
728
+ throw error;
729
+ }
730
+ })();
714
731
  const model = request.model ?? session.model ?? this.configManager.getDefaultModel();
715
732
  const agentId = request.agentId ?? session.agentId ?? resolveDefaultAgentProfileId(this.configManager.loadConfig());
716
733
  const spec = {
@@ -2511,11 +2528,11 @@ var ExtensionManager = class {
2511
2528
  //#endregion
2512
2529
  //#region src/utils/model-message-vision.utils.ts
2513
2530
  const IMAGE_OMITTED_TEXT = "[Image omitted: the selected model is not configured for vision input.]";
2514
- function isRecord$10(value) {
2531
+ function isRecord$11(value) {
2515
2532
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2516
2533
  }
2517
2534
  function isImageContentPart(value) {
2518
- if (!isRecord$10(value)) return false;
2535
+ if (!isRecord$11(value)) return false;
2519
2536
  const type = value.type;
2520
2537
  return type === "image_url" || type === "input_image";
2521
2538
  }
@@ -2531,7 +2548,7 @@ function normalizeContentWithoutVision(content) {
2531
2548
  };
2532
2549
  });
2533
2550
  if (!sawImage) return content;
2534
- const textParts = parts.filter((part) => isRecord$10(part) && part.type === "text" && typeof part.text === "string").map((part) => part.text.trim()).filter(Boolean);
2551
+ const textParts = parts.filter((part) => isRecord$11(part) && part.type === "text" && typeof part.text === "string").map((part) => part.text.trim()).filter(Boolean);
2535
2552
  if (textParts.length === parts.length) return textParts.join("\n\n");
2536
2553
  return parts;
2537
2554
  }
@@ -3070,7 +3087,7 @@ function safeNcpSessionFilename(value) {
3070
3087
  function normalizeNcpAgentId(agentId) {
3071
3088
  return agentId?.trim().toLowerCase() || void 0;
3072
3089
  }
3073
- function isRecord$9(value) {
3090
+ function isRecord$10(value) {
3074
3091
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3075
3092
  }
3076
3093
  function toIsoString(value, fallback) {
@@ -3690,7 +3707,7 @@ var PanelAppStateStore = class {
3690
3707
  };
3691
3708
  //#endregion
3692
3709
  //#region src/stores/panel-app-capability-grant.store.ts
3693
- const EMPTY_GRANTS$1 = {
3710
+ const EMPTY_GRANTS$2 = {
3694
3711
  version: 1,
3695
3712
  grants: {}
3696
3713
  };
@@ -3725,9 +3742,9 @@ var PanelAppCapabilityGrantStore = class {
3725
3742
  };
3726
3743
  load = async () => {
3727
3744
  try {
3728
- return normalizeStoreData$1(JSON.parse(await readFile(this.filePath, "utf8")));
3745
+ return normalizeStoreData$2(JSON.parse(await readFile(this.filePath, "utf8")));
3729
3746
  } catch (error) {
3730
- if (isMissingFileError$2(error)) return structuredClone(EMPTY_GRANTS$1);
3747
+ if (isMissingFileError$3(error)) return structuredClone(EMPTY_GRANTS$2);
3731
3748
  throw error;
3732
3749
  }
3733
3750
  };
@@ -3736,13 +3753,13 @@ var PanelAppCapabilityGrantStore = class {
3736
3753
  await writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
3737
3754
  };
3738
3755
  };
3739
- function normalizeStoreData$1(value) {
3740
- if (!isRecord$8(value) || value.version !== 1 || !isRecord$8(value.grants)) return structuredClone(EMPTY_GRANTS$1);
3756
+ function normalizeStoreData$2(value) {
3757
+ if (!isRecord$9(value) || value.version !== 1 || !isRecord$9(value.grants)) return structuredClone(EMPTY_GRANTS$2);
3741
3758
  const grants = {};
3742
3759
  for (const [callerKey, callerValue] of Object.entries(value.grants)) {
3743
- if (!isRecord$8(callerValue) || !isRecord$8(callerValue.capabilities)) continue;
3760
+ if (!isRecord$9(callerValue) || !isRecord$9(callerValue.capabilities)) continue;
3744
3761
  const capabilities = {};
3745
- for (const [capability, grantValue] of Object.entries(callerValue.capabilities)) if (isRecord$8(grantValue) && typeof grantValue.grantedAt === "string") capabilities[capability] = { grantedAt: grantValue.grantedAt };
3762
+ for (const [capability, grantValue] of Object.entries(callerValue.capabilities)) if (isRecord$9(grantValue) && typeof grantValue.grantedAt === "string") capabilities[capability] = { grantedAt: grantValue.grantedAt };
3746
3763
  grants[callerKey] = { capabilities };
3747
3764
  }
3748
3765
  return {
@@ -3753,11 +3770,65 @@ function normalizeStoreData$1(value) {
3753
3770
  function getCallerKey(caller) {
3754
3771
  return `${caller.surface}:${caller.appId}`;
3755
3772
  }
3773
+ function isRecord$9(value) {
3774
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3775
+ }
3776
+ function isMissingFileError$3(error) {
3777
+ return Boolean(error) && typeof error === "object" && error.code === "ENOENT";
3778
+ }
3779
+ //#endregion
3780
+ //#region src/stores/panel-app-client-grant.store.ts
3781
+ const EMPTY_GRANTS$1 = {
3782
+ grants: {},
3783
+ version: 1
3784
+ };
3785
+ var PanelAppClientGrantStore = class {
3786
+ constructor(filePath) {
3787
+ this.filePath = filePath;
3788
+ }
3789
+ isGranted = async (appId) => {
3790
+ const data = await this.load();
3791
+ return Boolean(data.grants[appId]);
3792
+ };
3793
+ grant = async (params) => {
3794
+ const data = await this.load();
3795
+ data.grants[params.appId] = { grantedAt: params.grantedAt };
3796
+ await this.save(data);
3797
+ return params;
3798
+ };
3799
+ revoke = async (appId) => {
3800
+ const data = await this.load();
3801
+ if (!data.grants[appId]) return;
3802
+ delete data.grants[appId];
3803
+ await this.save(data);
3804
+ };
3805
+ load = async () => {
3806
+ try {
3807
+ return normalizeStoreData$1(JSON.parse(await readFile(this.filePath, "utf8")));
3808
+ } catch (error) {
3809
+ if (isMissingFileError$2(error)) return structuredClone(EMPTY_GRANTS$1);
3810
+ throw error;
3811
+ }
3812
+ };
3813
+ save = async (data) => {
3814
+ await mkdir(dirname(this.filePath), { recursive: true });
3815
+ await writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
3816
+ };
3817
+ };
3818
+ function normalizeStoreData$1(value) {
3819
+ if (!isRecord$8(value) || value.version !== 1 || !isRecord$8(value.grants)) return structuredClone(EMPTY_GRANTS$1);
3820
+ const grants = {};
3821
+ for (const [appId, grant] of Object.entries(value.grants)) if (isRecord$8(grant) && typeof grant.grantedAt === "string") grants[appId] = { grantedAt: grant.grantedAt };
3822
+ return {
3823
+ grants,
3824
+ version: 1
3825
+ };
3826
+ }
3756
3827
  function isRecord$8(value) {
3757
3828
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3758
3829
  }
3759
3830
  function isMissingFileError$2(error) {
3760
- return Boolean(error) && typeof error === "object" && error.code === "ENOENT";
3831
+ return isRecord$8(error) && error.code === "ENOENT";
3761
3832
  }
3762
3833
  //#endregion
3763
3834
  //#region src/services/agent-run-client.service.ts
@@ -3953,162 +4024,465 @@ var AgentRunClient = class {
3953
4024
  };
3954
4025
  };
3955
4026
  //#endregion
3956
- //#region src/utils/panel-app-bridge.utils.ts
3957
- const PANEL_APP_BRIDGE_MARKER = "nextclaw:panel-app-service-actions:request";
3958
- function injectPanelAppBridgeScript(html) {
3959
- if (html.includes(PANEL_APP_BRIDGE_MARKER)) return html;
3960
- const script = `<script>${getPanelAppBridgeScript()}<\/script>`;
3961
- const headMatch = /<head(?:\s[^>]*)?>/i.exec(html);
3962
- if (headMatch?.index !== void 0) {
3963
- const insertAt = headMatch.index + headMatch[0].length;
3964
- return `${html.slice(0, insertAt)}${script}${html.slice(insertAt)}`;
4027
+ //#region src/tools/structured-result.tools.ts
4028
+ const STRUCTURED_RESULT_TOOL_NAME = "nextclaw_submit_result";
4029
+ var StructuredResultSubmitTool = class {
4030
+ name = STRUCTURED_RESULT_TOOL_NAME;
4031
+ description = "Submit the structured object result for this request.";
4032
+ constructor(contract) {
4033
+ this.contract = contract;
3965
4034
  }
3966
- return `${script}${html}`;
3967
- }
3968
- function getPanelAppBridgeScript() {
3969
- return `
3970
- (() => {
3971
- const requestType = "nextclaw:panel-app-service-actions:request";
3972
- const responseType = "nextclaw:panel-app-service-actions:response";
3973
- const pending = new Map();
3974
- let counter = 0;
3975
-
3976
- function createRequestId() {
3977
- counter += 1;
3978
- return "panel-bridge-" + Date.now().toString(36) + "-" + counter.toString(36);
3979
- }
3980
-
3981
- function request(method, payload) {
3982
- const requestId = createRequestId();
3983
- return new Promise((resolve, reject) => {
3984
- pending.set(requestId, { method, resolve, reject });
3985
- window.parent.postMessage({ type: requestType, requestId, method, payload }, "*");
3986
- });
3987
- }
3988
-
3989
- function unwrapServiceActionResult(result) {
3990
- if (!result || typeof result !== "object") {
3991
- return result;
3992
- }
3993
- if (Object.prototype.hasOwnProperty.call(result, "structuredContent") && result.structuredContent !== undefined) {
3994
- return result.structuredContent;
3995
- }
3996
- const content = Array.isArray(result.content) ? result.content : undefined;
3997
- if (content && content.length === 1 && content[0]?.type === "text" && typeof content[0].text === "string") {
3998
- try {
3999
- return JSON.parse(content[0].text);
4000
- } catch {
4001
- return content[0].text;
4002
- }
4003
- }
4004
- return result;
4005
- }
4006
-
4007
- function resolveBridgeData(entry, data) {
4008
- if (entry.method === "list") {
4009
- return Array.isArray(data.data?.actions) ? data.data.actions : [];
4010
- }
4011
- if (entry.method === "invoke") {
4012
- return unwrapServiceActionResult(data.data?.result);
4013
- }
4014
- if (entry.method === "agent.generateObject") {
4015
- return data.data?.result;
4016
- }
4017
- return data.data;
4018
- }
4019
-
4020
- window.addEventListener("message", (event) => {
4021
- const data = event.data;
4022
- if (!data || data.type !== responseType || typeof data.requestId !== "string") {
4023
- return;
4024
- }
4025
- const entry = pending.get(data.requestId);
4026
- if (!entry) {
4027
- return;
4028
- }
4029
- pending.delete(data.requestId);
4030
- if (data.ok) {
4031
- entry.resolve(resolveBridgeData(entry, data));
4032
- return;
4033
- }
4034
- const error = new Error(data.error?.message || "NextClaw panel bridge request failed.");
4035
- error.code = data.error?.code;
4036
- error.details = data.error?.details;
4037
- entry.reject(error);
4038
- });
4039
-
4040
- const existing = window.nextclaw && typeof window.nextclaw === "object" ? window.nextclaw : {};
4041
- Object.defineProperty(window, "nextclaw", {
4042
- configurable: true,
4043
- value: {
4044
- ...existing,
4045
- serviceActions: {
4046
- list: () => request("list", {}),
4047
- invoke: (actionId, input) => request("invoke", { actionId, input }),
4048
- requestGrant: (actionId) => request("requestGrant", { actionId }),
4049
- revokeGrant: (actionId) => request("revokeGrant", { actionId })
4050
- },
4051
- agent: {
4052
- send: (input) => request("agent.send", { request: input }),
4053
- generateObject: (input) => request("agent.generateObject", { input })
4054
- }
4055
- }
4056
- });
4057
- })();
4058
- `.trim();
4059
- }
4060
- //#endregion
4061
- //#region src/utils/panel-app-manifest.utils.ts
4062
- const PANEL_APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4063
- function parsePanelAppManifest(html) {
4064
- return {
4065
- ...readHtmlTitle(html),
4066
- ...readStandardIcon(html),
4067
- ...readPanelAppMeta(html),
4068
- capabilities: readPanelAppCapabilities(html),
4069
- serviceActions: readPanelAppServiceActions(html)
4070
- };
4071
- }
4072
- function parsePanelAppFolderManifest(raw) {
4073
- let parsed;
4074
- try {
4075
- parsed = JSON.parse(raw);
4076
- } catch (error) {
4077
- throw new Error(`panel-app.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
4035
+ get parameters() {
4036
+ return this.contract.schema;
4078
4037
  }
4079
- if (!isRecord$7(parsed)) throw new Error("panel-app.json must contain an object.");
4080
- const id = readOptionalString$6(parsed, "id");
4081
- if (id && !PANEL_APP_ID_PATTERN.test(id)) throw new Error("panel app id must be kebab-case.");
4038
+ validateArgs = (args) => validateToolArgs(args, this.contract.schema);
4039
+ execute = async (args) => args;
4040
+ };
4041
+ //#endregion
4042
+ //#region src/utils/panel-app-agent.utils.ts
4043
+ const PANEL_APP_AGENT_DEFAULT_TIMEOUT_MS = 6e4;
4044
+ const PANEL_APP_AGENT_MAX_TIMEOUT_MS = 12e4;
4045
+ const PANEL_APP_AGENT_MAX_PROMPT_CHARS = 2e4;
4046
+ const PANEL_APP_AGENT_MAX_CONTEXT_CHARS = 8e4;
4047
+ function normalizePanelAppGenerateObjectInput(input) {
4048
+ const peerId = input.peerId.trim();
4049
+ const prompt = input.prompt.trim();
4050
+ if (!peerId || !prompt || !isRecord$7(input.schema)) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "invalid generateObject request");
4051
+ if (prompt.length > PANEL_APP_AGENT_MAX_PROMPT_CHARS) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "generateObject prompt is too large");
4052
+ if (stringifyJsonValue(input.context ?? null).length > PANEL_APP_AGENT_MAX_CONTEXT_CHARS) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "generateObject context is too large");
4082
4053
  return {
4083
- id,
4084
- title: readRequiredString$3(parsed, "title"),
4085
- description: readOptionalString$6(parsed, "description"),
4086
- icon: readOptionalString$6(parsed, "icon"),
4087
- entry: readRequiredString$3(parsed, "entry"),
4088
- capabilities: readStringArray$1(parsed.capabilities, "capabilities"),
4089
- serviceActions: readStringArray$1(parsed.actions, "actions")
4054
+ context: input.context,
4055
+ peerId,
4056
+ prompt,
4057
+ schema: input.schema,
4058
+ timeoutMs: normalizeTimeoutMs(input.timeoutMs),
4059
+ title: input.title?.trim() || void 0
4090
4060
  };
4091
4061
  }
4092
- function readPanelAppMeta(html) {
4062
+ function createPanelAppGenerateObjectMessage(params) {
4063
+ const { bridgeSession, request, requestId } = params;
4093
4064
  return {
4094
- ...readPanelAppMetaField(html, "title"),
4095
- ...readPanelAppMetaField(html, "description"),
4096
- ...readPanelAppMetaField(html, "icon")
4065
+ id: `panel-app-agent-message-${randomUUID()}`,
4066
+ metadata: {
4067
+ ...createPanelAppAgentMetadata(bridgeSession),
4068
+ panel_app_peer_id: request.peerId,
4069
+ structured_result: {
4070
+ request_id: requestId,
4071
+ schema: structuredClone(request.schema),
4072
+ tool_name: STRUCTURED_RESULT_TOOL_NAME
4073
+ }
4074
+ },
4075
+ parts: [{
4076
+ type: "text",
4077
+ text: buildGenerateObjectPrompt(bridgeSession, request)
4078
+ }],
4079
+ role: "user",
4080
+ status: "final",
4081
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4097
4082
  };
4098
4083
  }
4099
- function readPanelAppMetaField(html, field) {
4100
- const content = readMetaContent(html, `nextclaw-panel-${field}`, field === "icon" ? "attribute" : "text");
4101
- const manifest = {};
4102
- if (content) manifest[field] = content;
4103
- return manifest;
4104
- }
4105
- function readHtmlTitle(html) {
4106
- const title = normalizeTextValue(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]);
4107
- return title ? { title } : {};
4108
- }
4109
- function readStandardIcon(html) {
4110
- const icon = readLinkHref(html, (relTokens) => relTokens.includes("icon"));
4111
- const appleTouchIcon = readLinkHref(html, (relTokens) => relTokens.some((token) => token === "apple-touch-icon" || token === "apple-touch-icon-precomposed"));
4084
+ async function waitForPanelAppStructuredResult(agentRunClient, params) {
4085
+ const iterator = agentRunClient.sendAndStreamEvents(params.payload)[Symbol.asyncIterator]();
4086
+ let timeoutId;
4087
+ const timeout = new Promise((_, reject) => {
4088
+ timeoutId = setTimeout(() => reject(new PanelAppError("AGENT_OBJECT_RESULT_TIMEOUT", "agent object result timed out")), params.timeoutMs);
4089
+ });
4090
+ let resultToolCallId = null;
4091
+ try {
4092
+ while (true) {
4093
+ const next = await Promise.race([iterator.next(), timeout]);
4094
+ if (next.done) break;
4095
+ const result = readStructuredResultEvent(next.value, resultToolCallId);
4096
+ if (result.matchedToolCallId) resultToolCallId = result.matchedToolCallId;
4097
+ if (result.submitted) return result.content;
4098
+ throwIfTerminalError(next.value);
4099
+ }
4100
+ } finally {
4101
+ if (timeoutId) clearTimeout(timeoutId);
4102
+ await iterator.return?.(void 0);
4103
+ }
4104
+ throw new PanelAppError("AGENT_OBJECT_RESULT_NOT_SUBMITTED", "agent did not submit a structured object result");
4105
+ }
4106
+ function withPanelAppAgentMetadata(payload, bridgeSession) {
4107
+ const panelAppMetadata = createPanelAppAgentMetadata(bridgeSession);
4108
+ const peerId = readOptionalPeerId(payload.peerId);
4109
+ const sessionId = readOptionalSessionId(payload.sessionId);
4110
+ if (peerId && sessionId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent send request cannot include both sessionId and peerId");
4111
+ const metadata = {
4112
+ ...payload.metadata ?? {},
4113
+ ...panelAppMetadata
4114
+ };
4115
+ if (peerId) metadata.panel_app_peer_id = peerId;
4116
+ if (Array.isArray(payload.content)) return {
4117
+ content: structuredClone(payload.content),
4118
+ metadata,
4119
+ peerId,
4120
+ sessionId
4121
+ };
4122
+ if (!payload.message) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent send request is required");
4123
+ return {
4124
+ message: {
4125
+ ...structuredClone(payload.message),
4126
+ metadata: {
4127
+ ...payload.message.metadata ?? {},
4128
+ ...panelAppMetadata
4129
+ }
4130
+ },
4131
+ metadata,
4132
+ peerId,
4133
+ sessionId
4134
+ };
4135
+ }
4136
+ function createPanelAppAgentMetadata(bridgeSession) {
4137
+ return {
4138
+ agent_peer_scope: `panel-app:${bridgeSession.appId}`,
4139
+ panel_app_bridge_request_id: randomUUID(),
4140
+ panel_app_id: bridgeSession.appId,
4141
+ source_kind: "panel_app"
4142
+ };
4143
+ }
4144
+ function normalizeTimeoutMs(timeoutMs) {
4145
+ if (!Number.isFinite(timeoutMs) || typeof timeoutMs !== "number") return PANEL_APP_AGENT_DEFAULT_TIMEOUT_MS;
4146
+ return Math.min(PANEL_APP_AGENT_MAX_TIMEOUT_MS, Math.max(1e3, Math.trunc(timeoutMs)));
4147
+ }
4148
+ function readOptionalPeerId(value) {
4149
+ if (typeof value !== "string") return;
4150
+ const peerId = value.trim();
4151
+ if (!peerId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent peerId cannot be empty");
4152
+ return peerId;
4153
+ }
4154
+ function readOptionalSessionId(value) {
4155
+ if (typeof value !== "string") return;
4156
+ const sessionId = value.trim();
4157
+ if (!sessionId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent sessionId cannot be empty");
4158
+ return sessionId;
4159
+ }
4160
+ function buildGenerateObjectPrompt(bridgeSession, request) {
4161
+ return [
4162
+ "Panel App generateObject request",
4163
+ "",
4164
+ `Panel App ID: ${bridgeSession.appId}`,
4165
+ `Peer ID: ${request.peerId}`,
4166
+ "",
4167
+ "Context JSON:",
4168
+ stringifyJsonValue(request.context ?? null),
4169
+ "",
4170
+ "Task:",
4171
+ request.prompt,
4172
+ "",
4173
+ "Result contract:",
4174
+ `Call the ${STRUCTURED_RESULT_TOOL_NAME} tool exactly once with an object matching the provided schema.`,
4175
+ "Do not use natural language as the result."
4176
+ ].join("\n");
4177
+ }
4178
+ function readStructuredResultEvent(event, resultToolCallId) {
4179
+ if (event.type === NcpEventType.MessageToolCallStart && event.payload.toolName === "nextclaw_submit_result") return {
4180
+ matchedToolCallId: event.payload.toolCallId,
4181
+ submitted: false
4182
+ };
4183
+ if (event.type !== NcpEventType.MessageToolCallResult || event.payload.toolCallId !== resultToolCallId) return { submitted: false };
4184
+ assertToolResultContent(event.payload.content);
4185
+ return {
4186
+ content: event.payload.content,
4187
+ submitted: true
4188
+ };
4189
+ }
4190
+ function assertToolResultContent(content) {
4191
+ if (!isRecord$7(content) || content.ok !== false || !isRecord$7(content.error)) return;
4192
+ if (content.error.code === "invalid_tool_arguments") throw new PanelAppError("AGENT_OBJECT_RESULT_SCHEMA_INVALID", "agent object result did not match the schema");
4193
+ throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", typeof content.error.message === "string" ? content.error.message : "agent object request failed");
4194
+ }
4195
+ function throwIfTerminalError(event) {
4196
+ if (event.type === NcpEventType.MessageFailed) throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", event.payload.error.message);
4197
+ if (event.type === NcpEventType.RunError) throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", event.payload.error ?? "agent object request failed");
4198
+ if (event.type === NcpEventType.RunFinished) throw new PanelAppError("AGENT_OBJECT_RESULT_NOT_SUBMITTED", "agent did not submit a structured object result");
4199
+ }
4200
+ function stringifyJsonValue(value) {
4201
+ try {
4202
+ return JSON.stringify(value, null, 2);
4203
+ } catch {
4204
+ throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "value is not JSON serializable");
4205
+ }
4206
+ }
4207
+ function isRecord$7(value) {
4208
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4209
+ }
4210
+ //#endregion
4211
+ //#region src/services/panel-app-agent-bridge.service.ts
4212
+ var PanelAppAgentBridgeService = class {
4213
+ constructor(params) {
4214
+ this.params = params;
4215
+ }
4216
+ sendAgentMessage = async (bridgeSession, payload) => {
4217
+ await this.assertAgentCapabilityGranted(bridgeSession, "agent:send");
4218
+ return await this.requireAgentRunClient().send(withPanelAppAgentMetadata(payload, bridgeSession));
4219
+ };
4220
+ generateAgentObject = async (bridgeSession, input) => {
4221
+ await this.assertAgentCapabilityGranted(bridgeSession, "agent:generateObject");
4222
+ const request = normalizePanelAppGenerateObjectInput(input);
4223
+ const message = createPanelAppGenerateObjectMessage({
4224
+ bridgeSession,
4225
+ request,
4226
+ requestId: randomUUID()
4227
+ });
4228
+ return { result: await waitForPanelAppStructuredResult(this.requireAgentRunClient(), {
4229
+ payload: {
4230
+ message,
4231
+ metadata: {
4232
+ ...createPanelAppAgentMetadata(bridgeSession),
4233
+ panel_app_peer_id: request.peerId
4234
+ },
4235
+ peerId: request.peerId
4236
+ },
4237
+ timeoutMs: request.timeoutMs
4238
+ }) };
4239
+ };
4240
+ grantAgentCapability = async (bridgeSession, capability) => {
4241
+ if (!isPanelAppAgentCapability(capability)) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "unknown panel app agent capability");
4242
+ this.assertDeclaredCapability(bridgeSession, capability);
4243
+ return await this.params.createCapabilityGrantStore().grant({
4244
+ caller: bridgeSession.caller,
4245
+ capability,
4246
+ grantedAt: (/* @__PURE__ */ new Date()).toISOString()
4247
+ });
4248
+ };
4249
+ assertAgentCapabilityGranted = async (bridgeSession, capability) => {
4250
+ this.assertDeclaredCapability(bridgeSession, capability);
4251
+ if (!await this.params.createCapabilityGrantStore().isGranted(bridgeSession.caller, capability)) throw new PanelAppError("AUTHORIZATION_REQUIRED", `This panel app needs permission to use ${capability}.`);
4252
+ };
4253
+ assertDeclaredCapability = (bridgeSession, capability) => {
4254
+ if (!bridgeSession.declaredCapabilities.includes(capability)) throw new PanelAppError("PANEL_APP_CAPABILITY_NOT_DECLARED", this.describeMissingAgentCapability(bridgeSession.declaredCapabilities, capability));
4255
+ };
4256
+ describeMissingAgentCapability = (declaredCapabilities, capability) => {
4257
+ const declared = declaredCapabilities.length > 0 ? declaredCapabilities.join(", ") : "none";
4258
+ const valid = PANEL_APP_AGENT_CAPABILITIES.join(", ");
4259
+ const hint = declaredCapabilities.includes(capability.replace(":", ".")) ? ` Use ${capability}, not ${capability.replace(":", ".")}.` : "";
4260
+ return [
4261
+ `panel app did not declare ${capability}.`,
4262
+ `Declared: ${declared}.`,
4263
+ `Valid capabilities: ${valid}.`,
4264
+ `Declare it with nextclaw-panel-capabilities or panel-app.json capabilities.`,
4265
+ hint.trim()
4266
+ ].filter(Boolean).join(" ");
4267
+ };
4268
+ requireAgentRunClient = () => {
4269
+ if (!this.params.agentRunClient) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "panel app agent client is not configured");
4270
+ return this.params.agentRunClient;
4271
+ };
4272
+ };
4273
+ //#endregion
4274
+ //#region src/utils/panel-app-bridge.utils.ts
4275
+ const PANEL_APP_BRIDGE_MARKER = "nextclaw:panel-app-service-actions:request";
4276
+ function injectPanelAppBridgeScript(html, params) {
4277
+ if (html.includes(PANEL_APP_BRIDGE_MARKER)) return html;
4278
+ const script = `<script>${getPanelAppBridgeScript(params)}<\/script>`;
4279
+ const headMatch = /<head(?:\s[^>]*)?>/i.exec(html);
4280
+ if (headMatch?.index !== void 0) {
4281
+ const insertAt = headMatch.index + headMatch[0].length;
4282
+ return `${html.slice(0, insertAt)}${script}${html.slice(insertAt)}`;
4283
+ }
4284
+ return `${script}${html}`;
4285
+ }
4286
+ function getPanelAppBridgeScript(params = {
4287
+ appId: "",
4288
+ runtimeToken: ""
4289
+ }) {
4290
+ return `
4291
+ (() => {
4292
+ const requestType = "nextclaw:panel-app-service-actions:request";
4293
+ const responseType = "nextclaw:panel-app-service-actions:response";
4294
+ const appId = ${JSON.stringify(params.appId)};
4295
+ const runtimeToken = ${JSON.stringify(params.runtimeToken)};
4296
+ const pending = new Map();
4297
+ let counter = 0;
4298
+
4299
+ function createRequestId() {
4300
+ counter += 1;
4301
+ return "panel-bridge-" + Date.now().toString(36) + "-" + counter.toString(36);
4302
+ }
4303
+
4304
+ function request(method, payload) {
4305
+ const requestId = createRequestId();
4306
+ return new Promise((resolve, reject) => {
4307
+ pending.set(requestId, { method, resolve, reject });
4308
+ window.parent.postMessage({ type: requestType, requestId, appId, runtimeToken, method, payload }, "*");
4309
+ });
4310
+ }
4311
+
4312
+ function unwrapServiceActionResult(result) {
4313
+ if (!result || typeof result !== "object") {
4314
+ return result;
4315
+ }
4316
+ if (Object.prototype.hasOwnProperty.call(result, "structuredContent") && result.structuredContent !== undefined) {
4317
+ return result.structuredContent;
4318
+ }
4319
+ const content = Array.isArray(result.content) ? result.content : undefined;
4320
+ if (content && content.length === 1 && content[0]?.type === "text" && typeof content[0].text === "string") {
4321
+ try {
4322
+ return JSON.parse(content[0].text);
4323
+ } catch {
4324
+ return content[0].text;
4325
+ }
4326
+ }
4327
+ return result;
4328
+ }
4329
+
4330
+ function resolveBridgeData(entry, data) {
4331
+ if (entry.method === "list") {
4332
+ return Array.isArray(data.data?.actions) ? data.data.actions : [];
4333
+ }
4334
+ if (entry.method === "invoke") {
4335
+ return unwrapServiceActionResult(data.data?.result);
4336
+ }
4337
+ if (entry.method === "agent.generateObject") {
4338
+ return data.data?.result;
4339
+ }
4340
+ return data.data;
4341
+ }
4342
+
4343
+ window.addEventListener("message", (event) => {
4344
+ const data = event.data;
4345
+ if (!data || data.type !== responseType || typeof data.requestId !== "string") {
4346
+ return;
4347
+ }
4348
+ const entry = pending.get(data.requestId);
4349
+ if (!entry) {
4350
+ return;
4351
+ }
4352
+ pending.delete(data.requestId);
4353
+ if (data.ok) {
4354
+ entry.resolve(resolveBridgeData(entry, data));
4355
+ return;
4356
+ }
4357
+ const error = new Error(data.error?.message || "NextClaw panel bridge request failed.");
4358
+ error.code = data.error?.code;
4359
+ error.details = data.error?.details;
4360
+ entry.reject(error);
4361
+ });
4362
+
4363
+ const existing = window.nextclaw && typeof window.nextclaw === "object" ? window.nextclaw : {};
4364
+ Object.defineProperty(window, "nextclaw", {
4365
+ configurable: true,
4366
+ value: {
4367
+ ...existing,
4368
+ serviceActions: {
4369
+ list: () => request("list", {}),
4370
+ invoke: (actionId, input) => request("invoke", { actionId, input }),
4371
+ requestGrant: (actionId) => request("requestGrant", { actionId }),
4372
+ revokeGrant: (actionId) => request("revokeGrant", { actionId })
4373
+ },
4374
+ agent: {
4375
+ send: (input) => request("agent.send", { request: input }),
4376
+ generateObject: (input) => request("agent.generateObject", { input })
4377
+ }
4378
+ }
4379
+ });
4380
+ })();
4381
+ `.trim();
4382
+ }
4383
+ //#endregion
4384
+ //#region src/utils/panel-app-client-injection.utils.ts
4385
+ const PANEL_APP_CLIENT_MARKER = "nextclaw:panel-app-client:init";
4386
+ const PANEL_APP_CLIENT_SDK_PATH = "/api/panel-app-client-sdk.js";
4387
+ function injectPanelAppClientScript(html, params) {
4388
+ if (html.includes(PANEL_APP_CLIENT_MARKER)) return html;
4389
+ const script = [`<script src="${PANEL_APP_CLIENT_SDK_PATH}"><\/script>`, `<script>${getPanelAppClientInitScript(params)}<\/script>`].join("");
4390
+ const headMatch = /<head(?:\s[^>]*)?>/i.exec(html);
4391
+ if (headMatch?.index !== void 0) {
4392
+ const insertAt = headMatch.index + headMatch[0].length;
4393
+ return `${html.slice(0, insertAt)}${script}${html.slice(insertAt)}`;
4394
+ }
4395
+ return `${script}${html}`;
4396
+ }
4397
+ function getPanelAppClientInitScript(params) {
4398
+ return `
4399
+ (() => {
4400
+ const marker = "${PANEL_APP_CLIENT_MARKER}";
4401
+ if (typeof window.NextClawClient !== "function") {
4402
+ console.error("[NextClaw] Panel App client SDK failed to load.");
4403
+ return;
4404
+ }
4405
+ if (typeof window.createNextClawAppClient !== "function") {
4406
+ console.error("[NextClaw] Panel App client projection failed to load.");
4407
+ return;
4408
+ }
4409
+ const existing = window.nextclaw && typeof window.nextclaw === "object" ? window.nextclaw : {};
4410
+ const hostClient = new window.NextClawClient({
4411
+ baseUrl: window.location.origin,
4412
+ headers: {
4413
+ "x-nextclaw-panel-bridge-session": ${JSON.stringify(params.runtimeToken)}
4414
+ }
4415
+ });
4416
+ const client = window.createNextClawAppClient(hostClient);
4417
+ Object.defineProperty(window, "nextclaw", {
4418
+ configurable: true,
4419
+ value: {
4420
+ ...existing,
4421
+ client
4422
+ }
4423
+ });
4424
+ Object.defineProperty(window.nextclaw, "__clientInitMarker", {
4425
+ configurable: true,
4426
+ enumerable: false,
4427
+ value: marker
4428
+ });
4429
+ })();
4430
+ `.trim();
4431
+ }
4432
+ //#endregion
4433
+ //#region src/utils/panel-app-manifest.utils.ts
4434
+ const PANEL_APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4435
+ function parsePanelAppManifest(html) {
4436
+ return {
4437
+ ...readHtmlTitle(html),
4438
+ ...readStandardIcon(html),
4439
+ ...readPanelAppMeta(html),
4440
+ capabilities: readPanelAppCapabilities(html),
4441
+ client: false,
4442
+ serviceActions: readPanelAppServiceActions(html)
4443
+ };
4444
+ }
4445
+ function parsePanelAppFolderManifest(raw) {
4446
+ let parsed;
4447
+ try {
4448
+ parsed = JSON.parse(raw);
4449
+ } catch (error) {
4450
+ throw new Error(`panel-app.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
4451
+ }
4452
+ if (!isRecord$6(parsed)) throw new Error("panel-app.json must contain an object.");
4453
+ const id = readOptionalString$6(parsed, "id");
4454
+ if (id && !PANEL_APP_ID_PATTERN.test(id)) throw new Error("panel app id must be kebab-case.");
4455
+ return {
4456
+ id,
4457
+ title: readRequiredString$3(parsed, "title"),
4458
+ description: readOptionalString$6(parsed, "description"),
4459
+ icon: readOptionalString$6(parsed, "icon"),
4460
+ entry: readRequiredString$3(parsed, "entry"),
4461
+ capabilities: readStringArray$1(parsed.capabilities, "capabilities"),
4462
+ client: readOptionalBoolean$2(parsed, "client"),
4463
+ serviceActions: readStringArray$1(parsed.actions, "actions")
4464
+ };
4465
+ }
4466
+ function readPanelAppMeta(html) {
4467
+ return {
4468
+ ...readPanelAppMetaField(html, "title"),
4469
+ ...readPanelAppMetaField(html, "description"),
4470
+ ...readPanelAppMetaField(html, "icon")
4471
+ };
4472
+ }
4473
+ function readPanelAppMetaField(html, field) {
4474
+ const content = readMetaContent(html, `nextclaw-panel-${field}`, field === "icon" ? "attribute" : "text");
4475
+ const manifest = {};
4476
+ if (content) manifest[field] = content;
4477
+ return manifest;
4478
+ }
4479
+ function readHtmlTitle(html) {
4480
+ const title = normalizeTextValue(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]);
4481
+ return title ? { title } : {};
4482
+ }
4483
+ function readStandardIcon(html) {
4484
+ const icon = readLinkHref(html, (relTokens) => relTokens.includes("icon"));
4485
+ const appleTouchIcon = readLinkHref(html, (relTokens) => relTokens.some((token) => token === "apple-touch-icon" || token === "apple-touch-icon-precomposed"));
4112
4486
  const href = normalizeIconHref(icon ?? appleTouchIcon);
4113
4487
  return href ? { icon: href } : {};
4114
4488
  }
@@ -4151,12 +4525,18 @@ function readRequiredString$3(record, key) {
4151
4525
  function readOptionalString$6(record, key) {
4152
4526
  return typeof record[key] === "string" && record[key].trim() ? record[key].trim() : void 0;
4153
4527
  }
4528
+ function readOptionalBoolean$2(record, key) {
4529
+ const value = record[key];
4530
+ if (value === void 0) return false;
4531
+ if (typeof value !== "boolean") throw new Error(`panel app ${key} must be a boolean.`);
4532
+ return value;
4533
+ }
4154
4534
  function readStringArray$1(value, key) {
4155
4535
  if (value === void 0) return [];
4156
4536
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`panel app ${key} must be a string array.`);
4157
4537
  return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
4158
4538
  }
4159
- function isRecord$7(value) {
4539
+ function isRecord$6(value) {
4160
4540
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4161
4541
  }
4162
4542
  function normalizeTextValue(value) {
@@ -4236,296 +4616,168 @@ function resolvePanelAppIconUrl(id, icon) {
4236
4616
  if (icon.startsWith("data:image/") || icon.startsWith("http://") || icon.startsWith("https://") || icon.startsWith("/") || isLikelyTextIcon(icon)) return icon;
4237
4617
  return `/api/panel-apps/${encodeURIComponent(id)}/assets/${encodePanelAppAssetPath(icon)}`;
4238
4618
  }
4239
- function injectPanelAppAssetBase(html, baseHref) {
4240
- const base = `<base href="${baseHref}">`;
4241
- if (/<base\b/i.test(html)) return html;
4242
- if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (head) => `${head}${base}`);
4243
- return `${base}${html}`;
4244
- }
4245
- function encodePanelAppAssetPath(path) {
4246
- return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
4247
- }
4248
- function hasSafeBaseName(name) {
4249
- return !name.includes("/") && !name.includes("\\") && !name.includes("\0");
4250
- }
4251
- function isLikelyTextIcon(icon) {
4252
- return !icon.includes("/") && !icon.includes(".") && icon.length <= 4;
4253
- }
4254
- function escapeRegExp(value) {
4255
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4256
- }
4257
- //#endregion
4258
- //#region src/services/panel-app-source.service.ts
4259
- var PanelAppSourceService = class {
4260
- listSources = async (panelsPath) => {
4261
- try {
4262
- const entries = await readdir(panelsPath, { withFileTypes: true });
4263
- const sources = [];
4264
- for (const entry of entries.filter(isPanelAppSourceEntry)) try {
4265
- sources.push(await this.readSource(panelsPath, entry.name));
4266
- } catch (error) {
4267
- if (!isPanelAppError(error) || error.code !== "PANEL_APP_NOT_FOUND") throw error;
4268
- }
4269
- return sources;
4270
- } catch (error) {
4271
- if (isPanelAppError(error)) throw error;
4272
- if (isMissingFileError$1(error)) return [];
4273
- throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4274
- }
4275
- };
4276
- resolveSource = async (panelsPath, id) => {
4277
- const sourceName = decodePanelAppId(id);
4278
- try {
4279
- return await this.readSource(panelsPath, sourceName);
4280
- } catch (error) {
4281
- if (isPanelAppError(error)) throw error;
4282
- if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4283
- throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4284
- }
4285
- };
4286
- getAsset = async (panelsPath, id, assetPath) => {
4287
- const source = await this.resolveSource(panelsPath, id);
4288
- if (source.kind !== "folder") throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4289
- const filePath = resolvePanelAppRelativePath(source.sourcePath, assetPath);
4290
- try {
4291
- if (!(await stat(filePath)).isFile()) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4292
- return {
4293
- content: await readFile(filePath),
4294
- contentType: resolvePanelAppAssetContentType(filePath)
4295
- };
4296
- } catch (error) {
4297
- if (isPanelAppError(error)) throw error;
4298
- if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4299
- throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4300
- }
4301
- };
4302
- readSource = async (panelsPath, sourceName) => {
4303
- const sourcePath = join(panelsPath, sourceName);
4304
- const sourceStat = await stat(sourcePath);
4305
- if (sourceStat.isFile() && isPanelAppFileName(sourceName)) return {
4306
- kind: "single-file",
4307
- sourceName,
4308
- sourcePath,
4309
- entryPath: sourcePath,
4310
- sourceStat
4311
- };
4312
- if (sourceStat.isDirectory() && isPanelAppDirName(sourceName)) return await this.readFolderSource(sourcePath, sourceName, sourceStat);
4313
- throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4314
- };
4315
- readFolderSource = async (sourcePath, sourceName, sourceStat) => {
4316
- try {
4317
- const manifest = await readPanelAppFolderManifest(sourcePath, sourceName);
4318
- const entryPath = resolvePanelAppRelativePath(sourcePath, manifest.entry);
4319
- if (!(await stat(entryPath)).isFile()) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app entry not found");
4320
- return {
4321
- kind: "folder",
4322
- sourceName,
4323
- sourcePath,
4324
- entryPath,
4325
- manifest,
4326
- sourceStat
4327
- };
4328
- } catch (error) {
4329
- if (isPanelAppError(error)) throw error;
4330
- if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4331
- throw new PanelAppError("PANEL_APP_MANIFEST_INVALID", error instanceof Error ? error.message : String(error));
4332
- }
4333
- };
4334
- };
4335
- function isMissingFileError$1(error) {
4336
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4337
- }
4338
- //#endregion
4339
- //#region src/utils/panel-app-time.utils.ts
4340
- function resolvePanelAppCreatedAt(fileStat) {
4341
- return fileStat.birthtimeMs > 0 ? fileStat.birthtime.toISOString() : fileStat.mtime.toISOString();
4342
- }
4343
- function resolvePanelAppActivityMs(entry) {
4344
- return Math.max(new Date(entry.lastOpenedAt ?? 0).getTime(), new Date(entry.createdAt).getTime(), new Date(entry.updatedAt).getTime());
4345
- }
4346
- //#endregion
4347
- //#region src/tools/structured-result.tools.ts
4348
- const STRUCTURED_RESULT_TOOL_NAME = "nextclaw_submit_result";
4349
- var StructuredResultSubmitTool = class {
4350
- name = STRUCTURED_RESULT_TOOL_NAME;
4351
- description = "Submit the structured object result for this request.";
4352
- constructor(contract) {
4353
- this.contract = contract;
4354
- }
4355
- get parameters() {
4356
- return this.contract.schema;
4357
- }
4358
- validateArgs = (args) => validateToolArgs(args, this.contract.schema);
4359
- execute = async (args) => args;
4360
- };
4361
- //#endregion
4362
- //#region src/utils/panel-app-agent.utils.ts
4363
- const PANEL_APP_AGENT_DEFAULT_TIMEOUT_MS = 6e4;
4364
- const PANEL_APP_AGENT_MAX_TIMEOUT_MS = 12e4;
4365
- const PANEL_APP_AGENT_MAX_PROMPT_CHARS = 2e4;
4366
- const PANEL_APP_AGENT_MAX_CONTEXT_CHARS = 8e4;
4367
- function normalizePanelAppGenerateObjectInput(input) {
4368
- const peerId = input.peerId.trim();
4369
- const prompt = input.prompt.trim();
4370
- if (!peerId || !prompt || !isRecord$6(input.schema)) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "invalid generateObject request");
4371
- if (prompt.length > PANEL_APP_AGENT_MAX_PROMPT_CHARS) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "generateObject prompt is too large");
4372
- if (stringifyJsonValue(input.context ?? null).length > PANEL_APP_AGENT_MAX_CONTEXT_CHARS) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "generateObject context is too large");
4373
- return {
4374
- context: input.context,
4375
- peerId,
4376
- prompt,
4377
- schema: input.schema,
4378
- timeoutMs: normalizeTimeoutMs(input.timeoutMs),
4379
- title: input.title?.trim() || void 0
4380
- };
4381
- }
4382
- function createPanelAppGenerateObjectMessage(params) {
4383
- const { bridgeSession, request, requestId } = params;
4384
- return {
4385
- id: `panel-app-agent-message-${randomUUID()}`,
4386
- metadata: {
4387
- ...createPanelAppAgentMetadata(bridgeSession),
4388
- panel_app_peer_id: request.peerId,
4389
- structured_result: {
4390
- request_id: requestId,
4391
- schema: structuredClone(request.schema),
4392
- tool_name: STRUCTURED_RESULT_TOOL_NAME
4393
- }
4394
- },
4395
- parts: [{
4396
- type: "text",
4397
- text: buildGenerateObjectPrompt(bridgeSession, request)
4398
- }],
4399
- role: "user",
4400
- status: "final",
4401
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4402
- };
4403
- }
4404
- async function waitForPanelAppStructuredResult(agentRunClient, params) {
4405
- const iterator = agentRunClient.sendAndStreamEvents(params.payload)[Symbol.asyncIterator]();
4406
- let timeoutId;
4407
- const timeout = new Promise((_, reject) => {
4408
- timeoutId = setTimeout(() => reject(new PanelAppError("AGENT_OBJECT_RESULT_TIMEOUT", "agent object result timed out")), params.timeoutMs);
4409
- });
4410
- let resultToolCallId = null;
4411
- try {
4412
- while (true) {
4413
- const next = await Promise.race([iterator.next(), timeout]);
4414
- if (next.done) break;
4415
- const result = readStructuredResultEvent(next.value, resultToolCallId);
4416
- if (result.matchedToolCallId) resultToolCallId = result.matchedToolCallId;
4417
- if (result.submitted) return result.content;
4418
- throwIfTerminalError(next.value);
4419
- }
4420
- } finally {
4421
- if (timeoutId) clearTimeout(timeoutId);
4422
- await iterator.return?.(void 0);
4423
- }
4424
- throw new PanelAppError("AGENT_OBJECT_RESULT_NOT_SUBMITTED", "agent did not submit a structured object result");
4425
- }
4426
- function withPanelAppAgentMetadata(payload, bridgeSession) {
4427
- const panelAppMetadata = createPanelAppAgentMetadata(bridgeSession);
4428
- const peerId = readOptionalPeerId(payload.peerId);
4429
- const sessionId = readOptionalSessionId(payload.sessionId);
4430
- if (peerId && sessionId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent send request cannot include both sessionId and peerId");
4431
- const metadata = {
4432
- ...payload.metadata ?? {},
4433
- ...panelAppMetadata
4434
- };
4435
- if (peerId) metadata.panel_app_peer_id = peerId;
4436
- if (Array.isArray(payload.content)) return {
4437
- content: structuredClone(payload.content),
4438
- metadata,
4439
- peerId,
4440
- sessionId
4441
- };
4442
- if (!payload.message) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent send request is required");
4443
- return {
4444
- message: {
4445
- ...structuredClone(payload.message),
4446
- metadata: {
4447
- ...payload.message.metadata ?? {},
4448
- ...panelAppMetadata
4449
- }
4450
- },
4451
- metadata,
4452
- peerId,
4453
- sessionId
4454
- };
4455
- }
4456
- function createPanelAppAgentMetadata(bridgeSession) {
4457
- return {
4458
- agent_peer_scope: `panel-app:${bridgeSession.panelAppId}`,
4459
- panel_app_bridge_request_id: randomUUID(),
4460
- panel_app_id: bridgeSession.panelAppId,
4461
- source_kind: "panel_app"
4462
- };
4619
+ function injectPanelAppAssetBase(html, baseHref) {
4620
+ const base = `<base href="${baseHref}">`;
4621
+ if (/<base\b/i.test(html)) return html;
4622
+ if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (head) => `${head}${base}`);
4623
+ return `${base}${html}`;
4463
4624
  }
4464
- function normalizeTimeoutMs(timeoutMs) {
4465
- if (!Number.isFinite(timeoutMs) || typeof timeoutMs !== "number") return PANEL_APP_AGENT_DEFAULT_TIMEOUT_MS;
4466
- return Math.min(PANEL_APP_AGENT_MAX_TIMEOUT_MS, Math.max(1e3, Math.trunc(timeoutMs)));
4625
+ function encodePanelAppAssetPath(path) {
4626
+ return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
4467
4627
  }
4468
- function readOptionalPeerId(value) {
4469
- if (typeof value !== "string") return;
4470
- const peerId = value.trim();
4471
- if (!peerId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent peerId cannot be empty");
4472
- return peerId;
4628
+ function hasSafeBaseName(name) {
4629
+ return !name.includes("/") && !name.includes("\\") && !name.includes("\0");
4473
4630
  }
4474
- function readOptionalSessionId(value) {
4475
- if (typeof value !== "string") return;
4476
- const sessionId = value.trim();
4477
- if (!sessionId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent sessionId cannot be empty");
4478
- return sessionId;
4631
+ function isLikelyTextIcon(icon) {
4632
+ return !icon.includes("/") && !icon.includes(".") && icon.length <= 4;
4479
4633
  }
4480
- function buildGenerateObjectPrompt(bridgeSession, request) {
4481
- return [
4482
- "Panel App generateObject request",
4483
- "",
4484
- `Panel App ID: ${bridgeSession.panelAppId}`,
4485
- `Peer ID: ${request.peerId}`,
4486
- "",
4487
- "Context JSON:",
4488
- stringifyJsonValue(request.context ?? null),
4489
- "",
4490
- "Task:",
4491
- request.prompt,
4492
- "",
4493
- "Result contract:",
4494
- `Call the ${STRUCTURED_RESULT_TOOL_NAME} tool exactly once with an object matching the provided schema.`,
4495
- "Do not use natural language as the result."
4496
- ].join("\n");
4634
+ function escapeRegExp(value) {
4635
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4497
4636
  }
4498
- function readStructuredResultEvent(event, resultToolCallId) {
4499
- if (event.type === NcpEventType.MessageToolCallStart && event.payload.toolName === "nextclaw_submit_result") return {
4500
- matchedToolCallId: event.payload.toolCallId,
4501
- submitted: false
4637
+ //#endregion
4638
+ //#region src/services/panel-app-source.service.ts
4639
+ var PanelAppSourceService = class {
4640
+ listSources = async (panelsPath) => {
4641
+ try {
4642
+ const entries = await readdir(panelsPath, { withFileTypes: true });
4643
+ const sources = [];
4644
+ for (const entry of entries.filter(isPanelAppSourceEntry)) try {
4645
+ sources.push(await this.readSource(panelsPath, entry.name));
4646
+ } catch (error) {
4647
+ if (!isPanelAppError(error) || error.code !== "PANEL_APP_NOT_FOUND") throw error;
4648
+ }
4649
+ return sources;
4650
+ } catch (error) {
4651
+ if (isPanelAppError(error)) throw error;
4652
+ if (isMissingFileError$1(error)) return [];
4653
+ throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4654
+ }
4502
4655
  };
4503
- if (event.type !== NcpEventType.MessageToolCallResult || event.payload.toolCallId !== resultToolCallId) return { submitted: false };
4504
- assertToolResultContent(event.payload.content);
4505
- return {
4506
- content: event.payload.content,
4507
- submitted: true
4656
+ resolveSource = async (panelsPath, id) => {
4657
+ const sourceName = decodePanelAppId(id);
4658
+ try {
4659
+ return await this.readSource(panelsPath, sourceName);
4660
+ } catch (error) {
4661
+ if (isPanelAppError(error)) throw error;
4662
+ if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4663
+ throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4664
+ }
4665
+ };
4666
+ getAsset = async (panelsPath, id, assetPath) => {
4667
+ const source = await this.resolveSource(panelsPath, id);
4668
+ if (source.kind !== "folder") throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4669
+ const filePath = resolvePanelAppRelativePath(source.sourcePath, assetPath);
4670
+ try {
4671
+ if (!(await stat(filePath)).isFile()) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4672
+ return {
4673
+ content: await readFile(filePath),
4674
+ contentType: resolvePanelAppAssetContentType(filePath)
4675
+ };
4676
+ } catch (error) {
4677
+ if (isPanelAppError(error)) throw error;
4678
+ if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4679
+ throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4680
+ }
4681
+ };
4682
+ readSource = async (panelsPath, sourceName) => {
4683
+ const sourcePath = join(panelsPath, sourceName);
4684
+ const sourceStat = await stat(sourcePath);
4685
+ if (sourceStat.isFile() && isPanelAppFileName(sourceName)) return {
4686
+ kind: "single-file",
4687
+ sourceName,
4688
+ sourcePath,
4689
+ entryPath: sourcePath,
4690
+ sourceStat
4691
+ };
4692
+ if (sourceStat.isDirectory() && isPanelAppDirName(sourceName)) return await this.readFolderSource(sourcePath, sourceName, sourceStat);
4693
+ throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4694
+ };
4695
+ readFolderSource = async (sourcePath, sourceName, sourceStat) => {
4696
+ try {
4697
+ const manifest = await readPanelAppFolderManifest(sourcePath, sourceName);
4698
+ const entryPath = resolvePanelAppRelativePath(sourcePath, manifest.entry);
4699
+ if (!(await stat(entryPath)).isFile()) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app entry not found");
4700
+ return {
4701
+ kind: "folder",
4702
+ sourceName,
4703
+ sourcePath,
4704
+ entryPath,
4705
+ manifest,
4706
+ sourceStat
4707
+ };
4708
+ } catch (error) {
4709
+ if (isPanelAppError(error)) throw error;
4710
+ if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4711
+ throw new PanelAppError("PANEL_APP_MANIFEST_INVALID", error instanceof Error ? error.message : String(error));
4712
+ }
4508
4713
  };
4714
+ };
4715
+ function isMissingFileError$1(error) {
4716
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4509
4717
  }
4510
- function assertToolResultContent(content) {
4511
- if (!isRecord$6(content) || content.ok !== false || !isRecord$6(content.error)) return;
4512
- if (content.error.code === "invalid_tool_arguments") throw new PanelAppError("AGENT_OBJECT_RESULT_SCHEMA_INVALID", "agent object result did not match the schema");
4513
- throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", typeof content.error.message === "string" ? content.error.message : "agent object request failed");
4718
+ //#endregion
4719
+ //#region src/utils/panel-app-time.utils.ts
4720
+ function resolvePanelAppCreatedAt(fileStat) {
4721
+ return fileStat.birthtimeMs > 0 ? fileStat.birthtime.toISOString() : fileStat.mtime.toISOString();
4514
4722
  }
4515
- function throwIfTerminalError(event) {
4516
- if (event.type === NcpEventType.MessageFailed) throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", event.payload.error.message);
4517
- if (event.type === NcpEventType.RunError) throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", event.payload.error ?? "agent object request failed");
4518
- if (event.type === NcpEventType.RunFinished) throw new PanelAppError("AGENT_OBJECT_RESULT_NOT_SUBMITTED", "agent did not submit a structured object result");
4723
+ function resolvePanelAppActivityMs(entry) {
4724
+ return Math.max(new Date(entry.lastOpenedAt ?? 0).getTime(), new Date(entry.createdAt).getTime(), new Date(entry.updatedAt).getTime());
4519
4725
  }
4520
- function stringifyJsonValue(value) {
4726
+ //#endregion
4727
+ //#region src/utils/panel-app-content-source.utils.ts
4728
+ async function readPanelAppContentSource(params) {
4729
+ const { createAssetBaseHref, id, panelsPath, sourceService } = params;
4730
+ const source = await sourceService.resolveSource(panelsPath, id);
4731
+ const html = await readFile(source.entryPath, "utf8");
4732
+ const manifest = source.manifest ?? parsePanelAppManifest(html);
4733
+ const sourceId = encodePanelAppId(source.sourceName);
4734
+ return {
4735
+ appId: resolvePanelAppAppId(source, manifest),
4736
+ sourceId,
4737
+ source,
4738
+ manifest,
4739
+ html,
4740
+ htmlWithBase: source.kind === "folder" ? injectPanelAppAssetBase(html, createAssetBaseHref(source)) : html
4741
+ };
4742
+ }
4743
+ async function readPanelAppContentSourceByIdOrAppId(params) {
4744
+ const { appIdOrSourceId, createAssetBaseHref, panelsPath, sourceService } = params;
4521
4745
  try {
4522
- return JSON.stringify(value, null, 2);
4523
- } catch {
4524
- throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "value is not JSON serializable");
4746
+ return await readPanelAppContentSource({
4747
+ createAssetBaseHref,
4748
+ id: appIdOrSourceId,
4749
+ panelsPath,
4750
+ sourceService
4751
+ });
4752
+ } catch (error) {
4753
+ if (!isPanelAppError(error)) throw error;
4754
+ if (error.code !== "PANEL_APP_INVALID_ID" && error.code !== "PANEL_APP_NOT_FOUND") throw error;
4755
+ }
4756
+ const sources = await sourceService.listSources(panelsPath);
4757
+ for (const source of sources) {
4758
+ const html = await readFile(source.entryPath, "utf8");
4759
+ if (resolvePanelAppAppId(source, source.manifest ?? parsePanelAppManifest(html)) === appIdOrSourceId) return await readPanelAppContentSource({
4760
+ createAssetBaseHref,
4761
+ id: encodePanelAppId(source.sourceName),
4762
+ panelsPath,
4763
+ sourceService
4764
+ });
4525
4765
  }
4766
+ throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4526
4767
  }
4527
- function isRecord$6(value) {
4528
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4768
+ function resolvePanelAppAppId(source, manifest) {
4769
+ return manifest.id ?? encodePanelAppId(source.sourceName);
4770
+ }
4771
+ async function assertPanelAppDeclaresClient(params) {
4772
+ const { appId, panelsPath, sourceService } = params;
4773
+ const sources = await sourceService.listSources(panelsPath);
4774
+ for (const source of sources) {
4775
+ const manifest = source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8"));
4776
+ if (resolvePanelAppAppId(source, manifest) !== appId) continue;
4777
+ if (!manifest.client) throw new PanelAppError("PANEL_APP_CLIENT_NOT_DECLARED", "panel app did not declare client access");
4778
+ return;
4779
+ }
4780
+ throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4529
4781
  }
4530
4782
  //#endregion
4531
4783
  //#region src/managers/panel-app.manager.ts
@@ -4533,9 +4785,12 @@ const PANEL_APP_CONTENT_BASE_PATH = "/api/panel-apps";
4533
4785
  const PANEL_APP_TOKENIZED_ASSET_BASE_PATH = "/api/panel-app-assets";
4534
4786
  const PANEL_APP_CONTENT_TYPE = "text/html; charset=utf-8";
4535
4787
  const PANEL_APP_CAPABILITY_GRANTS_FILE_NAME = ".panel-app-capability-grants.json";
4788
+ const PANEL_APP_CLIENT_GRANTS_FILE_NAME = ".panel-app-client-grants.json";
4789
+ const PANEL_APP_RUNTIME_TOKEN_TTL_MS = 1440 * 60 * 1e3;
4536
4790
  var PanelAppManager = class {
4537
4791
  bridgeSessions = /* @__PURE__ */ new Map();
4538
4792
  agentRunClient;
4793
+ agentBridgeService;
4539
4794
  assetTokenService = new PanelAppAssetTokenService();
4540
4795
  sourceService = new PanelAppSourceService();
4541
4796
  constructor(params) {
@@ -4544,6 +4799,10 @@ var PanelAppManager = class {
4544
4799
  eventBus: params.eventBus,
4545
4800
  ingress: params.ingress
4546
4801
  }) : null);
4802
+ this.agentBridgeService = new PanelAppAgentBridgeService({
4803
+ agentRunClient: this.agentRunClient,
4804
+ createCapabilityGrantStore: this.createCapabilityGrantStore
4805
+ });
4547
4806
  }
4548
4807
  listPanelApps = async () => {
4549
4808
  const workspacePath = this.getWorkspacePath();
@@ -4557,20 +4816,35 @@ var PanelAppManager = class {
4557
4816
  };
4558
4817
  };
4559
4818
  getPanelAppContent = async (id) => {
4560
- const panelsPath = this.getPanelsPath(this.getWorkspacePath());
4561
4819
  try {
4562
- const source = await this.sourceService.resolveSource(panelsPath, id);
4563
- const html = await readFile(source.entryPath, "utf8");
4564
- const manifest = source.manifest ?? parsePanelAppManifest(html);
4565
- const sourceId = encodePanelAppId(source.sourceName);
4566
- const htmlWithBase = source.kind === "folder" ? injectPanelAppAssetBase(html, this.createAssetBaseHref(source)) : html;
4820
+ const resolved = await readPanelAppContentSource({
4821
+ createAssetBaseHref: this.createAssetBaseHref,
4822
+ id,
4823
+ panelsPath: this.getPanelsPath(this.getWorkspacePath()),
4824
+ sourceService: this.sourceService
4825
+ });
4826
+ const clientGranted = await this.isPanelAppClientGranted(resolved.appId, resolved.manifest.client);
4827
+ const session = this.createPanelAppRuntimeTokenSession({
4828
+ appId: resolved.appId,
4829
+ clientDeclared: resolved.manifest.client,
4830
+ declaredActions: resolved.manifest.serviceActions,
4831
+ declaredCapabilities: resolved.manifest.capabilities
4832
+ });
4833
+ const htmlWithBridge = injectPanelAppBridgeScript(resolved.htmlWithBase, {
4834
+ appId: resolved.appId,
4835
+ runtimeToken: session.token
4836
+ });
4837
+ const html = resolved.manifest.client && clientGranted ? injectPanelAppClientScript(htmlWithBridge, { runtimeToken: session.token }) : htmlWithBridge;
4567
4838
  return {
4568
- id: sourceId,
4569
- fileName: source.sourceName,
4570
- html: injectPanelAppBridgeScript(htmlWithBase),
4571
- capabilities: manifest.capabilities,
4839
+ id: resolved.sourceId,
4840
+ appId: resolved.appId,
4841
+ fileName: resolved.source.sourceName,
4842
+ html,
4843
+ capabilities: resolved.manifest.capabilities,
4844
+ clientDeclared: resolved.manifest.client,
4845
+ clientGranted,
4572
4846
  contentType: PANEL_APP_CONTENT_TYPE,
4573
- serviceActions: manifest.serviceActions
4847
+ serviceActions: resolved.manifest.serviceActions
4574
4848
  };
4575
4849
  } catch (error) {
4576
4850
  if (isPanelAppError(error)) throw error;
@@ -4588,27 +4862,58 @@ var PanelAppManager = class {
4588
4862
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
4589
4863
  return await this.sourceService.getAsset(panelsPath, claims.panelAppId, assetPath);
4590
4864
  };
4591
- getPanelAppBridgeScript = () => getPanelAppBridgeScript();
4592
- createPanelAppBridgeSession = async (params) => {
4593
- const content = await this.getPanelAppContent(params.id);
4865
+ getPanelAppBridgeScript = () => getPanelAppBridgeScript({
4866
+ appId: "",
4867
+ runtimeToken: ""
4868
+ });
4869
+ createPanelAppRuntimeTokenSession = (params) => {
4870
+ const { appId, clientDeclared, declaredActions, declaredCapabilities } = params;
4594
4871
  const now = /* @__PURE__ */ new Date();
4595
4872
  const session = {
4596
4873
  id: randomUUID(),
4597
4874
  token: randomUUID(),
4598
- panelAppId: content.id,
4599
- tabId: params.tabId,
4875
+ appId,
4600
4876
  caller: {
4601
4877
  surface: "panel-app",
4602
- appId: content.id
4878
+ appId
4603
4879
  },
4604
- declaredCapabilities: content.capabilities,
4605
- declaredActions: content.serviceActions,
4880
+ declaredCapabilities,
4881
+ declaredActions,
4882
+ clientDeclared,
4606
4883
  createdAt: now.toISOString(),
4607
- expiresAt: new Date(now.getTime() + 3600 * 1e3).toISOString()
4884
+ expiresAt: new Date(now.getTime() + PANEL_APP_RUNTIME_TOKEN_TTL_MS).toISOString()
4608
4885
  };
4609
4886
  this.bridgeSessions.set(session.token, session);
4610
4887
  return session;
4611
4888
  };
4889
+ createPanelAppBridgeSession = async (params) => {
4890
+ const resolved = await readPanelAppContentSourceByIdOrAppId({
4891
+ appIdOrSourceId: params.id,
4892
+ createAssetBaseHref: this.createAssetBaseHref,
4893
+ panelsPath: this.getPanelsPath(this.getWorkspacePath()),
4894
+ sourceService: this.sourceService
4895
+ });
4896
+ return this.createPanelAppRuntimeTokenSession({
4897
+ appId: resolved.appId,
4898
+ clientDeclared: resolved.manifest.client,
4899
+ declaredActions: resolved.manifest.serviceActions,
4900
+ declaredCapabilities: resolved.manifest.capabilities
4901
+ });
4902
+ };
4903
+ grantPanelAppClient = async (appId) => {
4904
+ await assertPanelAppDeclaresClient({
4905
+ appId,
4906
+ panelsPath: this.getPanelsPath(this.getWorkspacePath()),
4907
+ sourceService: this.sourceService
4908
+ });
4909
+ return await this.createClientGrantStore().grant({
4910
+ appId,
4911
+ grantedAt: (/* @__PURE__ */ new Date()).toISOString()
4912
+ });
4913
+ };
4914
+ revokePanelAppClient = async (appId) => {
4915
+ await this.createClientGrantStore().revoke(appId);
4916
+ };
4612
4917
  resolvePanelAppBridgeSession = (token) => {
4613
4918
  this.deleteExpiredBridgeSessions();
4614
4919
  const session = this.bridgeSessions.get(token.trim());
@@ -4620,39 +4925,15 @@ var PanelAppManager = class {
4620
4925
  };
4621
4926
  sendAgentMessage = async (bridgeSessionToken, payload) => {
4622
4927
  const bridgeSession = this.resolvePanelAppBridgeSession(bridgeSessionToken);
4623
- await this.assertAgentCapabilityGranted(bridgeSession, "agent:send");
4624
- return await this.requireAgentRunClient().send(withPanelAppAgentMetadata(payload, bridgeSession));
4928
+ return await this.agentBridgeService.sendAgentMessage(bridgeSession, payload);
4625
4929
  };
4626
4930
  generateAgentObject = async (bridgeSessionToken, input) => {
4627
4931
  const bridgeSession = this.resolvePanelAppBridgeSession(bridgeSessionToken);
4628
- await this.assertAgentCapabilityGranted(bridgeSession, "agent:generateObject");
4629
- const request = normalizePanelAppGenerateObjectInput(input);
4630
- const message = createPanelAppGenerateObjectMessage({
4631
- bridgeSession,
4632
- request,
4633
- requestId: randomUUID()
4634
- });
4635
- return { result: await waitForPanelAppStructuredResult(this.requireAgentRunClient(), {
4636
- payload: {
4637
- message,
4638
- metadata: {
4639
- ...createPanelAppAgentMetadata(bridgeSession),
4640
- panel_app_peer_id: request.peerId
4641
- },
4642
- peerId: request.peerId
4643
- },
4644
- timeoutMs: request.timeoutMs
4645
- }) };
4932
+ return await this.agentBridgeService.generateAgentObject(bridgeSession, input);
4646
4933
  };
4647
4934
  grantAgentCapability = async (bridgeSessionToken, capability) => {
4648
4935
  const bridgeSession = this.resolvePanelAppBridgeSession(bridgeSessionToken);
4649
- if (!isPanelAppAgentCapability(capability)) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "unknown panel app agent capability");
4650
- this.assertDeclaredCapability(bridgeSession, capability);
4651
- return await this.createCapabilityGrantStore().grant({
4652
- caller: bridgeSession.caller,
4653
- capability,
4654
- grantedAt: (/* @__PURE__ */ new Date()).toISOString()
4655
- });
4936
+ return await this.agentBridgeService.grantAgentCapability(bridgeSession, capability);
4656
4937
  };
4657
4938
  updatePanelAppPreferences = async (id, preferences) => {
4658
4939
  const fileName = await this.resolvePanelAppFileName(id);
@@ -4670,42 +4951,21 @@ var PanelAppManager = class {
4670
4951
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
4671
4952
  const source = await this.sourceService.resolveSource(panelsPath, id);
4672
4953
  const panelAppId = encodePanelAppId(source.sourceName);
4954
+ const appId = resolvePanelAppAppId(source, source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8")));
4673
4955
  await rm(source.sourcePath, { recursive: source.kind === "folder" });
4674
4956
  await this.createStateStore(panelsPath).deleteEntry(panelAppId);
4675
4957
  await this.createCapabilityGrantStore().deleteCaller({
4676
4958
  surface: "panel-app",
4677
- appId: panelAppId
4959
+ appId
4678
4960
  });
4679
- this.deleteBridgeSessionsByPanelAppId(panelAppId);
4961
+ await this.createClientGrantStore().revoke(appId);
4962
+ this.deleteBridgeSessionsByPanelAppId(appId);
4680
4963
  return {
4681
4964
  deleted: true,
4682
4965
  fileName: source.sourceName,
4683
4966
  id: panelAppId
4684
4967
  };
4685
4968
  };
4686
- assertAgentCapabilityGranted = async (bridgeSession, capability) => {
4687
- this.assertDeclaredCapability(bridgeSession, capability);
4688
- if (!await this.createCapabilityGrantStore().isGranted(bridgeSession.caller, capability)) throw new PanelAppError("AUTHORIZATION_REQUIRED", `This panel app needs permission to use ${capability}.`);
4689
- };
4690
- assertDeclaredCapability = (bridgeSession, capability) => {
4691
- if (!bridgeSession.declaredCapabilities.includes(capability)) throw new PanelAppError("PANEL_APP_CAPABILITY_NOT_DECLARED", this.describeMissingAgentCapability(bridgeSession.declaredCapabilities, capability));
4692
- };
4693
- describeMissingAgentCapability = (declaredCapabilities, capability) => {
4694
- const declared = declaredCapabilities.length > 0 ? declaredCapabilities.join(", ") : "none";
4695
- const valid = PANEL_APP_AGENT_CAPABILITIES.join(", ");
4696
- const hint = declaredCapabilities.includes(capability.replace(":", ".")) ? ` Use ${capability}, not ${capability.replace(":", ".")}.` : "";
4697
- return [
4698
- `panel app did not declare ${capability}.`,
4699
- `Declared: ${declared}.`,
4700
- `Valid capabilities: ${valid}.`,
4701
- `Declare it with nextclaw-panel-capabilities or panel-app.json capabilities.`,
4702
- hint.trim()
4703
- ].filter(Boolean).join(" ");
4704
- };
4705
- requireAgentRunClient = () => {
4706
- if (!this.agentRunClient) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "panel app agent client is not configured");
4707
- return this.agentRunClient;
4708
- };
4709
4969
  getWorkspacePath = () => getWorkspacePathFromConfig(this.params.configManager.config);
4710
4970
  getPanelsPath = (workspacePath) => join(workspacePath, DEFAULT_PANELS_DIR);
4711
4971
  createAssetBaseHref = (source) => {
@@ -4717,13 +4977,16 @@ var PanelAppManager = class {
4717
4977
  };
4718
4978
  createStateStore = (panelsPath) => new PanelAppStateStore(panelsPath);
4719
4979
  createCapabilityGrantStore = () => new PanelAppCapabilityGrantStore(join(this.getPanelsPath(this.getWorkspacePath()), PANEL_APP_CAPABILITY_GRANTS_FILE_NAME));
4980
+ createClientGrantStore = () => new PanelAppClientGrantStore(join(this.getPanelsPath(this.getWorkspacePath()), PANEL_APP_CLIENT_GRANTS_FILE_NAME));
4720
4981
  buildPanelAppEntry = async (source, state) => {
4721
4982
  const manifest = source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8"));
4722
4983
  const id = encodePanelAppId(source.sourceName);
4984
+ const appId = resolvePanelAppAppId(source, manifest);
4723
4985
  const createdAt = resolvePanelAppCreatedAt(source.sourceStat);
4724
4986
  const updatedAt = source.sourceStat.mtime.toISOString();
4725
4987
  const entry = {
4726
4988
  id,
4989
+ appId,
4727
4990
  fileName: source.sourceName,
4728
4991
  kind: source.kind,
4729
4992
  title: manifest.title ?? toPanelAppTitle(source.sourceName),
@@ -4732,6 +4995,8 @@ var PanelAppManager = class {
4732
4995
  updatedAt,
4733
4996
  sizeBytes: source.sourceStat.size,
4734
4997
  favorite: state.favorite ?? false,
4998
+ clientDeclared: manifest.client,
4999
+ clientGranted: await this.isPanelAppClientGranted(appId, manifest.client),
4735
5000
  openCount: state.openCount ?? 0
4736
5001
  };
4737
5002
  if (manifest.description) entry.description = manifest.description;
@@ -4748,7 +5013,11 @@ var PanelAppManager = class {
4748
5013
  for (const [token, session] of this.bridgeSessions) if (new Date(session.expiresAt).getTime() <= now) this.bridgeSessions.delete(token);
4749
5014
  };
4750
5015
  deleteBridgeSessionsByPanelAppId = (panelAppId) => {
4751
- for (const [token, session] of this.bridgeSessions) if (session.panelAppId === panelAppId) this.bridgeSessions.delete(token);
5016
+ for (const [token, session] of this.bridgeSessions) if (session.appId === panelAppId) this.bridgeSessions.delete(token);
5017
+ };
5018
+ isPanelAppClientGranted = async (appId, clientDeclared) => {
5019
+ if (!clientDeclared) return false;
5020
+ return await this.createClientGrantStore().isGranted(appId);
4752
5021
  };
4753
5022
  isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4754
5023
  };
@@ -4860,7 +5129,7 @@ var McpServiceAppRuntimeService = class {
4860
5129
  command: manifest.command,
4861
5130
  args: manifest.args,
4862
5131
  cwd: app.dirPath,
4863
- env: {},
5132
+ env: createRuntimeChildEnv(process.env),
4864
5133
  stderr: "pipe"
4865
5134
  },
4866
5135
  scope: {
@@ -5139,7 +5408,7 @@ var ServiceAppManager = class {
5139
5408
  return record;
5140
5409
  };
5141
5410
  listServiceActions = async (params = {}) => {
5142
- const actions = (params.appId ? [await this.requireServiceApp(params.appId)] : await this.listValidServiceApps()).flatMap(({ manifest, record }) => this.listManifestActions(record, manifest));
5411
+ const actions = (params.appId ? [await this.requireServiceApp(params.appId)] : await this.listValidServiceApps()).flatMap(({ manifest, record }) => listServiceAppManifestActions(record, manifest));
5143
5412
  return await Promise.all(actions.map(async (action) => await this.withGrantState(action, params)));
5144
5413
  };
5145
5414
  discoverServiceAppActions = async (appId) => {
@@ -5234,7 +5503,7 @@ var ServiceAppManager = class {
5234
5503
  };
5235
5504
  requireServiceAction = async (actionId) => {
5236
5505
  const { manifest, record } = await this.requireServiceAppForAction(actionId);
5237
- const action = this.listManifestActions(record, manifest).find((entry) => entry.id === actionId);
5506
+ const action = listServiceAppManifestActions(record, manifest).find((entry) => entry.id === actionId);
5238
5507
  if (!action) throw new ServiceAppError("SERVICE_APP_ACTION_NOT_FOUND", "service action not found");
5239
5508
  return action;
5240
5509
  };
@@ -5243,7 +5512,6 @@ var ServiceAppManager = class {
5243
5512
  if (!appId) throw new ServiceAppError("SERVICE_APP_INVALID_ACTION", "service action id is invalid");
5244
5513
  return await this.requireServiceApp(appId);
5245
5514
  };
5246
- listManifestActions = (record, manifest) => listServiceAppManifestActions(record, manifest);
5247
5515
  requireServiceApp = async (appId) => {
5248
5516
  const dirPath = join(this.getServiceAppsPath(this.getWorkspacePath()), appId);
5249
5517
  try {
@@ -5299,9 +5567,10 @@ var ServiceAppManager = class {
5299
5567
  };
5300
5568
  toServiceAppRecord = (dirPath, manifest) => {
5301
5569
  const runtimeStatus = this.runtimeService.getStatus(manifest.id);
5302
- const record = {
5570
+ return {
5303
5571
  id: manifest.id,
5304
5572
  title: manifest.title,
5573
+ description: manifest.description,
5305
5574
  dirPath,
5306
5575
  manifestPath: getServiceAppManifestPath(dirPath),
5307
5576
  command: manifest.command,
@@ -5309,14 +5578,12 @@ var ServiceAppManager = class {
5309
5578
  cwd: dirPath,
5310
5579
  enabled: manifest.enabled,
5311
5580
  protocol: manifest.protocol,
5312
- status: manifest.enabled ? runtimeStatus.status : "stopped"
5581
+ status: manifest.enabled ? runtimeStatus.status : "stopped",
5582
+ lastError: runtimeStatus.lastError,
5583
+ lastStartedAt: runtimeStatus.lastStartedAt,
5584
+ lastReadyAt: runtimeStatus.lastReadyAt,
5585
+ lastFailedAt: runtimeStatus.lastFailedAt
5313
5586
  };
5314
- if (manifest.description) record.description = manifest.description;
5315
- if (runtimeStatus.lastError) record.lastError = runtimeStatus.lastError;
5316
- if (runtimeStatus.lastStartedAt) record.lastStartedAt = runtimeStatus.lastStartedAt;
5317
- if (runtimeStatus.lastReadyAt) record.lastReadyAt = runtimeStatus.lastReadyAt;
5318
- if (runtimeStatus.lastFailedAt) record.lastFailedAt = runtimeStatus.lastFailedAt;
5319
- return record;
5320
5587
  };
5321
5588
  assertCaller = (caller) => {
5322
5589
  if (caller.surface !== "panel-app" || !caller.appId.trim()) throw new ServiceAppError("SERVICE_APP_INVALID_CALLER", "service action caller is invalid");
@@ -5336,7 +5603,7 @@ var ServiceAppManager = class {
5336
5603
  throw new ServiceAppError("SERVICE_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
5337
5604
  }
5338
5605
  };
5339
- isMissingFileError = (error) => Boolean(error) && typeof error === "object" && error.code === "ENOENT";
5606
+ isMissingFileError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
5340
5607
  };
5341
5608
  function toTitle(value) {
5342
5609
  return basename(value).replace(/[-_]+/g, " ").trim() || value;
@@ -5351,7 +5618,6 @@ var MessageInbox = class {
5351
5618
  drain = () => {
5352
5619
  return this.messages.splice(0, this.messages.length);
5353
5620
  };
5354
- isEmpty = () => this.messages.length === 0;
5355
5621
  };
5356
5622
  function isConversationStateEvent(event) {
5357
5623
  return event.type !== NcpEventType.ContextWindowUpdated;
@@ -5359,10 +5625,10 @@ function isConversationStateEvent(event) {
5359
5625
  var SessionRun = class {
5360
5626
  inbox = new MessageInbox();
5361
5627
  sessionId;
5628
+ statusListeners = /* @__PURE__ */ new Set();
5362
5629
  activeRunId = null;
5363
5630
  activeRunController = null;
5364
- constructor(seed, eventBus, stateManager = new DefaultNcpAgentConversationStateManager()) {
5365
- this.eventBus = eventBus;
5631
+ constructor(seed, stateManager = new DefaultNcpAgentConversationStateManager()) {
5366
5632
  this.stateManager = stateManager;
5367
5633
  this.sessionId = seed.sessionId;
5368
5634
  this.stateManager.hydrate({
@@ -5379,19 +5645,20 @@ var SessionRun = class {
5379
5645
  const conversationEvents = events.filter(isConversationStateEvent);
5380
5646
  if (conversationEvents.length > 0) await this.stateManager.dispatchBatch(conversationEvents);
5381
5647
  };
5382
- applyAndPublishEvents = async (events, meta) => {
5383
- await this.applyEvents(events);
5384
- for (const event of events) this.eventBus?.emit(eventKeys.ncpEvent, event, {
5385
- emittedAt: meta.emittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5386
- source: meta.source
5387
- });
5648
+ onStatusChange = (listener) => {
5649
+ this.statusListeners.add(listener);
5650
+ return () => {
5651
+ this.statusListeners.delete(listener);
5652
+ };
5388
5653
  };
5389
5654
  beginRun = () => {
5390
5655
  if (this.activeRunId) throw new Error(`Session ${this.sessionId} already has an active run.`);
5656
+ const wasRunning = this.isRunning();
5391
5657
  const runId = `agent-run-${randomUUID()}`;
5392
5658
  const controller = new AbortController();
5393
5659
  this.activeRunId = runId;
5394
5660
  this.activeRunController = controller;
5661
+ this.emitStatusChangeIfNeeded(wasRunning);
5395
5662
  return {
5396
5663
  runId,
5397
5664
  signal: controller.signal
@@ -5400,30 +5667,43 @@ var SessionRun = class {
5400
5667
  abortRun = (runId) => {
5401
5668
  if (!this.activeRunId || !this.activeRunController) return false;
5402
5669
  if (runId && this.activeRunId !== runId) return false;
5670
+ const wasRunning = this.isRunning();
5403
5671
  this.activeRunController.abort();
5672
+ this.activeRunController = null;
5673
+ this.activeRunId = null;
5674
+ this.emitStatusChangeIfNeeded(wasRunning);
5404
5675
  return true;
5405
5676
  };
5406
5677
  isRunning = () => this.activeRunId !== null;
5407
5678
  dispose = () => {
5679
+ const wasRunning = this.isRunning();
5408
5680
  this.activeRunController?.abort();
5409
5681
  this.activeRunController = null;
5410
5682
  this.activeRunId = null;
5683
+ this.emitStatusChangeIfNeeded(wasRunning);
5684
+ this.statusListeners.clear();
5411
5685
  };
5412
5686
  applyRunEvents = (events) => {
5687
+ const wasRunning = this.isRunning();
5413
5688
  for (const event of events) {
5414
5689
  if (event.type === NcpEventType.RunStarted && event.payload.runId) this.activeRunId = event.payload.runId;
5415
- if (event.type === NcpEventType.MessageAbort || (event.type === NcpEventType.RunFinished || event.type === NcpEventType.RunError) && (!event.payload.runId || event.payload.runId === this.activeRunId)) {
5690
+ if ((event.type === NcpEventType.RunFinished || event.type === NcpEventType.RunError) && (!event.payload.runId || event.payload.runId === this.activeRunId)) {
5416
5691
  this.activeRunId = null;
5417
5692
  this.activeRunController = null;
5418
5693
  }
5419
5694
  }
5695
+ this.emitStatusChangeIfNeeded(wasRunning);
5696
+ };
5697
+ emitStatusChangeIfNeeded = (wasRunning) => {
5698
+ const running = this.isRunning();
5699
+ if (running === wasRunning) return;
5700
+ for (const listener of [...this.statusListeners]) listener(running ? "running" : "idle");
5420
5701
  };
5421
5702
  };
5422
5703
  var SessionRunManager = class {
5423
5704
  runs = /* @__PURE__ */ new Map();
5424
- constructor(sessionManager, eventBus) {
5705
+ constructor(sessionManager) {
5425
5706
  this.sessionManager = sessionManager;
5426
- this.eventBus = eventBus;
5427
5707
  }
5428
5708
  getSessionRun = (sessionId) => this.runs.get(sessionId) ?? null;
5429
5709
  isSessionRunning = (sessionId) => this.runs.get(sessionId.trim())?.isRunning() ?? false;
@@ -5432,7 +5712,7 @@ var SessionRunManager = class {
5432
5712
  const run = new SessionRun({
5433
5713
  messages: await this.sessionManager.listSessionMessages(sessionId),
5434
5714
  sessionId
5435
- }, this.eventBus);
5715
+ });
5436
5716
  this.runs.set(sessionId, run);
5437
5717
  return run;
5438
5718
  };
@@ -5684,7 +5964,7 @@ var NcpAgentSessionMetadataStore = class {
5684
5964
  read = async (sessionId, activitySnapshot) => {
5685
5965
  try {
5686
5966
  const parsed = JSON.parse(await readFile(this.metadataPath(sessionId), "utf-8"));
5687
- if (!isRecord$9(parsed) || parsed._type !== "metadata" || !isRecord$9(parsed.metadata)) throw new Error(`invalid ncp agent session metadata sidecar: ${sessionId}`);
5967
+ if (!isRecord$10(parsed) || parsed._type !== "metadata" || !isRecord$10(parsed.metadata)) throw new Error(`invalid ncp agent session metadata sidecar: ${sessionId}`);
5688
5968
  const createdAt = toIsoString(parsed.created_at, activitySnapshot.createdAt);
5689
5969
  const agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
5690
5970
  return {
@@ -5785,7 +6065,7 @@ var NcpAgentSessionSummaryIndexStore = class {
5785
6065
  function serializeJournalEntry(entry) {
5786
6066
  const serialized = JSON.stringify(entry);
5787
6067
  if (!serialized) throw new Error("ncp agent session journal entry serialization produced empty output");
5788
- if (!isRecord$9(JSON.parse(serialized))) throw new Error("ncp agent session journal entry serialization produced a non-object entry");
6068
+ if (!isRecord$10(JSON.parse(serialized))) throw new Error("ncp agent session journal entry serialization produced a non-object entry");
5789
6069
  return serialized;
5790
6070
  }
5791
6071
  var NcpAgentSessionJournalStore = class {
@@ -6001,15 +6281,15 @@ var NcpAgentSessionJournalStore = class {
6001
6281
  console.warn(`[ncp-agent-session-journal] skipped corrupted journal line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
6002
6282
  continue;
6003
6283
  }
6004
- if (!isRecord$9(parsed)) continue;
6284
+ if (!isRecord$10(parsed)) continue;
6005
6285
  if (parsed._type === "metadata") {
6006
- metadata = isRecord$9(parsed.metadata) ? structuredClone(parsed.metadata) : {};
6286
+ metadata = isRecord$10(parsed.metadata) ? structuredClone(parsed.metadata) : {};
6007
6287
  agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
6008
6288
  createdAt = toIsoString(parsed.created_at, createdAt);
6009
6289
  updatedAt = toIsoString(parsed.updated_at, updatedAt);
6010
6290
  continue;
6011
6291
  }
6012
- if (parsed._type === "event" && isRecord$9(parsed.event)) {
6292
+ if (parsed._type === "event" && isRecord$10(parsed.event)) {
6013
6293
  const seq = Number(parsed.seq);
6014
6294
  nextSeq = Math.max(nextSeq, Number.isFinite(seq) ? Math.trunc(seq) + 1 : nextSeq);
6015
6295
  updatedAt = toIsoString(parsed.timestamp, updatedAt);
@@ -7785,11 +8065,19 @@ function formatErrorStatus(error) {
7785
8065
  }
7786
8066
  return "运行出错";
7787
8067
  }
7788
- function createSessionActivityPreviewFromNcpEvent(event, timestamp) {
8068
+ function readToolCallId(value) {
8069
+ if (typeof value !== "string") return null;
8070
+ const trimmed = value.trim();
8071
+ return trimmed.length > 0 ? trimmed : null;
8072
+ }
8073
+ function formatToolDoneStatus(toolName) {
8074
+ return toolName ? `工具调用完成:${toolName}` : "工具调用完成";
8075
+ }
8076
+ function createSessionActivityPreviewFromNcpEvent(event, timestamp, options = {}) {
7789
8077
  switch (event.type) {
7790
8078
  case NcpEventType.RunStarted: return createProjection(readSessionId(event.payload.sessionId), {
7791
8079
  state: "running",
7792
- statusText: "正在处理...",
8080
+ statusText: "正在思考",
7793
8081
  timestamp
7794
8082
  });
7795
8083
  case NcpEventType.RunFinished: return createProjection(readSessionId(event.payload.sessionId), {
@@ -7830,11 +8118,15 @@ function createSessionActivityPreviewFromNcpEvent(event, timestamp) {
7830
8118
  timestamp
7831
8119
  });
7832
8120
  case NcpEventType.MessageToolCallEnd:
7833
- case NcpEventType.MessageToolCallResult: return createProjection(readSessionId(event.payload.sessionId), {
7834
- state: "running",
7835
- statusText: "工具调用完成",
7836
- timestamp
7837
- });
8121
+ case NcpEventType.MessageToolCallResult: {
8122
+ const sessionId = readSessionId(event.payload.sessionId);
8123
+ const toolCallId = readToolCallId(event.payload.toolCallId);
8124
+ return createProjection(sessionId, {
8125
+ state: "running",
8126
+ statusText: formatToolDoneStatus(sessionId && toolCallId ? options.readToolName?.(sessionId, toolCallId) ?? null : null),
8127
+ timestamp
8128
+ });
8129
+ }
7838
8130
  default: return null;
7839
8131
  }
7840
8132
  }
@@ -7902,13 +8194,16 @@ function writeSessionActivityPreviewMetadata(metadata, projection) {
7902
8194
  var SessionActivityPreviewContribution = class {
7903
8195
  unsubscribeNcpEvent = null;
7904
8196
  metadataWriteChains = /* @__PURE__ */ new Map();
8197
+ toolNames = /* @__PURE__ */ new Map();
7905
8198
  constructor(kernel) {
7906
8199
  this.kernel = kernel;
7907
8200
  }
7908
8201
  start = () => {
7909
8202
  if (this.unsubscribeNcpEvent) return;
7910
8203
  this.unsubscribeNcpEvent = this.kernel.eventBus.on(eventKeys.ncpEvent, (event) => {
7911
- const projection = createSessionActivityPreviewFromNcpEvent(event, (/* @__PURE__ */ new Date()).toISOString());
8204
+ this.rememberToolName(event);
8205
+ const projection = createSessionActivityPreviewFromNcpEvent(event, (/* @__PURE__ */ new Date()).toISOString(), { readToolName: this.readToolName });
8206
+ this.clearFinishedRunToolNames(event);
7912
8207
  if (!projection) return;
7913
8208
  this.updatePreview(projection);
7914
8209
  });
@@ -7917,7 +8212,29 @@ var SessionActivityPreviewContribution = class {
7917
8212
  this.unsubscribeNcpEvent?.();
7918
8213
  this.unsubscribeNcpEvent = null;
7919
8214
  this.metadataWriteChains.clear();
8215
+ this.toolNames.clear();
8216
+ };
8217
+ rememberToolName = (event) => {
8218
+ if (event.type !== NcpEventType.MessageToolCallStart) return;
8219
+ const sessionId = event.payload.sessionId.trim();
8220
+ const toolCallId = event.payload.toolCallId.trim();
8221
+ const toolName = event.payload.toolName.trim();
8222
+ if (!sessionId || !toolCallId || !toolName) return;
8223
+ this.toolNames.set(this.createToolNameKey(sessionId, toolCallId), toolName);
8224
+ };
8225
+ readToolName = (sessionId, toolCallId) => this.toolNames.get(this.createToolNameKey(sessionId, toolCallId)) ?? null;
8226
+ clearFinishedRunToolNames = (event) => {
8227
+ if (event.type !== NcpEventType.RunFinished && event.type !== NcpEventType.RunError) return;
8228
+ const sessionId = this.readNonEmptyString(event.payload.sessionId);
8229
+ if (!sessionId) return;
8230
+ for (const key of this.toolNames.keys()) if (key.startsWith(`${sessionId}:`)) this.toolNames.delete(key);
8231
+ };
8232
+ readNonEmptyString = (value) => {
8233
+ if (typeof value !== "string") return null;
8234
+ const trimmed = value.trim();
8235
+ return trimmed.length > 0 ? trimmed : null;
7920
8236
  };
8237
+ createToolNameKey = (sessionId, toolCallId) => `${sessionId}:${toolCallId}`;
7921
8238
  updatePreview = (projection) => {
7922
8239
  const next = (this.metadataWriteChains.get(projection.sessionId) ?? Promise.resolve()).then(() => this.updatePreviewMetadata(projection));
7923
8240
  this.metadataWriteChains.set(projection.sessionId, next.catch(() => void 0));
@@ -8630,7 +8947,7 @@ var NextclawKernel = class {
8630
8947
  })
8631
8948
  });
8632
8949
  this.contextCompactionManager = new AgentRunContextCompactionManager(this.configManager, this.llmProviders, this.sessionManager);
8633
- this.sessionRunManager = new SessionRunManager(this.sessionManager, this.eventBus);
8950
+ this.sessionRunManager = new SessionRunManager(this.sessionManager);
8634
8951
  this.agentRunRequestManager = new AgentRunRequestManager(this.agentRuntimeManager, this.configManager, this.contextProviderManager, this.eventBus, this.ingress, this.sessionManager, this.sessionRunManager, this.toolProviderManager);
8635
8952
  this.contributions = [
8636
8953
  new ToolProviderContribution(this),