@nextclaw/kernel 0.3.3 → 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) {
@@ -4232,300 +4606,172 @@ function resolvePanelAppAssetContentType(path) {
4232
4606
  }
4233
4607
  }
4234
4608
  function resolvePanelAppIconUrl(id, icon) {
4235
- if (!icon) return;
4236
- if (icon.startsWith("data:image/") || icon.startsWith("http://") || icon.startsWith("https://") || icon.startsWith("/") || isLikelyTextIcon(icon)) return icon;
4237
- return `/api/panel-apps/${encodeURIComponent(id)}/assets/${encodePanelAppAssetPath(icon)}`;
4238
- }
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
- };
4609
+ if (!icon) return;
4610
+ if (icon.startsWith("data:image/") || icon.startsWith("http://") || icon.startsWith("https://") || icon.startsWith("/") || isLikelyTextIcon(icon)) return icon;
4611
+ return `/api/panel-apps/${encodeURIComponent(id)}/assets/${encodePanelAppAssetPath(icon)}`;
4455
4612
  }
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
- };
4613
+ function injectPanelAppAssetBase(html, baseHref) {
4614
+ const base = `<base href="${baseHref}">`;
4615
+ if (/<base\b/i.test(html)) return html;
4616
+ if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (head) => `${head}${base}`);
4617
+ return `${base}${html}`;
4463
4618
  }
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)));
4619
+ function encodePanelAppAssetPath(path) {
4620
+ return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
4467
4621
  }
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;
4622
+ function hasSafeBaseName(name) {
4623
+ return !name.includes("/") && !name.includes("\\") && !name.includes("\0");
4473
4624
  }
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;
4625
+ function isLikelyTextIcon(icon) {
4626
+ return !icon.includes("/") && !icon.includes(".") && icon.length <= 4;
4479
4627
  }
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");
4628
+ function escapeRegExp(value) {
4629
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4497
4630
  }
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
4631
+ //#endregion
4632
+ //#region src/services/panel-app-source.service.ts
4633
+ var PanelAppSourceService = class {
4634
+ listSources = async (panelsPath) => {
4635
+ try {
4636
+ const entries = await readdir(panelsPath, { withFileTypes: true });
4637
+ const sources = [];
4638
+ for (const entry of entries.filter(isPanelAppSourceEntry)) try {
4639
+ sources.push(await this.readSource(panelsPath, entry.name));
4640
+ } catch (error) {
4641
+ if (!isPanelAppError(error) || error.code !== "PANEL_APP_NOT_FOUND") throw error;
4642
+ }
4643
+ return sources;
4644
+ } catch (error) {
4645
+ if (isPanelAppError(error)) throw error;
4646
+ if (isMissingFileError$1(error)) return [];
4647
+ throw new PanelAppError("PANEL_APP_READ_FAILED", error instanceof Error ? error.message : String(error));
4648
+ }
4502
4649
  };
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
4650
+ resolveSource = async (panelsPath, id) => {
4651
+ const sourceName = decodePanelAppId(id);
4652
+ try {
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));
4658
+ }
4659
+ };
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
+ }
4675
+ };
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");
4688
+ };
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
+ }
4508
4707
  };
4708
+ };
4709
+ function isMissingFileError$1(error) {
4710
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4509
4711
  }
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");
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();
4514
4716
  }
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");
4717
+ function resolvePanelAppActivityMs(entry) {
4718
+ return Math.max(new Date(entry.lastOpenedAt ?? 0).getTime(), new Date(entry.createdAt).getTime(), new Date(entry.updatedAt).getTime());
4519
4719
  }
4520
- function stringifyJsonValue(value) {
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);
4728
+ return {
4729
+ appId: resolvePanelAppAppId(source, manifest),
4730
+ sourceId,
4731
+ source,
4732
+ manifest,
4733
+ html,
4734
+ htmlWithBase: source.kind === "folder" ? injectPanelAppAssetBase(html, createAssetBaseHref(source)) : html
4735
+ };
4736
+ }
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
  };
@@ -5351,7 +5614,6 @@ var MessageInbox = class {
5351
5614
  drain = () => {
5352
5615
  return this.messages.splice(0, this.messages.length);
5353
5616
  };
5354
- isEmpty = () => this.messages.length === 0;
5355
5617
  };
5356
5618
  function isConversationStateEvent(event) {
5357
5619
  return event.type !== NcpEventType.ContextWindowUpdated;
@@ -5359,10 +5621,10 @@ function isConversationStateEvent(event) {
5359
5621
  var SessionRun = class {
5360
5622
  inbox = new MessageInbox();
5361
5623
  sessionId;
5624
+ statusListeners = /* @__PURE__ */ new Set();
5362
5625
  activeRunId = null;
5363
5626
  activeRunController = null;
5364
- constructor(seed, eventBus, stateManager = new DefaultNcpAgentConversationStateManager()) {
5365
- this.eventBus = eventBus;
5627
+ constructor(seed, stateManager = new DefaultNcpAgentConversationStateManager()) {
5366
5628
  this.stateManager = stateManager;
5367
5629
  this.sessionId = seed.sessionId;
5368
5630
  this.stateManager.hydrate({
@@ -5379,19 +5641,20 @@ var SessionRun = class {
5379
5641
  const conversationEvents = events.filter(isConversationStateEvent);
5380
5642
  if (conversationEvents.length > 0) await this.stateManager.dispatchBatch(conversationEvents);
5381
5643
  };
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
- });
5644
+ onStatusChange = (listener) => {
5645
+ this.statusListeners.add(listener);
5646
+ return () => {
5647
+ this.statusListeners.delete(listener);
5648
+ };
5388
5649
  };
5389
5650
  beginRun = () => {
5390
5651
  if (this.activeRunId) throw new Error(`Session ${this.sessionId} already has an active run.`);
5652
+ const wasRunning = this.isRunning();
5391
5653
  const runId = `agent-run-${randomUUID()}`;
5392
5654
  const controller = new AbortController();
5393
5655
  this.activeRunId = runId;
5394
5656
  this.activeRunController = controller;
5657
+ this.emitStatusChangeIfNeeded(wasRunning);
5395
5658
  return {
5396
5659
  runId,
5397
5660
  signal: controller.signal
@@ -5400,30 +5663,43 @@ var SessionRun = class {
5400
5663
  abortRun = (runId) => {
5401
5664
  if (!this.activeRunId || !this.activeRunController) return false;
5402
5665
  if (runId && this.activeRunId !== runId) return false;
5666
+ const wasRunning = this.isRunning();
5403
5667
  this.activeRunController.abort();
5668
+ this.activeRunController = null;
5669
+ this.activeRunId = null;
5670
+ this.emitStatusChangeIfNeeded(wasRunning);
5404
5671
  return true;
5405
5672
  };
5406
5673
  isRunning = () => this.activeRunId !== null;
5407
5674
  dispose = () => {
5675
+ const wasRunning = this.isRunning();
5408
5676
  this.activeRunController?.abort();
5409
5677
  this.activeRunController = null;
5410
5678
  this.activeRunId = null;
5679
+ this.emitStatusChangeIfNeeded(wasRunning);
5680
+ this.statusListeners.clear();
5411
5681
  };
5412
5682
  applyRunEvents = (events) => {
5683
+ const wasRunning = this.isRunning();
5413
5684
  for (const event of events) {
5414
5685
  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)) {
5686
+ if ((event.type === NcpEventType.RunFinished || event.type === NcpEventType.RunError) && (!event.payload.runId || event.payload.runId === this.activeRunId)) {
5416
5687
  this.activeRunId = null;
5417
5688
  this.activeRunController = null;
5418
5689
  }
5419
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");
5420
5697
  };
5421
5698
  };
5422
5699
  var SessionRunManager = class {
5423
5700
  runs = /* @__PURE__ */ new Map();
5424
- constructor(sessionManager, eventBus) {
5701
+ constructor(sessionManager) {
5425
5702
  this.sessionManager = sessionManager;
5426
- this.eventBus = eventBus;
5427
5703
  }
5428
5704
  getSessionRun = (sessionId) => this.runs.get(sessionId) ?? null;
5429
5705
  isSessionRunning = (sessionId) => this.runs.get(sessionId.trim())?.isRunning() ?? false;
@@ -5432,7 +5708,7 @@ var SessionRunManager = class {
5432
5708
  const run = new SessionRun({
5433
5709
  messages: await this.sessionManager.listSessionMessages(sessionId),
5434
5710
  sessionId
5435
- }, this.eventBus);
5711
+ });
5436
5712
  this.runs.set(sessionId, run);
5437
5713
  return run;
5438
5714
  };
@@ -5684,7 +5960,7 @@ var NcpAgentSessionMetadataStore = class {
5684
5960
  read = async (sessionId, activitySnapshot) => {
5685
5961
  try {
5686
5962
  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}`);
5963
+ if (!isRecord$10(parsed) || parsed._type !== "metadata" || !isRecord$10(parsed.metadata)) throw new Error(`invalid ncp agent session metadata sidecar: ${sessionId}`);
5688
5964
  const createdAt = toIsoString(parsed.created_at, activitySnapshot.createdAt);
5689
5965
  const agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
5690
5966
  return {
@@ -5785,7 +6061,7 @@ var NcpAgentSessionSummaryIndexStore = class {
5785
6061
  function serializeJournalEntry(entry) {
5786
6062
  const serialized = JSON.stringify(entry);
5787
6063
  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");
6064
+ if (!isRecord$10(JSON.parse(serialized))) throw new Error("ncp agent session journal entry serialization produced a non-object entry");
5789
6065
  return serialized;
5790
6066
  }
5791
6067
  var NcpAgentSessionJournalStore = class {
@@ -6001,15 +6277,15 @@ var NcpAgentSessionJournalStore = class {
6001
6277
  console.warn(`[ncp-agent-session-journal] skipped corrupted journal line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
6002
6278
  continue;
6003
6279
  }
6004
- if (!isRecord$9(parsed)) continue;
6280
+ if (!isRecord$10(parsed)) continue;
6005
6281
  if (parsed._type === "metadata") {
6006
- metadata = isRecord$9(parsed.metadata) ? structuredClone(parsed.metadata) : {};
6282
+ metadata = isRecord$10(parsed.metadata) ? structuredClone(parsed.metadata) : {};
6007
6283
  agentId = normalizeNcpAgentId(typeof parsed.agent_id === "string" ? parsed.agent_id : void 0);
6008
6284
  createdAt = toIsoString(parsed.created_at, createdAt);
6009
6285
  updatedAt = toIsoString(parsed.updated_at, updatedAt);
6010
6286
  continue;
6011
6287
  }
6012
- if (parsed._type === "event" && isRecord$9(parsed.event)) {
6288
+ if (parsed._type === "event" && isRecord$10(parsed.event)) {
6013
6289
  const seq = Number(parsed.seq);
6014
6290
  nextSeq = Math.max(nextSeq, Number.isFinite(seq) ? Math.trunc(seq) + 1 : nextSeq);
6015
6291
  updatedAt = toIsoString(parsed.timestamp, updatedAt);
@@ -7785,11 +8061,19 @@ function formatErrorStatus(error) {
7785
8061
  }
7786
8062
  return "运行出错";
7787
8063
  }
7788
- 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 = {}) {
7789
8073
  switch (event.type) {
7790
8074
  case NcpEventType.RunStarted: return createProjection(readSessionId(event.payload.sessionId), {
7791
8075
  state: "running",
7792
- statusText: "正在处理...",
8076
+ statusText: "正在思考",
7793
8077
  timestamp
7794
8078
  });
7795
8079
  case NcpEventType.RunFinished: return createProjection(readSessionId(event.payload.sessionId), {
@@ -7830,11 +8114,15 @@ function createSessionActivityPreviewFromNcpEvent(event, timestamp) {
7830
8114
  timestamp
7831
8115
  });
7832
8116
  case NcpEventType.MessageToolCallEnd:
7833
- case NcpEventType.MessageToolCallResult: return createProjection(readSessionId(event.payload.sessionId), {
7834
- state: "running",
7835
- statusText: "工具调用完成",
7836
- timestamp
7837
- });
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
+ }
7838
8126
  default: return null;
7839
8127
  }
7840
8128
  }
@@ -7902,13 +8190,16 @@ function writeSessionActivityPreviewMetadata(metadata, projection) {
7902
8190
  var SessionActivityPreviewContribution = class {
7903
8191
  unsubscribeNcpEvent = null;
7904
8192
  metadataWriteChains = /* @__PURE__ */ new Map();
8193
+ toolNames = /* @__PURE__ */ new Map();
7905
8194
  constructor(kernel) {
7906
8195
  this.kernel = kernel;
7907
8196
  }
7908
8197
  start = () => {
7909
8198
  if (this.unsubscribeNcpEvent) return;
7910
8199
  this.unsubscribeNcpEvent = this.kernel.eventBus.on(eventKeys.ncpEvent, (event) => {
7911
- 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);
7912
8203
  if (!projection) return;
7913
8204
  this.updatePreview(projection);
7914
8205
  });
@@ -7917,7 +8208,29 @@ var SessionActivityPreviewContribution = class {
7917
8208
  this.unsubscribeNcpEvent?.();
7918
8209
  this.unsubscribeNcpEvent = null;
7919
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;
7920
8232
  };
8233
+ createToolNameKey = (sessionId, toolCallId) => `${sessionId}:${toolCallId}`;
7921
8234
  updatePreview = (projection) => {
7922
8235
  const next = (this.metadataWriteChains.get(projection.sessionId) ?? Promise.resolve()).then(() => this.updatePreviewMetadata(projection));
7923
8236
  this.metadataWriteChains.set(projection.sessionId, next.catch(() => void 0));
@@ -8630,7 +8943,7 @@ var NextclawKernel = class {
8630
8943
  })
8631
8944
  });
8632
8945
  this.contextCompactionManager = new AgentRunContextCompactionManager(this.configManager, this.llmProviders, this.sessionManager);
8633
- this.sessionRunManager = new SessionRunManager(this.sessionManager, this.eventBus);
8946
+ this.sessionRunManager = new SessionRunManager(this.sessionManager);
8634
8947
  this.agentRunRequestManager = new AgentRunRequestManager(this.agentRuntimeManager, this.configManager, this.contextProviderManager, this.eventBus, this.ingress, this.sessionManager, this.sessionRunManager, this.toolProviderManager);
8635
8948
  this.contributions = [
8636
8949
  new ToolProviderContribution(this),