@lapis-notes/ai 0.1.1 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE.md +3 -7
  3. package/README.md +1 -1
  4. package/dist/ai-plugin.d.ts +2 -2
  5. package/dist/ai-plugin.js +28 -11
  6. package/dist/catalog/ai-catalog-panel.svelte +20 -3
  7. package/dist/chat/ai-chat-panel.svelte +187 -87
  8. package/dist/chat/ai-chat-panel.svelte.d.ts +2 -0
  9. package/dist/chat/ai-view-bootstrap.d.ts +1 -0
  10. package/dist/chat/ai-view-bootstrap.js +32 -12
  11. package/dist/chat/ai-view-panel.svelte +14 -0
  12. package/dist/chat/ai-view.d.ts +1 -1
  13. package/dist/chat/chat-controller.svelte.d.ts +1 -1
  14. package/dist/chat/chat-controller.svelte.js +94 -29
  15. package/dist/chat/chat-tool-display.d.ts +2 -12
  16. package/dist/chat/chat-tool-display.js +2 -206
  17. package/dist/chat/composer-model-catalog.d.ts +20 -0
  18. package/dist/chat/composer-model-catalog.js +44 -0
  19. package/dist/commands/agent.d.ts +1 -1
  20. package/dist/commands/agent.js +6 -1
  21. package/dist/conversations/context-handoff.d.ts +1 -25
  22. package/dist/conversations/context-handoff.js +1 -207
  23. package/dist/conversations/hashes.d.ts +1 -5
  24. package/dist/conversations/hashes.js +1 -40
  25. package/dist/core/types.d.ts +1 -241
  26. package/dist/core/types.js +1 -50
  27. package/dist/history/ai-history-panel.svelte +4 -1
  28. package/dist/host/controller-connection.d.ts +14 -0
  29. package/dist/host/controller-connection.js +62 -0
  30. package/dist/host/controller-runtime.d.ts +13 -0
  31. package/dist/host/controller-runtime.js +112 -0
  32. package/dist/host/create-host-runtimes.d.ts +2 -1
  33. package/dist/host/create-host-runtimes.js +5 -7
  34. package/dist/index.d.ts +1 -1
  35. package/dist/memory/candidate-extractor.d.ts +2 -25
  36. package/dist/memory/candidate-extractor.js +1 -191
  37. package/dist/memory/hashes.d.ts +1 -5
  38. package/dist/memory/hashes.js +1 -37
  39. package/dist/memory/memory-record-store.d.ts +3 -48
  40. package/dist/memory/memory-record-store.js +16 -220
  41. package/dist/memory/memory-scheduler.d.ts +1 -21
  42. package/dist/memory/memory-scheduler.js +1 -143
  43. package/dist/memory/native-memory-service.d.ts +1 -72
  44. package/dist/memory/native-memory-service.js +1 -1256
  45. package/dist/memory/paths.d.ts +1 -13
  46. package/dist/memory/paths.js +1 -60
  47. package/dist/memory/runtime-consolidation-provider.d.ts +2 -18
  48. package/dist/memory/runtime-consolidation-provider.js +1 -154
  49. package/dist/memory/types.d.ts +1 -139
  50. package/dist/providers/acp-model-provider.d.ts +8 -3
  51. package/dist/providers/acp-model-provider.js +21 -49
  52. package/dist/providers/model-provider.d.ts +3 -2
  53. package/dist/providers/model-provider.js +15 -2
  54. package/dist/runtime-adapters.d.ts +3 -0
  55. package/dist/runtime-adapters.js +2 -0
  56. package/dist/settings/acp-agents.d.ts +4 -1
  57. package/dist/settings/acp-agents.js +5 -1
  58. package/dist/settings/ai-settings-tab.js +8 -6
  59. package/dist/settings/ai-settings.d.ts +4 -4
  60. package/dist/settings/ai-settings.js +5 -3
  61. package/dist/settings/register-ai-settings.js +27 -14
  62. package/dist/tools/app-tool-host.d.ts +22 -2
  63. package/dist/tools/app-tool-host.js +51 -4
  64. package/dist/tools/desktop-app-tool-bridge.d.ts +4 -1
  65. package/dist/tools/desktop-app-tool-bridge.js +94 -157
  66. package/manifest.json +20 -2
  67. package/package.json +20 -7
  68. package/registry-content/overview.md +10 -0
  69. package/registry.json +221 -0
@@ -1,5 +1,5 @@
1
1
  import type { AiThinkingLevel } from "../core/types";
2
- import { type AcpAgentId } from "./acp-agents";
2
+ import { type AcpAgentId, type ProcessorAgentId } from "./acp-agents";
3
3
  export type AppToolEnablementOwner = {
4
4
  pluginId: string;
5
5
  source: "core" | "community" | "official" | "system";
@@ -9,7 +9,7 @@ export type AppToolEnablementRef = {
9
9
  owner: AppToolEnablementOwner;
10
10
  };
11
11
  export type AiPluginSettings = {
12
- defaultRuntime: "auto" | "acp" | "codex-native" | "fake";
12
+ defaultRuntime: "auto" | "acp" | "codex-native" | "opencode-native" | "fake";
13
13
  acpAgent: AcpAgentId;
14
14
  defaultModels: Record<AcpAgentId, string>;
15
15
  /** Active-agent compatibility alias. Persisted model ownership lives in defaultModels. */
@@ -18,11 +18,11 @@ export type AiPluginSettings = {
18
18
  memoryAutomaticRecall: boolean;
19
19
  memoryConsolidationEnabled: boolean;
20
20
  memoryConsolidationRuntime: "acp" | "codex-native";
21
- memoryConsolidationAgent: AcpAgentId;
21
+ memoryConsolidationAgent: ProcessorAgentId;
22
22
  memoryConsolidationModel: string;
23
23
  handoffSummariesEnabled: boolean;
24
24
  handoffSummaryRuntime: "acp" | "codex-native";
25
- handoffSummaryAgent: AcpAgentId;
25
+ handoffSummaryAgent: ProcessorAgentId;
26
26
  handoffSummaryModel: string;
27
27
  appToolsEnabled: boolean;
28
28
  disabledAppToolNames: string[];
@@ -1,10 +1,11 @@
1
- import { DEFAULT_ACP_AGENT, normalizeAcpAgent, } from "./acp-agents";
1
+ import { DEFAULT_ACP_AGENT, normalizeAcpAgent, normalizeProcessorAgent, } from "./acp-agents";
2
2
  export const DEFAULT_AI_SETTINGS = {
3
3
  defaultRuntime: "auto",
4
4
  acpAgent: DEFAULT_ACP_AGENT,
5
5
  defaultModels: {
6
6
  codex: "gpt-5.6-sol",
7
7
  cursor: "",
8
+ opencode: "",
8
9
  },
9
10
  defaultModel: "gpt-5.6-sol",
10
11
  thinking: "medium",
@@ -113,6 +114,7 @@ export function mergeAiSettings(value, registeredTools) {
113
114
  legacyModel ||
114
115
  DEFAULT_AI_SETTINGS.defaultModels.codex,
115
116
  cursor: storedModels?.cursor?.trim() || "",
117
+ opencode: storedModels?.opencode?.trim() || "",
116
118
  };
117
119
  const thinking = value?.thinking;
118
120
  const merged = {
@@ -128,12 +130,12 @@ export function mergeAiSettings(value, registeredTools) {
128
130
  memoryConsolidationRuntime: value?.memoryConsolidationRuntime === "codex-native"
129
131
  ? "codex-native"
130
132
  : "acp",
131
- memoryConsolidationAgent: normalizeAcpAgent(value?.memoryConsolidationAgent),
133
+ memoryConsolidationAgent: normalizeProcessorAgent(value?.memoryConsolidationAgent),
132
134
  memoryConsolidationModel: value?.memoryConsolidationModel?.trim() ||
133
135
  DEFAULT_AI_SETTINGS.memoryConsolidationModel,
134
136
  handoffSummariesEnabled: value?.handoffSummariesEnabled === true,
135
137
  handoffSummaryRuntime: value?.handoffSummaryRuntime === "codex-native" ? "codex-native" : "acp",
136
- handoffSummaryAgent: normalizeAcpAgent(value?.handoffSummaryAgent),
138
+ handoffSummaryAgent: normalizeProcessorAgent(value?.handoffSummaryAgent),
137
139
  handoffSummaryModel: value?.handoffSummaryModel?.trim() ||
138
140
  DEFAULT_AI_SETTINGS.handoffSummaryModel,
139
141
  appToolsEnabled: value?.appToolsEnabled !== false,
@@ -1,5 +1,6 @@
1
+ import { modelCatalogProviderId } from "../providers/model-provider";
1
2
  import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
2
- import { ACP_AGENT_IDS, normalizeAcpAgent, } from "./acp-agents";
3
+ import { ACP_AGENT_IDS, PROCESSOR_AGENT_IDS, normalizeAcpAgent, normalizeProcessorAgent, } from "./acp-agents";
3
4
  import { APP_TOOL_SETTING_PREFIX, DEFAULT_AI_SETTINGS, applyAppToolEnablement, migrateLegacyCommunityToolOptIns, } from "./ai-settings";
4
5
  import { listAppToolSettingRows, registeredAppToolRefs, } from "./app-tool-setting-rows";
5
6
  const FIELD_IDS = {
@@ -25,7 +26,7 @@ export function registerAiSettings(plugin) {
25
26
  return;
26
27
  const controller = binding.controller;
27
28
  const settings = plugin.getSettings();
28
- const modelSourceId = (provider) => `ai.models.${provider}`;
29
+ const modelSourceId = (provider, runtime) => `ai.models.${modelCatalogProviderId(provider, runtime)}`;
29
30
  const persistLegacyToolMigration = () => {
30
31
  const current = plugin.getSettings();
31
32
  const migrated = migrateLegacyCommunityToolOptIns(current, registeredAppToolRefs(plugin.app));
@@ -40,14 +41,18 @@ export function registerAiSettings(plugin) {
40
41
  enabledCommunityToolPluginIds: migrated.enabledCommunityToolPluginIds,
41
42
  });
42
43
  };
43
- for (const provider of ACP_AGENT_IDS) {
44
+ for (const { provider, runtime } of [
45
+ ...ACP_AGENT_IDS.map((provider) => ({ provider, runtime: "acp" })),
46
+ { provider: "codex", runtime: "codex-native" },
47
+ { provider: "opencode", runtime: "opencode-native" },
48
+ ]) {
44
49
  const dispose = controller.configuration.optionSources.register({
45
- id: modelSourceId(provider),
50
+ id: modelSourceId(provider, runtime),
46
51
  load: async () => {
47
52
  const current = plugin.getSettings();
48
53
  const saved = current.defaultModels[provider];
49
54
  try {
50
- const models = await plugin.models.listModels(provider);
55
+ const models = await plugin.models.listModels(provider, runtime);
51
56
  if (models.length === 0) {
52
57
  return saved
53
58
  ? [
@@ -112,6 +117,7 @@ export function registerAiSettings(plugin) {
112
117
  { value: "auto", label: "Automatic" },
113
118
  { value: "acp", label: "ACP" },
114
119
  { value: "codex-native", label: "Codex native" },
120
+ { value: "opencode-native", label: "OpenCode native" },
115
121
  { value: "fake", label: "Fake (tests)" },
116
122
  ],
117
123
  },
@@ -123,7 +129,11 @@ export function registerAiSettings(plugin) {
123
129
  default: DEFAULT_AI_SETTINGS.acpAgent,
124
130
  options: ACP_AGENT_IDS.map((value) => ({
125
131
  value,
126
- label: value === "cursor" ? "Cursor" : "Codex",
132
+ label: value === "cursor"
133
+ ? "Cursor"
134
+ : value === "opencode"
135
+ ? "OpenCode"
136
+ : "Codex",
127
137
  })),
128
138
  },
129
139
  {
@@ -132,7 +142,7 @@ export function registerAiSettings(plugin) {
132
142
  title: "Default model",
133
143
  description: "Model reported by the selected agent provider and sent on the next request.",
134
144
  default: current.defaultModel,
135
- optionsSource: modelSourceId(current.acpAgent),
145
+ optionsSource: modelSourceId(current.acpAgent, current.defaultRuntime),
136
146
  allowUnknownOptions: false,
137
147
  },
138
148
  {
@@ -179,7 +189,7 @@ export function registerAiSettings(plugin) {
179
189
  title: "Memory processor agent",
180
190
  description: "Pinned independently from the interactive chat agent.",
181
191
  default: DEFAULT_AI_SETTINGS.memoryConsolidationAgent,
182
- options: ACP_AGENT_IDS.map((value) => ({
192
+ options: PROCESSOR_AGENT_IDS.map((value) => ({
183
193
  value,
184
194
  label: value === "cursor" ? "Cursor" : "Codex",
185
195
  })),
@@ -215,7 +225,7 @@ export function registerAiSettings(plugin) {
215
225
  title: "Handoff summary agent",
216
226
  description: "Pinned independently from the interactive chat agent.",
217
227
  default: DEFAULT_AI_SETTINGS.handoffSummaryAgent,
218
- options: ACP_AGENT_IDS.map((value) => ({
228
+ options: PROCESSOR_AGENT_IDS.map((value) => ({
219
229
  value,
220
230
  label: value === "cursor" ? "Cursor" : "Codex",
221
231
  })),
@@ -305,9 +315,12 @@ export function registerAiSettings(plugin) {
305
315
  return;
306
316
  }
307
317
  if (event.id === FIELD_IDS.defaultRuntime) {
308
- void plugin.updateSettings({
309
- defaultRuntime: values[FIELD_IDS.defaultRuntime],
310
- });
318
+ void (async () => {
319
+ await plugin.updateSettings({
320
+ defaultRuntime: values[FIELD_IDS.defaultRuntime],
321
+ });
322
+ refreshSection(plugin.getSettings());
323
+ })();
311
324
  return;
312
325
  }
313
326
  if (event.id === FIELD_IDS.thinking) {
@@ -338,7 +351,7 @@ export function registerAiSettings(plugin) {
338
351
  }
339
352
  if (event.id === FIELD_IDS.memoryConsolidationAgent) {
340
353
  void plugin.updateSettings({
341
- memoryConsolidationAgent: normalizeAcpAgent(values[FIELD_IDS.memoryConsolidationAgent]),
354
+ memoryConsolidationAgent: normalizeProcessorAgent(values[FIELD_IDS.memoryConsolidationAgent]),
342
355
  });
343
356
  return;
344
357
  }
@@ -364,7 +377,7 @@ export function registerAiSettings(plugin) {
364
377
  }
365
378
  if (event.id === FIELD_IDS.handoffSummaryAgent) {
366
379
  void plugin.updateSettings({
367
- handoffSummaryAgent: normalizeAcpAgent(values[FIELD_IDS.handoffSummaryAgent]),
380
+ handoffSummaryAgent: normalizeProcessorAgent(values[FIELD_IDS.handoffSummaryAgent]),
368
381
  });
369
382
  return;
370
383
  }
@@ -1,7 +1,26 @@
1
1
  import { type AppToolApprovalDetails, type AppToolRegistry, type AppToolResult, type RegisteredAppTool } from "@lapis-notes/api/agent-tools";
2
2
  import type { AppToolSessionDescriptor, ApprovalOptionKind, ApprovalRequest } from "../core/types";
3
3
  import { type AiPluginSettings } from "../settings/ai-settings";
4
- export type AppToolExecutionErrorCode = "approval_denied" | "cancelled" | "execution_failed" | "invalid_arguments" | "invalid_result" | "invalid_tool_schema" | "timed_out" | "tool_unavailable";
4
+ export type AppToolExecutionErrorCode = "approval_denied" | "cancelled" | "capture_unavailable" | "execution_failed" | "invalid_arguments" | "invalid_result" | "invalid_tool_schema" | "timed_out" | "tool_unavailable";
5
+ export interface AppToolCapture {
6
+ begin(input: {
7
+ caller: string;
8
+ requestKey: string;
9
+ workspaceId: string;
10
+ captureMode: "cooperative" | "observed";
11
+ intent: string;
12
+ toolName: string;
13
+ }): Promise<{
14
+ spanId: string;
15
+ }>;
16
+ finish(input: {
17
+ caller: string;
18
+ requestKey: string;
19
+ workspaceId: string;
20
+ spanId: string;
21
+ outcome: "succeeded" | "failed" | "unknown";
22
+ }): Promise<void>;
23
+ }
5
24
  export declare class AppToolExecutionError extends Error {
6
25
  readonly code: AppToolExecutionErrorCode;
7
26
  constructor(code: AppToolExecutionErrorCode, message: string);
@@ -34,8 +53,9 @@ export declare class AppToolHost {
34
53
  private readonly registry;
35
54
  private readonly settings;
36
55
  private readonly timeoutMs;
56
+ private readonly capture?;
37
57
  readonly approvals: AppToolApprovalBroker;
38
- constructor(registry: AppToolRegistry, settings: () => AppToolPolicySettings, timeoutMs?: number);
58
+ constructor(registry: AppToolRegistry, settings: () => AppToolPolicySettings, timeoutMs?: number, capture?: AppToolCapture | undefined);
39
59
  createSession(input: CreateAppToolSessionInput): AppToolSessionDescriptor;
40
60
  getSession(agentBindingId: string): AppToolSessionDescriptor | undefined;
41
61
  invoke(agentBindingId: string, call: AppToolCall, signal?: AbortSignal): Promise<AppToolResult>;
@@ -102,6 +102,7 @@ export class AppToolHost {
102
102
  registry;
103
103
  settings;
104
104
  timeoutMs;
105
+ capture;
105
106
  approvals = new AppToolApprovalBroker();
106
107
  #ajv = new Ajv({ allErrors: true, strict: false });
107
108
  #sessions = new Map();
@@ -109,10 +110,11 @@ export class AppToolHost {
109
110
  #validators = new Map();
110
111
  #activeInvocations = new Map();
111
112
  #registryChangeRef;
112
- constructor(registry, settings, timeoutMs = DEFAULT_TIMEOUT_MS) {
113
+ constructor(registry, settings, timeoutMs = DEFAULT_TIMEOUT_MS, capture) {
113
114
  this.registry = registry;
114
115
  this.settings = settings;
115
116
  this.timeoutMs = timeoutMs;
117
+ this.capture = capture;
116
118
  this.#registryChangeRef = registry.on("changed", (change) => {
117
119
  if (change.reason !== "unregistered")
118
120
  return;
@@ -190,9 +192,54 @@ export class AppToolHost {
190
192
  if (grantForSession) {
191
193
  this.#grants.add(grantKey(session.descriptor, registered));
192
194
  }
193
- const result = await raceWithAbort(registered.tool.execute(call.input, context), invocation.signal);
194
- this.validateResult(registered, result);
195
- return result;
195
+ let spanId;
196
+ if (this.capture && registered.tool.effect !== "read") {
197
+ try {
198
+ const span = await this.capture.begin({
199
+ caller: "app-tool-host",
200
+ requestKey: call.toolCallId,
201
+ workspaceId: session.descriptor.scopeDir,
202
+ captureMode: "cooperative",
203
+ intent: "tool",
204
+ toolName: call.name,
205
+ });
206
+ spanId = span.spanId;
207
+ }
208
+ catch {
209
+ throw new AppToolExecutionError("capture_unavailable", `Checkpoint capture is unavailable for app tool: ${call.name}`);
210
+ }
211
+ }
212
+ try {
213
+ const result = await raceWithAbort(registered.tool.execute(call.input, context), invocation.signal);
214
+ this.validateResult(registered, result);
215
+ if (spanId && this.capture) {
216
+ await this.capture.finish({
217
+ caller: "app-tool-host",
218
+ requestKey: `${call.toolCallId}:finish`,
219
+ workspaceId: session.descriptor.scopeDir,
220
+ spanId,
221
+ outcome: "succeeded",
222
+ });
223
+ }
224
+ return result;
225
+ }
226
+ catch (error) {
227
+ if (spanId && this.capture) {
228
+ try {
229
+ await this.capture.finish({
230
+ caller: "app-tool-host",
231
+ requestKey: `${call.toolCallId}:failed`,
232
+ workspaceId: session.descriptor.scopeDir,
233
+ spanId,
234
+ outcome: "failed",
235
+ });
236
+ }
237
+ catch {
238
+ // Preserve the original tool error.
239
+ }
240
+ }
241
+ throw error;
242
+ }
196
243
  }
197
244
  catch (error) {
198
245
  if (error instanceof AppToolExecutionError) {
@@ -1,4 +1,5 @@
1
1
  import type { AgentEvent, AppToolSessionDescriptor, ApprovalOptionKind } from "../core/types";
2
+ import type { ControllerConnection } from "../host/controller-connection";
2
3
  import { AppToolHost, type CreateAppToolSessionInput } from "./app-tool-host";
3
4
  export type AppToolBridgeEvent = {
4
5
  bindingId: string;
@@ -11,10 +12,12 @@ export interface AppToolBridgeCoordinator {
11
12
  subscribe(listener: (event: AppToolBridgeEvent) => void): () => void;
12
13
  close(): Promise<void>;
13
14
  }
15
+ /** App authorization stays here; the shared SDK owns transport and tool receipts. */
14
16
  export declare class DesktopAppToolBridge implements AppToolBridgeCoordinator {
15
17
  #private;
16
18
  private readonly host;
17
- constructor(host: AppToolHost);
19
+ private readonly connection;
20
+ constructor(host: AppToolHost, connection: () => Promise<ControllerConnection>);
18
21
  prepare(input: CreateAppToolSessionInput): Promise<AppToolSessionDescriptor>;
19
22
  respondToApproval(requestId: string, decision: ApprovalOptionKind): boolean;
20
23
  subscribe(listener: (event: AppToolBridgeEvent) => void): () => void;
@@ -1,78 +1,127 @@
1
- import { getNativeDesktopBridge, getNativeDesktopCapability, } from "@lapis-notes/api/desktop-native";
2
1
  import { AppToolExecutionError, AppToolHost, } from "./app-tool-host";
3
- const APP_TOOL_MCP_SERVER_NAME = "lapis-tools";
4
- const SUPPORTED_APP_TOOL_TRANSPORTS = new Set(["stdio-mcp", "http-mcp"]);
2
+ /** App authorization stays here; the shared SDK owns transport and tool receipts. */
5
3
  export class DesktopAppToolBridge {
6
4
  host;
5
+ connection;
7
6
  #records = new Map();
8
7
  #listeners = new Set();
9
8
  #unsubscribeApprovals;
10
- #subscribedBridge = null;
11
- #unsubscribeCall;
12
- #unsubscribeCancel;
13
- constructor(host) {
9
+ constructor(host, connection) {
14
10
  this.host = host;
11
+ this.connection = connection;
15
12
  this.#unsubscribeApprovals = host.approvals.subscribe((request, bindingId) => {
16
13
  const record = this.#records.get(bindingId);
17
14
  const active = record ? [...record.calls.values()].at(-1) : undefined;
18
15
  this.#emit(bindingId, {
19
16
  type: "permission.request",
20
17
  request,
21
- ...(record && active
22
- ? { source: nextSource(record, active) }
23
- : {}),
18
+ ...(record && active ? { source: nextSource(record, active) } : {}),
24
19
  });
25
20
  });
26
21
  }
27
22
  async prepare(input) {
28
- if (this.#records.has(input.agentBindingId)) {
23
+ if (this.#records.has(input.agentBindingId))
29
24
  throw new Error(`App tool bridge already exists: ${input.agentBindingId}`);
30
- }
31
- const bridge = getNativeDesktopBridge();
32
- const hostCapable = supportsAppTools();
33
- const runtimeCapable = input.runtimeSupportsAppTools;
34
- const descriptor = this.host.createSession({
35
- ...input,
36
- runtimeSupportsAppTools: runtimeCapable && hostCapable,
37
- });
25
+ const descriptor = this.host.createSession(input);
38
26
  const record = {
39
27
  bindingId: input.agentBindingId,
28
+ bridgeId: `lapis.tools.${input.agentBindingId}`,
40
29
  calls: new Map(),
41
30
  };
42
31
  this.#records.set(input.agentBindingId, record);
43
- if (!runtimeCapable) {
44
- return withStatus(descriptor, "runtime-unavailable", "The selected agent runtime does not support application tools.");
45
- }
46
- if (!hostCapable || !bridge?.onAgentToolCall || !bridge.onAgentToolCancel) {
47
- return withStatus(descriptor, "host-upgrade-required", "Application tools require an agent host with protocol v3 support.");
48
- }
49
- if (descriptor.tools.length === 0) {
50
- return withStatus(descriptor, "disabled");
51
- }
52
- this.#subscribeToBridge(bridge);
32
+ if (!input.runtimeSupportsAppTools)
33
+ return Object.freeze({
34
+ ...descriptor,
35
+ status: "runtime-unavailable",
36
+ unavailableReason: "The selected runtime does not support application tools.",
37
+ });
38
+ if (!descriptor.tools.length)
39
+ return Object.freeze({ ...descriptor, status: "disabled" });
53
40
  try {
54
- const opened = await bridge.invoke("desktop_agent_tools_open", {
55
- bindingId: descriptor.agentBindingId,
56
- conversationId: descriptor.conversationId,
57
- descriptors: descriptor.tools.map((tool) => ({
41
+ const { client } = await this.connection();
42
+ record.unregister = await client.registerEndpoint({
43
+ id: record.bridgeId,
44
+ tools: descriptor.tools.map((tool) => ({
58
45
  name: tool.name,
59
46
  description: tool.description,
60
47
  inputSchema: tool.inputSchema,
61
- outputSchema: tool.outputSchema,
62
- effect: tool.effect,
48
+ effect: tool.effect === "read" ? "read" : "write",
63
49
  })),
50
+ handle: async (request, signal) => {
51
+ if (request.operation !== "tool.execute" ||
52
+ this.#records.get(record.bindingId) !== record)
53
+ throw new Error("Application tool binding is unavailable.");
54
+ const call = request.payload;
55
+ const active = {
56
+ abort: new AbortController(),
57
+ runId: `app-tool-${call.invocationId}`,
58
+ sequence: 0,
59
+ };
60
+ const abort = () => active.abort.abort();
61
+ if (signal.aborted)
62
+ abort();
63
+ signal.addEventListener("abort", abort, { once: true });
64
+ record.calls.set(call.invocationId, active);
65
+ this.#emit(record.bindingId, {
66
+ type: "tool.start",
67
+ id: call.invocationId,
68
+ name: call.name,
69
+ server: "lapis-tools",
70
+ input: call.arguments,
71
+ source: nextSource(record, active),
72
+ });
73
+ try {
74
+ const result = await this.host.invoke(record.bindingId, {
75
+ runId: active.runId,
76
+ toolCallId: call.invocationId,
77
+ name: call.name,
78
+ input: call.arguments,
79
+ }, active.abort.signal);
80
+ this.#emit(record.bindingId, {
81
+ type: "tool.end",
82
+ id: call.invocationId,
83
+ name: call.name,
84
+ server: "lapis-tools",
85
+ output: result,
86
+ source: nextSource(record, active),
87
+ });
88
+ return result;
89
+ }
90
+ catch (error) {
91
+ const normalized = error instanceof AppToolExecutionError
92
+ ? { code: error.code, message: error.message }
93
+ : {
94
+ code: "execution_failed",
95
+ message: "Application tool execution failed.",
96
+ };
97
+ this.#emit(record.bindingId, {
98
+ type: "tool.end",
99
+ id: call.invocationId,
100
+ name: call.name,
101
+ server: "lapis-tools",
102
+ error: normalized,
103
+ source: nextSource(record, active),
104
+ });
105
+ return {
106
+ content: [{ type: "text", text: normalized.message }],
107
+ isError: true,
108
+ };
109
+ }
110
+ finally {
111
+ signal.removeEventListener("abort", abort);
112
+ record.calls.delete(call.invocationId);
113
+ }
114
+ },
64
115
  });
65
- record.bridge = bridge;
66
- record.bridgeId = opened.bridgeId;
67
116
  return Object.freeze({
68
117
  ...descriptor,
69
- bridgeId: opened.bridgeId,
118
+ bridgeId: record.bridgeId,
70
119
  status: "available",
71
120
  });
72
121
  }
73
122
  catch (error) {
74
- this.#records.delete(input.agentBindingId);
75
- this.host.closeBinding(input.agentBindingId);
123
+ this.#records.delete(record.bindingId);
124
+ this.host.closeBinding(record.bindingId);
76
125
  throw error;
77
126
  }
78
127
  }
@@ -88,136 +137,24 @@ export class DesktopAppToolBridge {
88
137
  this.#records.delete(bindingId);
89
138
  for (const call of record?.calls.values() ?? [])
90
139
  call.abort.abort();
91
- record?.calls.clear();
92
140
  this.host.closeBinding(bindingId);
93
- if (record?.bridge && record.bridgeId) {
94
- await record.bridge
95
- .invoke("desktop_agent_tools_close", { bridgeId: record.bridgeId })
96
- .catch(() => undefined);
97
- }
141
+ await record?.unregister?.().catch(() => undefined);
98
142
  }
99
143
  async close() {
100
- for (const bindingId of [...this.#records.keys()]) {
101
- await this.closeBinding(bindingId);
102
- }
144
+ for (const id of [...this.#records.keys()])
145
+ await this.closeBinding(id);
103
146
  this.#unsubscribeApprovals();
104
- this.#unsubscribeCall?.();
105
- this.#unsubscribeCancel?.();
106
147
  this.#listeners.clear();
107
- this.#subscribedBridge = null;
108
- }
109
- #subscribeToBridge(bridge) {
110
- if (this.#subscribedBridge === bridge)
111
- return;
112
- this.#unsubscribeCall?.();
113
- this.#unsubscribeCancel?.();
114
- this.#subscribedBridge = bridge;
115
- this.#unsubscribeCall = bridge.onAgentToolCall?.((call) => {
116
- void this.#handleCall(call);
117
- });
118
- this.#unsubscribeCancel = bridge.onAgentToolCancel?.((cancel) => {
119
- this.#handleCancel(cancel);
120
- });
121
- }
122
- async #handleCall(call) {
123
- const record = this.#records.get(call.bindingId);
124
- if (!record || record.bridgeId !== call.bridgeId || !record.bridge)
125
- return;
126
- if (record.calls.has(call.callId))
127
- return;
128
- const active = {
129
- abort: new AbortController(),
130
- runId: `app-tool-${call.callId}`,
131
- sequence: 0,
132
- };
133
- record.calls.set(call.callId, active);
134
- this.#emit(call.bindingId, {
135
- type: "tool.start",
136
- id: call.callId,
137
- name: call.name,
138
- server: APP_TOOL_MCP_SERVER_NAME,
139
- input: call.input,
140
- source: nextSource(record, active),
141
- });
142
- try {
143
- const result = await this.host.invoke(call.bindingId, {
144
- runId: active.runId,
145
- toolCallId: call.callId,
146
- name: call.name,
147
- input: call.input,
148
- }, active.abort.signal);
149
- await record.bridge.invoke("desktop_agent_tools_respond", {
150
- bridgeId: call.bridgeId,
151
- callId: call.callId,
152
- result,
153
- });
154
- this.#emit(call.bindingId, {
155
- type: "tool.end",
156
- id: call.callId,
157
- name: call.name,
158
- server: APP_TOOL_MCP_SERVER_NAME,
159
- output: result,
160
- source: nextSource(record, active),
161
- });
162
- }
163
- catch (error) {
164
- const normalized = normalizeExecutionError(error);
165
- await record.bridge
166
- .invoke("desktop_agent_tools_respond", {
167
- bridgeId: call.bridgeId,
168
- callId: call.callId,
169
- error: normalized,
170
- })
171
- .catch(() => undefined);
172
- this.#emit(call.bindingId, {
173
- type: "tool.end",
174
- id: call.callId,
175
- name: call.name,
176
- server: APP_TOOL_MCP_SERVER_NAME,
177
- error: normalized,
178
- source: nextSource(record, active),
179
- });
180
- }
181
- finally {
182
- record.calls.delete(call.callId);
183
- }
184
- }
185
- #handleCancel(cancel) {
186
- const record = this.#records.get(cancel.bindingId);
187
- if (!record || record.bridgeId !== cancel.bridgeId)
188
- return;
189
- record.calls.get(cancel.callId)?.abort.abort();
190
148
  }
191
149
  #emit(bindingId, event) {
192
150
  for (const listener of this.#listeners)
193
151
  listener({ bindingId, event });
194
152
  }
195
153
  }
196
- function supportsAppTools() {
197
- const capability = getNativeDesktopCapability("agent-runtime");
198
- const protocolVersion = Number(capability?.details?.protocolVersion ?? 0);
199
- const transport = String(capability?.details?.appTools ?? "");
200
- return (capability?.status === "available" &&
201
- protocolVersion >= 3 &&
202
- SUPPORTED_APP_TOOL_TRANSPORTS.has(transport));
203
- }
204
- function withStatus(descriptor, status, unavailableReason) {
205
- return Object.freeze({ ...descriptor, status, unavailableReason });
206
- }
207
154
  function nextSource(record, active) {
208
- active.sequence += 1;
209
155
  return {
210
- sessionId: record.bridgeId ?? record.bindingId,
156
+ sessionId: record.bridgeId,
211
157
  runId: active.runId,
212
- sequence: active.sequence,
213
- };
214
- }
215
- function normalizeExecutionError(error) {
216
- if (error instanceof AppToolExecutionError) {
217
- return { code: error.code, message: error.message };
218
- }
219
- return {
220
- code: "execution_failed",
221
- message: "Application tool execution failed.",
158
+ sequence: ++active.sequence,
222
159
  };
223
160
  }
package/manifest.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "id": "ai",
3
3
  "name": "AI",
4
- "version": "0.1.1",
4
+ "version": "0.1.5",
5
5
  "minAppVersion": "0.1.0",
6
6
  "description": "Provider-agnostic agent chat with ACP and optional native runtimes.",
7
7
  "author": "Lapis Notes",
8
+ "authorAvatarUrl": "https://www.gravatar.com/avatar/1df68d9ff087b0dd72fd8626056fca7f?s=128&d=404",
9
+ "isDesktopOnly": false,
8
10
  "lapis": {
9
11
  "manifestVersion": 1,
10
12
  "source": "official",
@@ -24,7 +26,23 @@
24
26
  "@lapis-notes/api/path",
25
27
  "@lapis-notes/api/vault",
26
28
  "@lapis-notes/api/workspace-host",
27
- "@lapis-notes/markdown/embed"
29
+ "@lapis-notes/markdown/embed",
30
+ "@codemirror/autocomplete",
31
+ "@codemirror/commands",
32
+ "@codemirror/language",
33
+ "@codemirror/state",
34
+ "@codemirror/view",
35
+ "@lezer/common",
36
+ "@lezer/highlight",
37
+ "@lezer/lr",
38
+ "svelte/renderer/5.56.10",
39
+ "svelte/renderer/5.56.10/attachments",
40
+ "svelte/renderer/5.56.10/events",
41
+ "svelte/renderer/5.56.10/internal/client",
42
+ "svelte/renderer/5.56.10/internal/disclose-version",
43
+ "svelte/renderer/5.56.10/internal/flags/legacy",
44
+ "svelte/renderer/5.56.10/reactivity",
45
+ "svelte/renderer/5.56.10/store"
28
46
  ],
29
47
  "requiresReloadOnUpdate": false
30
48
  }