@nextclaw/kernel 0.3.2 → 0.3.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -627,16 +627,6 @@ function readOptionalString$9(value) {
627
627
  if (typeof value !== "string") return;
628
628
  return value.trim() || void 0;
629
629
  }
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
630
  function toRunHandle(accepted) {
641
631
  return {
642
632
  sessionId: accepted.sessionId,
@@ -684,7 +674,14 @@ var AgentRunRequestManager = class {
684
674
  };
685
675
  handleSessionMessageRequest = async (envelope) => {
686
676
  if (!envelope.payload) throw new Error("Invalid agent run session message request.");
687
- return toRunHandle(await this.send(toSessionMessageRequest(envelope.payload)));
677
+ return toRunHandle(await this.send({
678
+ sessionId: envelope.payload.sessionId,
679
+ message: {
680
+ ...envelope.payload.message,
681
+ sessionId: envelope.payload.sessionId
682
+ },
683
+ correlationId: envelope.payload.requestId
684
+ }));
688
685
  };
689
686
  send = async (request) => {
690
687
  const session = await this.sessionManager.getOrCreateAgentRunSession({
@@ -710,7 +707,26 @@ var AgentRunRequestManager = class {
710
707
  message
711
708
  };
712
709
  sessionRun.inbox.enqueue(message);
713
- const activeRun = sessionRun.beginRun();
710
+ let stopPublishingRunStatus = () => void 0;
711
+ stopPublishingRunStatus = sessionRun.onStatusChange((status) => {
712
+ this.eventBus.emit(eventKeys.sessionRunStatus, {
713
+ sessionKey: sessionRun.sessionId,
714
+ status
715
+ }, {
716
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
717
+ source: "agent-run-request"
718
+ });
719
+ if (status === "idle") stopPublishingRunStatus();
720
+ });
721
+ this.cleanups.push(stopPublishingRunStatus);
722
+ const activeRun = (() => {
723
+ try {
724
+ return sessionRun.beginRun();
725
+ } catch (error) {
726
+ stopPublishingRunStatus();
727
+ throw error;
728
+ }
729
+ })();
714
730
  const model = request.model ?? session.model ?? this.configManager.getDefaultModel();
715
731
  const agentId = request.agentId ?? session.agentId ?? resolveDefaultAgentProfileId(this.configManager.loadConfig());
716
732
  const spec = {
@@ -2511,11 +2527,11 @@ var ExtensionManager = class {
2511
2527
  //#endregion
2512
2528
  //#region src/utils/model-message-vision.utils.ts
2513
2529
  const IMAGE_OMITTED_TEXT = "[Image omitted: the selected model is not configured for vision input.]";
2514
- function isRecord$10(value) {
2530
+ function isRecord$11(value) {
2515
2531
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2516
2532
  }
2517
2533
  function isImageContentPart(value) {
2518
- if (!isRecord$10(value)) return false;
2534
+ if (!isRecord$11(value)) return false;
2519
2535
  const type = value.type;
2520
2536
  return type === "image_url" || type === "input_image";
2521
2537
  }
@@ -2531,7 +2547,7 @@ function normalizeContentWithoutVision(content) {
2531
2547
  };
2532
2548
  });
2533
2549
  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);
2550
+ const textParts = parts.filter((part) => isRecord$11(part) && part.type === "text" && typeof part.text === "string").map((part) => part.text.trim()).filter(Boolean);
2535
2551
  if (textParts.length === parts.length) return textParts.join("\n\n");
2536
2552
  return parts;
2537
2553
  }
@@ -3070,7 +3086,7 @@ function safeNcpSessionFilename(value) {
3070
3086
  function normalizeNcpAgentId(agentId) {
3071
3087
  return agentId?.trim().toLowerCase() || void 0;
3072
3088
  }
3073
- function isRecord$9(value) {
3089
+ function isRecord$10(value) {
3074
3090
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3075
3091
  }
3076
3092
  function toIsoString(value, fallback) {
@@ -3690,7 +3706,7 @@ var PanelAppStateStore = class {
3690
3706
  };
3691
3707
  //#endregion
3692
3708
  //#region src/stores/panel-app-capability-grant.store.ts
3693
- const EMPTY_GRANTS$1 = {
3709
+ const EMPTY_GRANTS$2 = {
3694
3710
  version: 1,
3695
3711
  grants: {}
3696
3712
  };
@@ -3725,9 +3741,9 @@ var PanelAppCapabilityGrantStore = class {
3725
3741
  };
3726
3742
  load = async () => {
3727
3743
  try {
3728
- return normalizeStoreData$1(JSON.parse(await readFile(this.filePath, "utf8")));
3744
+ return normalizeStoreData$2(JSON.parse(await readFile(this.filePath, "utf8")));
3729
3745
  } catch (error) {
3730
- if (isMissingFileError$2(error)) return structuredClone(EMPTY_GRANTS$1);
3746
+ if (isMissingFileError$3(error)) return structuredClone(EMPTY_GRANTS$2);
3731
3747
  throw error;
3732
3748
  }
3733
3749
  };
@@ -3736,13 +3752,13 @@ var PanelAppCapabilityGrantStore = class {
3736
3752
  await writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
3737
3753
  };
3738
3754
  };
3739
- function normalizeStoreData$1(value) {
3740
- if (!isRecord$8(value) || value.version !== 1 || !isRecord$8(value.grants)) return structuredClone(EMPTY_GRANTS$1);
3755
+ function normalizeStoreData$2(value) {
3756
+ if (!isRecord$9(value) || value.version !== 1 || !isRecord$9(value.grants)) return structuredClone(EMPTY_GRANTS$2);
3741
3757
  const grants = {};
3742
3758
  for (const [callerKey, callerValue] of Object.entries(value.grants)) {
3743
- if (!isRecord$8(callerValue) || !isRecord$8(callerValue.capabilities)) continue;
3759
+ if (!isRecord$9(callerValue) || !isRecord$9(callerValue.capabilities)) continue;
3744
3760
  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 };
3761
+ for (const [capability, grantValue] of Object.entries(callerValue.capabilities)) if (isRecord$9(grantValue) && typeof grantValue.grantedAt === "string") capabilities[capability] = { grantedAt: grantValue.grantedAt };
3746
3762
  grants[callerKey] = { capabilities };
3747
3763
  }
3748
3764
  return {
@@ -3753,11 +3769,65 @@ function normalizeStoreData$1(value) {
3753
3769
  function getCallerKey(caller) {
3754
3770
  return `${caller.surface}:${caller.appId}`;
3755
3771
  }
3772
+ function isRecord$9(value) {
3773
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3774
+ }
3775
+ function isMissingFileError$3(error) {
3776
+ return Boolean(error) && typeof error === "object" && error.code === "ENOENT";
3777
+ }
3778
+ //#endregion
3779
+ //#region src/stores/panel-app-client-grant.store.ts
3780
+ const EMPTY_GRANTS$1 = {
3781
+ grants: {},
3782
+ version: 1
3783
+ };
3784
+ var PanelAppClientGrantStore = class {
3785
+ constructor(filePath) {
3786
+ this.filePath = filePath;
3787
+ }
3788
+ isGranted = async (appId) => {
3789
+ const data = await this.load();
3790
+ return Boolean(data.grants[appId]);
3791
+ };
3792
+ grant = async (params) => {
3793
+ const data = await this.load();
3794
+ data.grants[params.appId] = { grantedAt: params.grantedAt };
3795
+ await this.save(data);
3796
+ return params;
3797
+ };
3798
+ revoke = async (appId) => {
3799
+ const data = await this.load();
3800
+ if (!data.grants[appId]) return;
3801
+ delete data.grants[appId];
3802
+ await this.save(data);
3803
+ };
3804
+ load = async () => {
3805
+ try {
3806
+ return normalizeStoreData$1(JSON.parse(await readFile(this.filePath, "utf8")));
3807
+ } catch (error) {
3808
+ if (isMissingFileError$2(error)) return structuredClone(EMPTY_GRANTS$1);
3809
+ throw error;
3810
+ }
3811
+ };
3812
+ save = async (data) => {
3813
+ await mkdir(dirname(this.filePath), { recursive: true });
3814
+ await writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
3815
+ };
3816
+ };
3817
+ function normalizeStoreData$1(value) {
3818
+ if (!isRecord$8(value) || value.version !== 1 || !isRecord$8(value.grants)) return structuredClone(EMPTY_GRANTS$1);
3819
+ const grants = {};
3820
+ for (const [appId, grant] of Object.entries(value.grants)) if (isRecord$8(grant) && typeof grant.grantedAt === "string") grants[appId] = { grantedAt: grant.grantedAt };
3821
+ return {
3822
+ grants,
3823
+ version: 1
3824
+ };
3825
+ }
3756
3826
  function isRecord$8(value) {
3757
3827
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3758
3828
  }
3759
3829
  function isMissingFileError$2(error) {
3760
- return Boolean(error) && typeof error === "object" && error.code === "ENOENT";
3830
+ return isRecord$8(error) && error.code === "ENOENT";
3761
3831
  }
3762
3832
  //#endregion
3763
3833
  //#region src/services/agent-run-client.service.ts
@@ -3953,162 +4023,460 @@ var AgentRunClient = class {
3953
4023
  };
3954
4024
  };
3955
4025
  //#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)}`;
4026
+ //#region src/tools/structured-result.tools.ts
4027
+ const STRUCTURED_RESULT_TOOL_NAME = "nextclaw_submit_result";
4028
+ var StructuredResultSubmitTool = class {
4029
+ name = STRUCTURED_RESULT_TOOL_NAME;
4030
+ description = "Submit the structured object result for this request.";
4031
+ constructor(contract) {
4032
+ this.contract = contract;
3965
4033
  }
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)}`);
4034
+ get parameters() {
4035
+ return this.contract.schema;
4078
4036
  }
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.");
4037
+ validateArgs = (args) => validateToolArgs(args, this.contract.schema);
4038
+ execute = async (args) => args;
4039
+ };
4040
+ //#endregion
4041
+ //#region src/utils/panel-app-agent.utils.ts
4042
+ const PANEL_APP_AGENT_DEFAULT_TIMEOUT_MS = 6e4;
4043
+ const PANEL_APP_AGENT_MAX_TIMEOUT_MS = 12e4;
4044
+ const PANEL_APP_AGENT_MAX_PROMPT_CHARS = 2e4;
4045
+ const PANEL_APP_AGENT_MAX_CONTEXT_CHARS = 8e4;
4046
+ function normalizePanelAppGenerateObjectInput(input) {
4047
+ const peerId = input.peerId.trim();
4048
+ const prompt = input.prompt.trim();
4049
+ if (!peerId || !prompt || !isRecord$7(input.schema)) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "invalid generateObject request");
4050
+ if (prompt.length > PANEL_APP_AGENT_MAX_PROMPT_CHARS) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "generateObject prompt is too large");
4051
+ 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
4052
  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")
4053
+ context: input.context,
4054
+ peerId,
4055
+ prompt,
4056
+ schema: input.schema,
4057
+ timeoutMs: normalizeTimeoutMs(input.timeoutMs),
4058
+ title: input.title?.trim() || void 0
4090
4059
  };
4091
4060
  }
4092
- function readPanelAppMeta(html) {
4061
+ function createPanelAppGenerateObjectMessage(params) {
4062
+ const { bridgeSession, request, requestId } = params;
4093
4063
  return {
4094
- ...readPanelAppMetaField(html, "title"),
4095
- ...readPanelAppMetaField(html, "description"),
4096
- ...readPanelAppMetaField(html, "icon")
4064
+ id: `panel-app-agent-message-${randomUUID()}`,
4065
+ metadata: {
4066
+ ...createPanelAppAgentMetadata(bridgeSession),
4067
+ panel_app_peer_id: request.peerId,
4068
+ structured_result: {
4069
+ request_id: requestId,
4070
+ schema: structuredClone(request.schema),
4071
+ tool_name: STRUCTURED_RESULT_TOOL_NAME
4072
+ }
4073
+ },
4074
+ parts: [{
4075
+ type: "text",
4076
+ text: buildGenerateObjectPrompt(bridgeSession, request)
4077
+ }],
4078
+ role: "user",
4079
+ status: "final",
4080
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4097
4081
  };
4098
4082
  }
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"));
4083
+ async function waitForPanelAppStructuredResult(agentRunClient, params) {
4084
+ const iterator = agentRunClient.sendAndStreamEvents(params.payload)[Symbol.asyncIterator]();
4085
+ let timeoutId;
4086
+ const timeout = new Promise((_, reject) => {
4087
+ timeoutId = setTimeout(() => reject(new PanelAppError("AGENT_OBJECT_RESULT_TIMEOUT", "agent object result timed out")), params.timeoutMs);
4088
+ });
4089
+ let resultToolCallId = null;
4090
+ try {
4091
+ while (true) {
4092
+ const next = await Promise.race([iterator.next(), timeout]);
4093
+ if (next.done) break;
4094
+ const result = readStructuredResultEvent(next.value, resultToolCallId);
4095
+ if (result.matchedToolCallId) resultToolCallId = result.matchedToolCallId;
4096
+ if (result.submitted) return result.content;
4097
+ throwIfTerminalError(next.value);
4098
+ }
4099
+ } finally {
4100
+ if (timeoutId) clearTimeout(timeoutId);
4101
+ await iterator.return?.(void 0);
4102
+ }
4103
+ throw new PanelAppError("AGENT_OBJECT_RESULT_NOT_SUBMITTED", "agent did not submit a structured object result");
4104
+ }
4105
+ function withPanelAppAgentMetadata(payload, bridgeSession) {
4106
+ const panelAppMetadata = createPanelAppAgentMetadata(bridgeSession);
4107
+ const peerId = readOptionalPeerId(payload.peerId);
4108
+ const sessionId = readOptionalSessionId(payload.sessionId);
4109
+ if (peerId && sessionId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent send request cannot include both sessionId and peerId");
4110
+ const metadata = {
4111
+ ...payload.metadata ?? {},
4112
+ ...panelAppMetadata
4113
+ };
4114
+ if (peerId) metadata.panel_app_peer_id = peerId;
4115
+ if (Array.isArray(payload.content)) return {
4116
+ content: structuredClone(payload.content),
4117
+ metadata,
4118
+ peerId,
4119
+ sessionId
4120
+ };
4121
+ if (!payload.message) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent send request is required");
4122
+ return {
4123
+ message: {
4124
+ ...structuredClone(payload.message),
4125
+ metadata: {
4126
+ ...payload.message.metadata ?? {},
4127
+ ...panelAppMetadata
4128
+ }
4129
+ },
4130
+ metadata,
4131
+ peerId,
4132
+ sessionId
4133
+ };
4134
+ }
4135
+ function createPanelAppAgentMetadata(bridgeSession) {
4136
+ return {
4137
+ agent_peer_scope: `panel-app:${bridgeSession.appId}`,
4138
+ panel_app_bridge_request_id: randomUUID(),
4139
+ panel_app_id: bridgeSession.appId,
4140
+ source_kind: "panel_app"
4141
+ };
4142
+ }
4143
+ function normalizeTimeoutMs(timeoutMs) {
4144
+ if (!Number.isFinite(timeoutMs) || typeof timeoutMs !== "number") return PANEL_APP_AGENT_DEFAULT_TIMEOUT_MS;
4145
+ return Math.min(PANEL_APP_AGENT_MAX_TIMEOUT_MS, Math.max(1e3, Math.trunc(timeoutMs)));
4146
+ }
4147
+ function readOptionalPeerId(value) {
4148
+ if (typeof value !== "string") return;
4149
+ const peerId = value.trim();
4150
+ if (!peerId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent peerId cannot be empty");
4151
+ return peerId;
4152
+ }
4153
+ function readOptionalSessionId(value) {
4154
+ if (typeof value !== "string") return;
4155
+ const sessionId = value.trim();
4156
+ if (!sessionId) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "agent sessionId cannot be empty");
4157
+ return sessionId;
4158
+ }
4159
+ function buildGenerateObjectPrompt(bridgeSession, request) {
4160
+ return [
4161
+ "Panel App generateObject request",
4162
+ "",
4163
+ `Panel App ID: ${bridgeSession.appId}`,
4164
+ `Peer ID: ${request.peerId}`,
4165
+ "",
4166
+ "Context JSON:",
4167
+ stringifyJsonValue(request.context ?? null),
4168
+ "",
4169
+ "Task:",
4170
+ request.prompt,
4171
+ "",
4172
+ "Result contract:",
4173
+ `Call the ${STRUCTURED_RESULT_TOOL_NAME} tool exactly once with an object matching the provided schema.`,
4174
+ "Do not use natural language as the result."
4175
+ ].join("\n");
4176
+ }
4177
+ function readStructuredResultEvent(event, resultToolCallId) {
4178
+ if (event.type === NcpEventType.MessageToolCallStart && event.payload.toolName === "nextclaw_submit_result") return {
4179
+ matchedToolCallId: event.payload.toolCallId,
4180
+ submitted: false
4181
+ };
4182
+ if (event.type !== NcpEventType.MessageToolCallResult || event.payload.toolCallId !== resultToolCallId) return { submitted: false };
4183
+ assertToolResultContent(event.payload.content);
4184
+ return {
4185
+ content: event.payload.content,
4186
+ submitted: true
4187
+ };
4188
+ }
4189
+ function assertToolResultContent(content) {
4190
+ if (!isRecord$7(content) || content.ok !== false || !isRecord$7(content.error)) return;
4191
+ if (content.error.code === "invalid_tool_arguments") throw new PanelAppError("AGENT_OBJECT_RESULT_SCHEMA_INVALID", "agent object result did not match the schema");
4192
+ throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", typeof content.error.message === "string" ? content.error.message : "agent object request failed");
4193
+ }
4194
+ function throwIfTerminalError(event) {
4195
+ if (event.type === NcpEventType.MessageFailed) throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", event.payload.error.message);
4196
+ if (event.type === NcpEventType.RunError) throw new PanelAppError("AGENT_OBJECT_REQUEST_FAILED", event.payload.error ?? "agent object request failed");
4197
+ if (event.type === NcpEventType.RunFinished) throw new PanelAppError("AGENT_OBJECT_RESULT_NOT_SUBMITTED", "agent did not submit a structured object result");
4198
+ }
4199
+ function stringifyJsonValue(value) {
4200
+ try {
4201
+ return JSON.stringify(value, null, 2);
4202
+ } catch {
4203
+ throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "value is not JSON serializable");
4204
+ }
4205
+ }
4206
+ function isRecord$7(value) {
4207
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4208
+ }
4209
+ //#endregion
4210
+ //#region src/services/panel-app-agent-bridge.service.ts
4211
+ var PanelAppAgentBridgeService = class {
4212
+ constructor(params) {
4213
+ this.params = params;
4214
+ }
4215
+ sendAgentMessage = async (bridgeSession, payload) => {
4216
+ await this.assertAgentCapabilityGranted(bridgeSession, "agent:send");
4217
+ return await this.requireAgentRunClient().send(withPanelAppAgentMetadata(payload, bridgeSession));
4218
+ };
4219
+ generateAgentObject = async (bridgeSession, input) => {
4220
+ await this.assertAgentCapabilityGranted(bridgeSession, "agent:generateObject");
4221
+ const request = normalizePanelAppGenerateObjectInput(input);
4222
+ const message = createPanelAppGenerateObjectMessage({
4223
+ bridgeSession,
4224
+ request,
4225
+ requestId: randomUUID()
4226
+ });
4227
+ return { result: await waitForPanelAppStructuredResult(this.requireAgentRunClient(), {
4228
+ payload: {
4229
+ message,
4230
+ metadata: {
4231
+ ...createPanelAppAgentMetadata(bridgeSession),
4232
+ panel_app_peer_id: request.peerId
4233
+ },
4234
+ peerId: request.peerId
4235
+ },
4236
+ timeoutMs: request.timeoutMs
4237
+ }) };
4238
+ };
4239
+ grantAgentCapability = async (bridgeSession, capability) => {
4240
+ if (!isPanelAppAgentCapability(capability)) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "unknown panel app agent capability");
4241
+ this.assertDeclaredCapability(bridgeSession, capability);
4242
+ return await this.params.createCapabilityGrantStore().grant({
4243
+ caller: bridgeSession.caller,
4244
+ capability,
4245
+ grantedAt: (/* @__PURE__ */ new Date()).toISOString()
4246
+ });
4247
+ };
4248
+ assertAgentCapabilityGranted = async (bridgeSession, capability) => {
4249
+ this.assertDeclaredCapability(bridgeSession, capability);
4250
+ if (!await this.params.createCapabilityGrantStore().isGranted(bridgeSession.caller, capability)) throw new PanelAppError("AUTHORIZATION_REQUIRED", `This panel app needs permission to use ${capability}.`);
4251
+ };
4252
+ assertDeclaredCapability = (bridgeSession, capability) => {
4253
+ if (!bridgeSession.declaredCapabilities.includes(capability)) throw new PanelAppError("PANEL_APP_CAPABILITY_NOT_DECLARED", this.describeMissingAgentCapability(bridgeSession.declaredCapabilities, capability));
4254
+ };
4255
+ describeMissingAgentCapability = (declaredCapabilities, capability) => {
4256
+ const declared = declaredCapabilities.length > 0 ? declaredCapabilities.join(", ") : "none";
4257
+ const valid = PANEL_APP_AGENT_CAPABILITIES.join(", ");
4258
+ const hint = declaredCapabilities.includes(capability.replace(":", ".")) ? ` Use ${capability}, not ${capability.replace(":", ".")}.` : "";
4259
+ return [
4260
+ `panel app did not declare ${capability}.`,
4261
+ `Declared: ${declared}.`,
4262
+ `Valid capabilities: ${valid}.`,
4263
+ `Declare it with nextclaw-panel-capabilities or panel-app.json capabilities.`,
4264
+ hint.trim()
4265
+ ].filter(Boolean).join(" ");
4266
+ };
4267
+ requireAgentRunClient = () => {
4268
+ if (!this.params.agentRunClient) throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "panel app agent client is not configured");
4269
+ return this.params.agentRunClient;
4270
+ };
4271
+ };
4272
+ //#endregion
4273
+ //#region src/utils/panel-app-bridge.utils.ts
4274
+ const PANEL_APP_BRIDGE_MARKER = "nextclaw:panel-app-service-actions:request";
4275
+ function injectPanelAppBridgeScript(html, params) {
4276
+ if (html.includes(PANEL_APP_BRIDGE_MARKER)) return html;
4277
+ const script = `<script>${getPanelAppBridgeScript(params)}<\/script>`;
4278
+ const headMatch = /<head(?:\s[^>]*)?>/i.exec(html);
4279
+ if (headMatch?.index !== void 0) {
4280
+ const insertAt = headMatch.index + headMatch[0].length;
4281
+ return `${html.slice(0, insertAt)}${script}${html.slice(insertAt)}`;
4282
+ }
4283
+ return `${script}${html}`;
4284
+ }
4285
+ function getPanelAppBridgeScript(params = {
4286
+ appId: "",
4287
+ runtimeToken: ""
4288
+ }) {
4289
+ return `
4290
+ (() => {
4291
+ const requestType = "nextclaw:panel-app-service-actions:request";
4292
+ const responseType = "nextclaw:panel-app-service-actions:response";
4293
+ const appId = ${JSON.stringify(params.appId)};
4294
+ const runtimeToken = ${JSON.stringify(params.runtimeToken)};
4295
+ const pending = new Map();
4296
+ let counter = 0;
4297
+
4298
+ function createRequestId() {
4299
+ counter += 1;
4300
+ return "panel-bridge-" + Date.now().toString(36) + "-" + counter.toString(36);
4301
+ }
4302
+
4303
+ function request(method, payload) {
4304
+ const requestId = createRequestId();
4305
+ return new Promise((resolve, reject) => {
4306
+ pending.set(requestId, { method, resolve, reject });
4307
+ window.parent.postMessage({ type: requestType, requestId, appId, runtimeToken, method, payload }, "*");
4308
+ });
4309
+ }
4310
+
4311
+ function unwrapServiceActionResult(result) {
4312
+ if (!result || typeof result !== "object") {
4313
+ return result;
4314
+ }
4315
+ if (Object.prototype.hasOwnProperty.call(result, "structuredContent") && result.structuredContent !== undefined) {
4316
+ return result.structuredContent;
4317
+ }
4318
+ const content = Array.isArray(result.content) ? result.content : undefined;
4319
+ if (content && content.length === 1 && content[0]?.type === "text" && typeof content[0].text === "string") {
4320
+ try {
4321
+ return JSON.parse(content[0].text);
4322
+ } catch {
4323
+ return content[0].text;
4324
+ }
4325
+ }
4326
+ return result;
4327
+ }
4328
+
4329
+ function resolveBridgeData(entry, data) {
4330
+ if (entry.method === "list") {
4331
+ return Array.isArray(data.data?.actions) ? data.data.actions : [];
4332
+ }
4333
+ if (entry.method === "invoke") {
4334
+ return unwrapServiceActionResult(data.data?.result);
4335
+ }
4336
+ if (entry.method === "agent.generateObject") {
4337
+ return data.data?.result;
4338
+ }
4339
+ return data.data;
4340
+ }
4341
+
4342
+ window.addEventListener("message", (event) => {
4343
+ const data = event.data;
4344
+ if (!data || data.type !== responseType || typeof data.requestId !== "string") {
4345
+ return;
4346
+ }
4347
+ const entry = pending.get(data.requestId);
4348
+ if (!entry) {
4349
+ return;
4350
+ }
4351
+ pending.delete(data.requestId);
4352
+ if (data.ok) {
4353
+ entry.resolve(resolveBridgeData(entry, data));
4354
+ return;
4355
+ }
4356
+ const error = new Error(data.error?.message || "NextClaw panel bridge request failed.");
4357
+ error.code = data.error?.code;
4358
+ error.details = data.error?.details;
4359
+ entry.reject(error);
4360
+ });
4361
+
4362
+ const existing = window.nextclaw && typeof window.nextclaw === "object" ? window.nextclaw : {};
4363
+ Object.defineProperty(window, "nextclaw", {
4364
+ configurable: true,
4365
+ value: {
4366
+ ...existing,
4367
+ serviceActions: {
4368
+ list: () => request("list", {}),
4369
+ invoke: (actionId, input) => request("invoke", { actionId, input }),
4370
+ requestGrant: (actionId) => request("requestGrant", { actionId }),
4371
+ revokeGrant: (actionId) => request("revokeGrant", { actionId })
4372
+ },
4373
+ agent: {
4374
+ send: (input) => request("agent.send", { request: input }),
4375
+ generateObject: (input) => request("agent.generateObject", { input })
4376
+ }
4377
+ }
4378
+ });
4379
+ })();
4380
+ `.trim();
4381
+ }
4382
+ //#endregion
4383
+ //#region src/utils/panel-app-client-injection.utils.ts
4384
+ const PANEL_APP_CLIENT_MARKER = "nextclaw:panel-app-client:init";
4385
+ const PANEL_APP_CLIENT_SDK_PATH = "/api/panel-app-client-sdk.js";
4386
+ function injectPanelAppClientScript(html, params) {
4387
+ if (html.includes(PANEL_APP_CLIENT_MARKER)) return html;
4388
+ const script = [`<script src="${PANEL_APP_CLIENT_SDK_PATH}"><\/script>`, `<script>${getPanelAppClientInitScript(params)}<\/script>`].join("");
4389
+ const headMatch = /<head(?:\s[^>]*)?>/i.exec(html);
4390
+ if (headMatch?.index !== void 0) {
4391
+ const insertAt = headMatch.index + headMatch[0].length;
4392
+ return `${html.slice(0, insertAt)}${script}${html.slice(insertAt)}`;
4393
+ }
4394
+ return `${script}${html}`;
4395
+ }
4396
+ function getPanelAppClientInitScript(params) {
4397
+ return `
4398
+ (() => {
4399
+ const marker = "${PANEL_APP_CLIENT_MARKER}";
4400
+ if (typeof window.NextClawClient !== "function") {
4401
+ console.error("[NextClaw] Panel App client SDK failed to load.");
4402
+ return;
4403
+ }
4404
+ const existing = window.nextclaw && typeof window.nextclaw === "object" ? window.nextclaw : {};
4405
+ const client = new window.NextClawClient({
4406
+ baseUrl: window.location.origin,
4407
+ headers: {
4408
+ "x-nextclaw-panel-bridge-session": ${JSON.stringify(params.runtimeToken)}
4409
+ }
4410
+ });
4411
+ Object.defineProperty(window, "nextclaw", {
4412
+ configurable: true,
4413
+ value: {
4414
+ ...existing,
4415
+ client
4416
+ }
4417
+ });
4418
+ Object.defineProperty(window.nextclaw, "__clientInitMarker", {
4419
+ configurable: true,
4420
+ enumerable: false,
4421
+ value: marker
4422
+ });
4423
+ })();
4424
+ `.trim();
4425
+ }
4426
+ //#endregion
4427
+ //#region src/utils/panel-app-manifest.utils.ts
4428
+ const PANEL_APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4429
+ function parsePanelAppManifest(html) {
4430
+ return {
4431
+ ...readHtmlTitle(html),
4432
+ ...readStandardIcon(html),
4433
+ ...readPanelAppMeta(html),
4434
+ capabilities: readPanelAppCapabilities(html),
4435
+ client: false,
4436
+ serviceActions: readPanelAppServiceActions(html)
4437
+ };
4438
+ }
4439
+ function parsePanelAppFolderManifest(raw) {
4440
+ let parsed;
4441
+ try {
4442
+ parsed = JSON.parse(raw);
4443
+ } catch (error) {
4444
+ throw new Error(`panel-app.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
4445
+ }
4446
+ if (!isRecord$6(parsed)) throw new Error("panel-app.json must contain an object.");
4447
+ const id = readOptionalString$6(parsed, "id");
4448
+ if (id && !PANEL_APP_ID_PATTERN.test(id)) throw new Error("panel app id must be kebab-case.");
4449
+ return {
4450
+ id,
4451
+ title: readRequiredString$3(parsed, "title"),
4452
+ description: readOptionalString$6(parsed, "description"),
4453
+ icon: readOptionalString$6(parsed, "icon"),
4454
+ entry: readRequiredString$3(parsed, "entry"),
4455
+ capabilities: readStringArray$1(parsed.capabilities, "capabilities"),
4456
+ client: readOptionalBoolean$2(parsed, "client"),
4457
+ serviceActions: readStringArray$1(parsed.actions, "actions")
4458
+ };
4459
+ }
4460
+ function readPanelAppMeta(html) {
4461
+ return {
4462
+ ...readPanelAppMetaField(html, "title"),
4463
+ ...readPanelAppMetaField(html, "description"),
4464
+ ...readPanelAppMetaField(html, "icon")
4465
+ };
4466
+ }
4467
+ function readPanelAppMetaField(html, field) {
4468
+ const content = readMetaContent(html, `nextclaw-panel-${field}`, field === "icon" ? "attribute" : "text");
4469
+ const manifest = {};
4470
+ if (content) manifest[field] = content;
4471
+ return manifest;
4472
+ }
4473
+ function readHtmlTitle(html) {
4474
+ const title = normalizeTextValue(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]);
4475
+ return title ? { title } : {};
4476
+ }
4477
+ function readStandardIcon(html) {
4478
+ const icon = readLinkHref(html, (relTokens) => relTokens.includes("icon"));
4479
+ const appleTouchIcon = readLinkHref(html, (relTokens) => relTokens.some((token) => token === "apple-touch-icon" || token === "apple-touch-icon-precomposed"));
4112
4480
  const href = normalizeIconHref(icon ?? appleTouchIcon);
4113
4481
  return href ? { icon: href } : {};
4114
4482
  }
@@ -4151,12 +4519,18 @@ function readRequiredString$3(record, key) {
4151
4519
  function readOptionalString$6(record, key) {
4152
4520
  return typeof record[key] === "string" && record[key].trim() ? record[key].trim() : void 0;
4153
4521
  }
4522
+ function readOptionalBoolean$2(record, key) {
4523
+ const value = record[key];
4524
+ if (value === void 0) return false;
4525
+ if (typeof value !== "boolean") throw new Error(`panel app ${key} must be a boolean.`);
4526
+ return value;
4527
+ }
4154
4528
  function readStringArray$1(value, key) {
4155
4529
  if (value === void 0) return [];
4156
4530
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`panel app ${key} must be a string array.`);
4157
4531
  return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
4158
4532
  }
4159
- function isRecord$7(value) {
4533
+ function isRecord$6(value) {
4160
4534
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4161
4535
  }
4162
4536
  function normalizeTextValue(value) {
@@ -4276,256 +4650,128 @@ var PanelAppSourceService = class {
4276
4650
  resolveSource = async (panelsPath, id) => {
4277
4651
  const sourceName = decodePanelAppId(id);
4278
4652
  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);
4653
+ return await this.readSource(panelsPath, sourceName);
4654
+ } catch (error) {
4655
+ if (isPanelAppError(error)) throw error;
4656
+ if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4657
+ throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4419
4658
  }
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
4659
  };
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
4660
+ getAsset = async (panelsPath, id, assetPath) => {
4661
+ const source = await this.resolveSource(panelsPath, id);
4662
+ if (source.kind !== "folder") throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4663
+ const filePath = resolvePanelAppRelativePath(source.sourcePath, assetPath);
4664
+ try {
4665
+ if (!(await stat(filePath)).isFile()) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4666
+ return {
4667
+ content: await readFile(filePath),
4668
+ contentType: resolvePanelAppAssetContentType(filePath)
4669
+ };
4670
+ } catch (error) {
4671
+ if (isPanelAppError(error)) throw error;
4672
+ if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app asset not found");
4673
+ throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4674
+ }
4441
4675
  };
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
4676
+ readSource = async (panelsPath, sourceName) => {
4677
+ const sourcePath = join(panelsPath, sourceName);
4678
+ const sourceStat = await stat(sourcePath);
4679
+ if (sourceStat.isFile() && isPanelAppFileName(sourceName)) return {
4680
+ kind: "single-file",
4681
+ sourceName,
4682
+ sourcePath,
4683
+ entryPath: sourcePath,
4684
+ sourceStat
4685
+ };
4686
+ if (sourceStat.isDirectory() && isPanelAppDirName(sourceName)) return await this.readFolderSource(sourcePath, sourceName, sourceStat);
4687
+ throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4454
4688
  };
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"
4689
+ readFolderSource = async (sourcePath, sourceName, sourceStat) => {
4690
+ try {
4691
+ const manifest = await readPanelAppFolderManifest(sourcePath, sourceName);
4692
+ const entryPath = resolvePanelAppRelativePath(sourcePath, manifest.entry);
4693
+ if (!(await stat(entryPath)).isFile()) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app entry not found");
4694
+ return {
4695
+ kind: "folder",
4696
+ sourceName,
4697
+ sourcePath,
4698
+ entryPath,
4699
+ manifest,
4700
+ sourceStat
4701
+ };
4702
+ } catch (error) {
4703
+ if (isPanelAppError(error)) throw error;
4704
+ if (isMissingFileError$1(error)) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4705
+ throw new PanelAppError("PANEL_APP_MANIFEST_INVALID", error instanceof Error ? error.message : String(error));
4706
+ }
4462
4707
  };
4708
+ };
4709
+ function isMissingFileError$1(error) {
4710
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4463
4711
  }
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)));
4467
- }
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;
4473
- }
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;
4712
+ //#endregion
4713
+ //#region src/utils/panel-app-time.utils.ts
4714
+ function resolvePanelAppCreatedAt(fileStat) {
4715
+ return fileStat.birthtimeMs > 0 ? fileStat.birthtime.toISOString() : fileStat.mtime.toISOString();
4479
4716
  }
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");
4717
+ function resolvePanelAppActivityMs(entry) {
4718
+ return Math.max(new Date(entry.lastOpenedAt ?? 0).getTime(), new Date(entry.createdAt).getTime(), new Date(entry.updatedAt).getTime());
4497
4719
  }
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
4502
- };
4503
- if (event.type !== NcpEventType.MessageToolCallResult || event.payload.toolCallId !== resultToolCallId) return { submitted: false };
4504
- assertToolResultContent(event.payload.content);
4720
+ //#endregion
4721
+ //#region src/utils/panel-app-content-source.utils.ts
4722
+ async function readPanelAppContentSource(params) {
4723
+ const { createAssetBaseHref, id, panelsPath, sourceService } = params;
4724
+ const source = await sourceService.resolveSource(panelsPath, id);
4725
+ const html = await readFile(source.entryPath, "utf8");
4726
+ const manifest = source.manifest ?? parsePanelAppManifest(html);
4727
+ const sourceId = encodePanelAppId(source.sourceName);
4505
4728
  return {
4506
- content: event.payload.content,
4507
- submitted: true
4729
+ appId: resolvePanelAppAppId(source, manifest),
4730
+ sourceId,
4731
+ source,
4732
+ manifest,
4733
+ html,
4734
+ htmlWithBase: source.kind === "folder" ? injectPanelAppAssetBase(html, createAssetBaseHref(source)) : html
4508
4735
  };
4509
4736
  }
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");
4514
- }
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");
4519
- }
4520
- function stringifyJsonValue(value) {
4737
+ async function readPanelAppContentSourceByIdOrAppId(params) {
4738
+ const { appIdOrSourceId, createAssetBaseHref, panelsPath, sourceService } = params;
4521
4739
  try {
4522
- return JSON.stringify(value, null, 2);
4523
- } catch {
4524
- throw new PanelAppError("PANEL_APP_AGENT_REQUEST_INVALID", "value is not JSON serializable");
4740
+ return await readPanelAppContentSource({
4741
+ createAssetBaseHref,
4742
+ id: appIdOrSourceId,
4743
+ panelsPath,
4744
+ sourceService
4745
+ });
4746
+ } catch (error) {
4747
+ if (!isPanelAppError(error)) throw error;
4748
+ if (error.code !== "PANEL_APP_INVALID_ID" && error.code !== "PANEL_APP_NOT_FOUND") throw error;
4749
+ }
4750
+ const sources = await sourceService.listSources(panelsPath);
4751
+ for (const source of sources) {
4752
+ const html = await readFile(source.entryPath, "utf8");
4753
+ if (resolvePanelAppAppId(source, source.manifest ?? parsePanelAppManifest(html)) === appIdOrSourceId) return await readPanelAppContentSource({
4754
+ createAssetBaseHref,
4755
+ id: encodePanelAppId(source.sourceName),
4756
+ panelsPath,
4757
+ sourceService
4758
+ });
4525
4759
  }
4760
+ throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4526
4761
  }
4527
- function isRecord$6(value) {
4528
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4762
+ function resolvePanelAppAppId(source, manifest) {
4763
+ return manifest.id ?? encodePanelAppId(source.sourceName);
4764
+ }
4765
+ async function assertPanelAppDeclaresClient(params) {
4766
+ const { appId, panelsPath, sourceService } = params;
4767
+ const sources = await sourceService.listSources(panelsPath);
4768
+ for (const source of sources) {
4769
+ const manifest = source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8"));
4770
+ if (resolvePanelAppAppId(source, manifest) !== appId) continue;
4771
+ if (!manifest.client) throw new PanelAppError("PANEL_APP_CLIENT_NOT_DECLARED", "panel app did not declare client access");
4772
+ return;
4773
+ }
4774
+ throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
4529
4775
  }
4530
4776
  //#endregion
4531
4777
  //#region src/managers/panel-app.manager.ts
@@ -4533,9 +4779,12 @@ const PANEL_APP_CONTENT_BASE_PATH = "/api/panel-apps";
4533
4779
  const PANEL_APP_TOKENIZED_ASSET_BASE_PATH = "/api/panel-app-assets";
4534
4780
  const PANEL_APP_CONTENT_TYPE = "text/html; charset=utf-8";
4535
4781
  const PANEL_APP_CAPABILITY_GRANTS_FILE_NAME = ".panel-app-capability-grants.json";
4782
+ const PANEL_APP_CLIENT_GRANTS_FILE_NAME = ".panel-app-client-grants.json";
4783
+ const PANEL_APP_RUNTIME_TOKEN_TTL_MS = 1440 * 60 * 1e3;
4536
4784
  var PanelAppManager = class {
4537
4785
  bridgeSessions = /* @__PURE__ */ new Map();
4538
4786
  agentRunClient;
4787
+ agentBridgeService;
4539
4788
  assetTokenService = new PanelAppAssetTokenService();
4540
4789
  sourceService = new PanelAppSourceService();
4541
4790
  constructor(params) {
@@ -4544,6 +4793,10 @@ var PanelAppManager = class {
4544
4793
  eventBus: params.eventBus,
4545
4794
  ingress: params.ingress
4546
4795
  }) : null);
4796
+ this.agentBridgeService = new PanelAppAgentBridgeService({
4797
+ agentRunClient: this.agentRunClient,
4798
+ createCapabilityGrantStore: this.createCapabilityGrantStore
4799
+ });
4547
4800
  }
4548
4801
  listPanelApps = async () => {
4549
4802
  const workspacePath = this.getWorkspacePath();
@@ -4557,20 +4810,35 @@ var PanelAppManager = class {
4557
4810
  };
4558
4811
  };
4559
4812
  getPanelAppContent = async (id) => {
4560
- const panelsPath = this.getPanelsPath(this.getWorkspacePath());
4561
4813
  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;
4814
+ const resolved = await readPanelAppContentSource({
4815
+ createAssetBaseHref: this.createAssetBaseHref,
4816
+ id,
4817
+ panelsPath: this.getPanelsPath(this.getWorkspacePath()),
4818
+ sourceService: this.sourceService
4819
+ });
4820
+ const clientGranted = await this.isPanelAppClientGranted(resolved.appId, resolved.manifest.client);
4821
+ const session = this.createPanelAppRuntimeTokenSession({
4822
+ appId: resolved.appId,
4823
+ clientDeclared: resolved.manifest.client,
4824
+ declaredActions: resolved.manifest.serviceActions,
4825
+ declaredCapabilities: resolved.manifest.capabilities
4826
+ });
4827
+ const htmlWithBridge = injectPanelAppBridgeScript(resolved.htmlWithBase, {
4828
+ appId: resolved.appId,
4829
+ runtimeToken: session.token
4830
+ });
4831
+ const html = resolved.manifest.client && clientGranted ? injectPanelAppClientScript(htmlWithBridge, { runtimeToken: session.token }) : htmlWithBridge;
4567
4832
  return {
4568
- id: sourceId,
4569
- fileName: source.sourceName,
4570
- html: injectPanelAppBridgeScript(htmlWithBase),
4571
- capabilities: manifest.capabilities,
4833
+ id: resolved.sourceId,
4834
+ appId: resolved.appId,
4835
+ fileName: resolved.source.sourceName,
4836
+ html,
4837
+ capabilities: resolved.manifest.capabilities,
4838
+ clientDeclared: resolved.manifest.client,
4839
+ clientGranted,
4572
4840
  contentType: PANEL_APP_CONTENT_TYPE,
4573
- serviceActions: manifest.serviceActions
4841
+ serviceActions: resolved.manifest.serviceActions
4574
4842
  };
4575
4843
  } catch (error) {
4576
4844
  if (isPanelAppError(error)) throw error;
@@ -4588,27 +4856,58 @@ var PanelAppManager = class {
4588
4856
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
4589
4857
  return await this.sourceService.getAsset(panelsPath, claims.panelAppId, assetPath);
4590
4858
  };
4591
- getPanelAppBridgeScript = () => getPanelAppBridgeScript();
4592
- createPanelAppBridgeSession = async (params) => {
4593
- const content = await this.getPanelAppContent(params.id);
4859
+ getPanelAppBridgeScript = () => getPanelAppBridgeScript({
4860
+ appId: "",
4861
+ runtimeToken: ""
4862
+ });
4863
+ createPanelAppRuntimeTokenSession = (params) => {
4864
+ const { appId, clientDeclared, declaredActions, declaredCapabilities } = params;
4594
4865
  const now = /* @__PURE__ */ new Date();
4595
4866
  const session = {
4596
4867
  id: randomUUID(),
4597
4868
  token: randomUUID(),
4598
- panelAppId: content.id,
4599
- tabId: params.tabId,
4869
+ appId,
4600
4870
  caller: {
4601
4871
  surface: "panel-app",
4602
- appId: content.id
4872
+ appId
4603
4873
  },
4604
- declaredCapabilities: content.capabilities,
4605
- declaredActions: content.serviceActions,
4874
+ declaredCapabilities,
4875
+ declaredActions,
4876
+ clientDeclared,
4606
4877
  createdAt: now.toISOString(),
4607
- expiresAt: new Date(now.getTime() + 3600 * 1e3).toISOString()
4878
+ expiresAt: new Date(now.getTime() + PANEL_APP_RUNTIME_TOKEN_TTL_MS).toISOString()
4608
4879
  };
4609
4880
  this.bridgeSessions.set(session.token, session);
4610
4881
  return session;
4611
4882
  };
4883
+ createPanelAppBridgeSession = async (params) => {
4884
+ const resolved = await readPanelAppContentSourceByIdOrAppId({
4885
+ appIdOrSourceId: params.id,
4886
+ createAssetBaseHref: this.createAssetBaseHref,
4887
+ panelsPath: this.getPanelsPath(this.getWorkspacePath()),
4888
+ sourceService: this.sourceService
4889
+ });
4890
+ return this.createPanelAppRuntimeTokenSession({
4891
+ appId: resolved.appId,
4892
+ clientDeclared: resolved.manifest.client,
4893
+ declaredActions: resolved.manifest.serviceActions,
4894
+ declaredCapabilities: resolved.manifest.capabilities
4895
+ });
4896
+ };
4897
+ grantPanelAppClient = async (appId) => {
4898
+ await assertPanelAppDeclaresClient({
4899
+ appId,
4900
+ panelsPath: this.getPanelsPath(this.getWorkspacePath()),
4901
+ sourceService: this.sourceService
4902
+ });
4903
+ return await this.createClientGrantStore().grant({
4904
+ appId,
4905
+ grantedAt: (/* @__PURE__ */ new Date()).toISOString()
4906
+ });
4907
+ };
4908
+ revokePanelAppClient = async (appId) => {
4909
+ await this.createClientGrantStore().revoke(appId);
4910
+ };
4612
4911
  resolvePanelAppBridgeSession = (token) => {
4613
4912
  this.deleteExpiredBridgeSessions();
4614
4913
  const session = this.bridgeSessions.get(token.trim());
@@ -4620,39 +4919,15 @@ var PanelAppManager = class {
4620
4919
  };
4621
4920
  sendAgentMessage = async (bridgeSessionToken, payload) => {
4622
4921
  const bridgeSession = this.resolvePanelAppBridgeSession(bridgeSessionToken);
4623
- await this.assertAgentCapabilityGranted(bridgeSession, "agent:send");
4624
- return await this.requireAgentRunClient().send(withPanelAppAgentMetadata(payload, bridgeSession));
4922
+ return await this.agentBridgeService.sendAgentMessage(bridgeSession, payload);
4625
4923
  };
4626
4924
  generateAgentObject = async (bridgeSessionToken, input) => {
4627
4925
  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
- }) };
4926
+ return await this.agentBridgeService.generateAgentObject(bridgeSession, input);
4646
4927
  };
4647
4928
  grantAgentCapability = async (bridgeSessionToken, capability) => {
4648
4929
  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
- });
4930
+ return await this.agentBridgeService.grantAgentCapability(bridgeSession, capability);
4656
4931
  };
4657
4932
  updatePanelAppPreferences = async (id, preferences) => {
4658
4933
  const fileName = await this.resolvePanelAppFileName(id);
@@ -4670,42 +4945,21 @@ var PanelAppManager = class {
4670
4945
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
4671
4946
  const source = await this.sourceService.resolveSource(panelsPath, id);
4672
4947
  const panelAppId = encodePanelAppId(source.sourceName);
4948
+ const appId = resolvePanelAppAppId(source, source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8")));
4673
4949
  await rm(source.sourcePath, { recursive: source.kind === "folder" });
4674
4950
  await this.createStateStore(panelsPath).deleteEntry(panelAppId);
4675
4951
  await this.createCapabilityGrantStore().deleteCaller({
4676
4952
  surface: "panel-app",
4677
- appId: panelAppId
4953
+ appId
4678
4954
  });
4679
- this.deleteBridgeSessionsByPanelAppId(panelAppId);
4955
+ await this.createClientGrantStore().revoke(appId);
4956
+ this.deleteBridgeSessionsByPanelAppId(appId);
4680
4957
  return {
4681
4958
  deleted: true,
4682
4959
  fileName: source.sourceName,
4683
4960
  id: panelAppId
4684
4961
  };
4685
4962
  };
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
4963
  getWorkspacePath = () => getWorkspacePathFromConfig(this.params.configManager.config);
4710
4964
  getPanelsPath = (workspacePath) => join(workspacePath, DEFAULT_PANELS_DIR);
4711
4965
  createAssetBaseHref = (source) => {
@@ -4717,13 +4971,16 @@ var PanelAppManager = class {
4717
4971
  };
4718
4972
  createStateStore = (panelsPath) => new PanelAppStateStore(panelsPath);
4719
4973
  createCapabilityGrantStore = () => new PanelAppCapabilityGrantStore(join(this.getPanelsPath(this.getWorkspacePath()), PANEL_APP_CAPABILITY_GRANTS_FILE_NAME));
4974
+ createClientGrantStore = () => new PanelAppClientGrantStore(join(this.getPanelsPath(this.getWorkspacePath()), PANEL_APP_CLIENT_GRANTS_FILE_NAME));
4720
4975
  buildPanelAppEntry = async (source, state) => {
4721
4976
  const manifest = source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8"));
4722
4977
  const id = encodePanelAppId(source.sourceName);
4978
+ const appId = resolvePanelAppAppId(source, manifest);
4723
4979
  const createdAt = resolvePanelAppCreatedAt(source.sourceStat);
4724
4980
  const updatedAt = source.sourceStat.mtime.toISOString();
4725
4981
  const entry = {
4726
4982
  id,
4983
+ appId,
4727
4984
  fileName: source.sourceName,
4728
4985
  kind: source.kind,
4729
4986
  title: manifest.title ?? toPanelAppTitle(source.sourceName),
@@ -4732,6 +4989,8 @@ var PanelAppManager = class {
4732
4989
  updatedAt,
4733
4990
  sizeBytes: source.sourceStat.size,
4734
4991
  favorite: state.favorite ?? false,
4992
+ clientDeclared: manifest.client,
4993
+ clientGranted: await this.isPanelAppClientGranted(appId, manifest.client),
4735
4994
  openCount: state.openCount ?? 0
4736
4995
  };
4737
4996
  if (manifest.description) entry.description = manifest.description;
@@ -4748,7 +5007,11 @@ var PanelAppManager = class {
4748
5007
  for (const [token, session] of this.bridgeSessions) if (new Date(session.expiresAt).getTime() <= now) this.bridgeSessions.delete(token);
4749
5008
  };
4750
5009
  deleteBridgeSessionsByPanelAppId = (panelAppId) => {
4751
- for (const [token, session] of this.bridgeSessions) if (session.panelAppId === panelAppId) this.bridgeSessions.delete(token);
5010
+ for (const [token, session] of this.bridgeSessions) if (session.appId === panelAppId) this.bridgeSessions.delete(token);
5011
+ };
5012
+ isPanelAppClientGranted = async (appId, clientDeclared) => {
5013
+ if (!clientDeclared) return false;
5014
+ return await this.createClientGrantStore().isGranted(appId);
4752
5015
  };
4753
5016
  isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4754
5017
  };
@@ -5171,15 +5434,29 @@ var ServiceAppManager = class {
5171
5434
  };
5172
5435
  };
5173
5436
  grantServiceAction = async (actionId, request) => {
5437
+ const [grant] = await this.grantServiceActions([actionId], request);
5438
+ if (!grant) throw new ServiceAppError("SERVICE_APP_INVALID_ACTION", "service action id is invalid");
5439
+ return grant;
5440
+ };
5441
+ grantServiceActions = async (actionIds, request) => {
5174
5442
  this.assertCaller(request.caller);
5175
- this.assertDeclaredAction(actionId, request.declaredActions);
5176
- const action = await this.requireServiceAction(actionId);
5177
- return await this.createGrantStore().grant({
5443
+ const normalizedActionIds = this.normalizeActionIds(actionIds);
5444
+ if (normalizedActionIds.length === 0) throw new ServiceAppError("SERVICE_APP_INVALID_ACTION", "service action id is invalid");
5445
+ const actions = [];
5446
+ for (const actionId of normalizedActionIds) {
5447
+ this.assertDeclaredAction(actionId, request.declaredActions);
5448
+ actions.push(await this.requireServiceAction(actionId));
5449
+ }
5450
+ const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
5451
+ const grantStore = this.createGrantStore();
5452
+ const grants = [];
5453
+ for (const action of actions) grants.push(await grantStore.grant({
5178
5454
  caller: request.caller,
5179
- actionId,
5455
+ actionId: action.id,
5180
5456
  risk: action.risk,
5181
- grantedAt: (/* @__PURE__ */ new Date()).toISOString()
5182
- });
5457
+ grantedAt
5458
+ }));
5459
+ return grants;
5183
5460
  };
5184
5461
  listServiceActionGrants = async () => {
5185
5462
  return await this.createGrantStore().list();
@@ -5310,6 +5587,7 @@ var ServiceAppManager = class {
5310
5587
  assertDeclaredAction = (actionId, declaredActions) => {
5311
5588
  if (!declaredActions.includes(actionId)) throw new ServiceAppError("SERVICE_APP_ACTION_NOT_DECLARED", "panel app did not declare this service action");
5312
5589
  };
5590
+ normalizeActionIds = (actionIds) => Array.from(new Set(actionIds.map((actionId) => actionId.trim()).filter((actionId) => actionId.length > 0)));
5313
5591
  getWorkspacePath = () => getWorkspacePathFromConfig(this.params.configManager.config);
5314
5592
  getServiceAppsPath = (workspacePath) => join(workspacePath, DEFAULT_SERVICE_APPS_DIR);
5315
5593
  createGrantStore = () => new ServiceActionGrantStore(join(this.getServiceAppsPath(this.getWorkspacePath()), SERVICE_ACTION_GRANTS_FILE_NAME));
@@ -5336,7 +5614,6 @@ var MessageInbox = class {
5336
5614
  drain = () => {
5337
5615
  return this.messages.splice(0, this.messages.length);
5338
5616
  };
5339
- isEmpty = () => this.messages.length === 0;
5340
5617
  };
5341
5618
  function isConversationStateEvent(event) {
5342
5619
  return event.type !== NcpEventType.ContextWindowUpdated;
@@ -5344,10 +5621,10 @@ function isConversationStateEvent(event) {
5344
5621
  var SessionRun = class {
5345
5622
  inbox = new MessageInbox();
5346
5623
  sessionId;
5624
+ statusListeners = /* @__PURE__ */ new Set();
5347
5625
  activeRunId = null;
5348
5626
  activeRunController = null;
5349
- constructor(seed, eventBus, stateManager = new DefaultNcpAgentConversationStateManager()) {
5350
- this.eventBus = eventBus;
5627
+ constructor(seed, stateManager = new DefaultNcpAgentConversationStateManager()) {
5351
5628
  this.stateManager = stateManager;
5352
5629
  this.sessionId = seed.sessionId;
5353
5630
  this.stateManager.hydrate({
@@ -5364,19 +5641,20 @@ var SessionRun = class {
5364
5641
  const conversationEvents = events.filter(isConversationStateEvent);
5365
5642
  if (conversationEvents.length > 0) await this.stateManager.dispatchBatch(conversationEvents);
5366
5643
  };
5367
- applyAndPublishEvents = async (events, meta) => {
5368
- await this.applyEvents(events);
5369
- for (const event of events) this.eventBus?.emit(eventKeys.ncpEvent, event, {
5370
- emittedAt: meta.emittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5371
- source: meta.source
5372
- });
5644
+ onStatusChange = (listener) => {
5645
+ this.statusListeners.add(listener);
5646
+ return () => {
5647
+ this.statusListeners.delete(listener);
5648
+ };
5373
5649
  };
5374
5650
  beginRun = () => {
5375
5651
  if (this.activeRunId) throw new Error(`Session ${this.sessionId} already has an active run.`);
5652
+ const wasRunning = this.isRunning();
5376
5653
  const runId = `agent-run-${randomUUID()}`;
5377
5654
  const controller = new AbortController();
5378
5655
  this.activeRunId = runId;
5379
5656
  this.activeRunController = controller;
5657
+ this.emitStatusChangeIfNeeded(wasRunning);
5380
5658
  return {
5381
5659
  runId,
5382
5660
  signal: controller.signal
@@ -5385,30 +5663,43 @@ var SessionRun = class {
5385
5663
  abortRun = (runId) => {
5386
5664
  if (!this.activeRunId || !this.activeRunController) return false;
5387
5665
  if (runId && this.activeRunId !== runId) return false;
5666
+ const wasRunning = this.isRunning();
5388
5667
  this.activeRunController.abort();
5668
+ this.activeRunController = null;
5669
+ this.activeRunId = null;
5670
+ this.emitStatusChangeIfNeeded(wasRunning);
5389
5671
  return true;
5390
5672
  };
5391
5673
  isRunning = () => this.activeRunId !== null;
5392
5674
  dispose = () => {
5675
+ const wasRunning = this.isRunning();
5393
5676
  this.activeRunController?.abort();
5394
5677
  this.activeRunController = null;
5395
5678
  this.activeRunId = null;
5679
+ this.emitStatusChangeIfNeeded(wasRunning);
5680
+ this.statusListeners.clear();
5396
5681
  };
5397
5682
  applyRunEvents = (events) => {
5683
+ const wasRunning = this.isRunning();
5398
5684
  for (const event of events) {
5399
5685
  if (event.type === NcpEventType.RunStarted && event.payload.runId) this.activeRunId = event.payload.runId;
5400
- if (event.type === NcpEventType.MessageAbort || (event.type === NcpEventType.RunFinished || event.type === NcpEventType.RunError) && (!event.payload.runId || event.payload.runId === this.activeRunId)) {
5686
+ if ((event.type === NcpEventType.RunFinished || event.type === NcpEventType.RunError) && (!event.payload.runId || event.payload.runId === this.activeRunId)) {
5401
5687
  this.activeRunId = null;
5402
5688
  this.activeRunController = null;
5403
5689
  }
5404
5690
  }
5691
+ this.emitStatusChangeIfNeeded(wasRunning);
5692
+ };
5693
+ emitStatusChangeIfNeeded = (wasRunning) => {
5694
+ const running = this.isRunning();
5695
+ if (running === wasRunning) return;
5696
+ for (const listener of [...this.statusListeners]) listener(running ? "running" : "idle");
5405
5697
  };
5406
5698
  };
5407
5699
  var SessionRunManager = class {
5408
5700
  runs = /* @__PURE__ */ new Map();
5409
- constructor(sessionManager, eventBus) {
5701
+ constructor(sessionManager) {
5410
5702
  this.sessionManager = sessionManager;
5411
- this.eventBus = eventBus;
5412
5703
  }
5413
5704
  getSessionRun = (sessionId) => this.runs.get(sessionId) ?? null;
5414
5705
  isSessionRunning = (sessionId) => this.runs.get(sessionId.trim())?.isRunning() ?? false;
@@ -5417,7 +5708,7 @@ var SessionRunManager = class {
5417
5708
  const run = new SessionRun({
5418
5709
  messages: await this.sessionManager.listSessionMessages(sessionId),
5419
5710
  sessionId
5420
- }, this.eventBus);
5711
+ });
5421
5712
  this.runs.set(sessionId, run);
5422
5713
  return run;
5423
5714
  };
@@ -5669,7 +5960,7 @@ var NcpAgentSessionMetadataStore = class {
5669
5960
  read = async (sessionId, activitySnapshot) => {
5670
5961
  try {
5671
5962
  const parsed = JSON.parse(await readFile(this.metadataPath(sessionId), "utf-8"));
5672
- if (!isRecord$9(parsed) || parsed._type !== "metadata" || !isRecord$9(parsed.metadata)) throw new Error(`invalid ncp agent session metadata sidecar: ${sessionId}`);
5963
+ if (!isRecord$10(parsed) || parsed._type !== "metadata" || !isRecord$10(parsed.metadata)) throw new Error(`invalid ncp agent session metadata sidecar: ${sessionId}`);
5673
5964
  const createdAt = toIsoString(parsed.created_at, activitySnapshot.createdAt);
5674
5965
  const agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
5675
5966
  return {
@@ -5770,7 +6061,7 @@ var NcpAgentSessionSummaryIndexStore = class {
5770
6061
  function serializeJournalEntry(entry) {
5771
6062
  const serialized = JSON.stringify(entry);
5772
6063
  if (!serialized) throw new Error("ncp agent session journal entry serialization produced empty output");
5773
- if (!isRecord$9(JSON.parse(serialized))) throw new Error("ncp agent session journal entry serialization produced a non-object entry");
6064
+ if (!isRecord$10(JSON.parse(serialized))) throw new Error("ncp agent session journal entry serialization produced a non-object entry");
5774
6065
  return serialized;
5775
6066
  }
5776
6067
  var NcpAgentSessionJournalStore = class {
@@ -5986,15 +6277,15 @@ var NcpAgentSessionJournalStore = class {
5986
6277
  console.warn(`[ncp-agent-session-journal] skipped corrupted journal line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
5987
6278
  continue;
5988
6279
  }
5989
- if (!isRecord$9(parsed)) continue;
6280
+ if (!isRecord$10(parsed)) continue;
5990
6281
  if (parsed._type === "metadata") {
5991
- metadata = isRecord$9(parsed.metadata) ? structuredClone(parsed.metadata) : {};
6282
+ metadata = isRecord$10(parsed.metadata) ? structuredClone(parsed.metadata) : {};
5992
6283
  agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
5993
6284
  createdAt = toIsoString(parsed.created_at, createdAt);
5994
6285
  updatedAt = toIsoString(parsed.updated_at, updatedAt);
5995
6286
  continue;
5996
6287
  }
5997
- if (parsed._type === "event" && isRecord$9(parsed.event)) {
6288
+ if (parsed._type === "event" && isRecord$10(parsed.event)) {
5998
6289
  const seq = Number(parsed.seq);
5999
6290
  nextSeq = Math.max(nextSeq, Number.isFinite(seq) ? Math.trunc(seq) + 1 : nextSeq);
6000
6291
  updatedAt = toIsoString(parsed.timestamp, updatedAt);
@@ -7770,11 +8061,19 @@ function formatErrorStatus(error) {
7770
8061
  }
7771
8062
  return "运行出错";
7772
8063
  }
7773
- function createSessionActivityPreviewFromNcpEvent(event, timestamp) {
8064
+ function readToolCallId(value) {
8065
+ if (typeof value !== "string") return null;
8066
+ const trimmed = value.trim();
8067
+ return trimmed.length > 0 ? trimmed : null;
8068
+ }
8069
+ function formatToolDoneStatus(toolName) {
8070
+ return toolName ? `工具调用完成:${toolName}` : "工具调用完成";
8071
+ }
8072
+ function createSessionActivityPreviewFromNcpEvent(event, timestamp, options = {}) {
7774
8073
  switch (event.type) {
7775
8074
  case NcpEventType.RunStarted: return createProjection(readSessionId(event.payload.sessionId), {
7776
8075
  state: "running",
7777
- statusText: "正在处理...",
8076
+ statusText: "正在思考",
7778
8077
  timestamp
7779
8078
  });
7780
8079
  case NcpEventType.RunFinished: return createProjection(readSessionId(event.payload.sessionId), {
@@ -7815,11 +8114,15 @@ function createSessionActivityPreviewFromNcpEvent(event, timestamp) {
7815
8114
  timestamp
7816
8115
  });
7817
8116
  case NcpEventType.MessageToolCallEnd:
7818
- case NcpEventType.MessageToolCallResult: return createProjection(readSessionId(event.payload.sessionId), {
7819
- state: "running",
7820
- statusText: "工具调用完成",
7821
- timestamp
7822
- });
8117
+ case NcpEventType.MessageToolCallResult: {
8118
+ const sessionId = readSessionId(event.payload.sessionId);
8119
+ const toolCallId = readToolCallId(event.payload.toolCallId);
8120
+ return createProjection(sessionId, {
8121
+ state: "running",
8122
+ statusText: formatToolDoneStatus(sessionId && toolCallId ? options.readToolName?.(sessionId, toolCallId) ?? null : null),
8123
+ timestamp
8124
+ });
8125
+ }
7823
8126
  default: return null;
7824
8127
  }
7825
8128
  }
@@ -7887,13 +8190,16 @@ function writeSessionActivityPreviewMetadata(metadata, projection) {
7887
8190
  var SessionActivityPreviewContribution = class {
7888
8191
  unsubscribeNcpEvent = null;
7889
8192
  metadataWriteChains = /* @__PURE__ */ new Map();
8193
+ toolNames = /* @__PURE__ */ new Map();
7890
8194
  constructor(kernel) {
7891
8195
  this.kernel = kernel;
7892
8196
  }
7893
8197
  start = () => {
7894
8198
  if (this.unsubscribeNcpEvent) return;
7895
8199
  this.unsubscribeNcpEvent = this.kernel.eventBus.on(eventKeys.ncpEvent, (event) => {
7896
- const projection = createSessionActivityPreviewFromNcpEvent(event, (/* @__PURE__ */ new Date()).toISOString());
8200
+ this.rememberToolName(event);
8201
+ const projection = createSessionActivityPreviewFromNcpEvent(event, (/* @__PURE__ */ new Date()).toISOString(), { readToolName: this.readToolName });
8202
+ this.clearFinishedRunToolNames(event);
7897
8203
  if (!projection) return;
7898
8204
  this.updatePreview(projection);
7899
8205
  });
@@ -7902,7 +8208,29 @@ var SessionActivityPreviewContribution = class {
7902
8208
  this.unsubscribeNcpEvent?.();
7903
8209
  this.unsubscribeNcpEvent = null;
7904
8210
  this.metadataWriteChains.clear();
8211
+ this.toolNames.clear();
8212
+ };
8213
+ rememberToolName = (event) => {
8214
+ if (event.type !== NcpEventType.MessageToolCallStart) return;
8215
+ const sessionId = event.payload.sessionId.trim();
8216
+ const toolCallId = event.payload.toolCallId.trim();
8217
+ const toolName = event.payload.toolName.trim();
8218
+ if (!sessionId || !toolCallId || !toolName) return;
8219
+ this.toolNames.set(this.createToolNameKey(sessionId, toolCallId), toolName);
8220
+ };
8221
+ readToolName = (sessionId, toolCallId) => this.toolNames.get(this.createToolNameKey(sessionId, toolCallId)) ?? null;
8222
+ clearFinishedRunToolNames = (event) => {
8223
+ if (event.type !== NcpEventType.RunFinished && event.type !== NcpEventType.RunError) return;
8224
+ const sessionId = this.readNonEmptyString(event.payload.sessionId);
8225
+ if (!sessionId) return;
8226
+ for (const key of this.toolNames.keys()) if (key.startsWith(`${sessionId}:`)) this.toolNames.delete(key);
8227
+ };
8228
+ readNonEmptyString = (value) => {
8229
+ if (typeof value !== "string") return null;
8230
+ const trimmed = value.trim();
8231
+ return trimmed.length > 0 ? trimmed : null;
7905
8232
  };
8233
+ createToolNameKey = (sessionId, toolCallId) => `${sessionId}:${toolCallId}`;
7906
8234
  updatePreview = (projection) => {
7907
8235
  const next = (this.metadataWriteChains.get(projection.sessionId) ?? Promise.resolve()).then(() => this.updatePreviewMetadata(projection));
7908
8236
  this.metadataWriteChains.set(projection.sessionId, next.catch(() => void 0));
@@ -8023,11 +8351,11 @@ var MessagingToolProvider = class {
8023
8351
  });
8024
8352
  const { channel, chatId, metadata } = toolRunContext;
8025
8353
  const accountId = readMetadataAccountId(metadata);
8026
- const messageTool = new MessageTool((message) => this.kernel.messageBus.publishOutbound(message), { resolveChannels: this.resolveMessageChannels });
8354
+ const messageTool = new MessageTool(async (message) => {
8355
+ if (!await this.kernel.channels.deliver(message)) throw new Error(`channel "${message.channel}" is not available`);
8356
+ }, { resolveChannels: this.resolveMessageChannels });
8027
8357
  messageTool.setContext(channel, chatId, accountId ?? null);
8028
- const cronTool = new CronTool(this.kernel.automation);
8029
- cronTool.setContext(channel, chatId, accountId ?? null);
8030
- return [messageTool, cronTool];
8358
+ return [messageTool, new CronTool(this.kernel.automation)];
8031
8359
  };
8032
8360
  resolveMessageChannels = () => {
8033
8361
  const channels = this.kernel.extensions.getExtensionRegistry()?.channels ?? [];
@@ -8615,7 +8943,7 @@ var NextclawKernel = class {
8615
8943
  })
8616
8944
  });
8617
8945
  this.contextCompactionManager = new AgentRunContextCompactionManager(this.configManager, this.llmProviders, this.sessionManager);
8618
- this.sessionRunManager = new SessionRunManager(this.sessionManager, this.eventBus);
8946
+ this.sessionRunManager = new SessionRunManager(this.sessionManager);
8619
8947
  this.agentRunRequestManager = new AgentRunRequestManager(this.agentRuntimeManager, this.configManager, this.contextProviderManager, this.eventBus, this.ingress, this.sessionManager, this.sessionRunManager, this.toolProviderManager);
8620
8948
  this.contributions = [
8621
8949
  new ToolProviderContribution(this),