@lapis-notes/ai 0.1.2 → 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 +24 -0
  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 +17 -7
  68. package/registry-content/overview.md +10 -0
  69. package/registry.json +211 -2
@@ -1,11 +1,10 @@
1
1
  <script lang="ts">
2
- import { onMount, untrack } from "svelte";
2
+ import { onDestroy, onMount, untrack } from "svelte";
3
3
  import type { App, WorkspaceLeaf } from "@lapis-notes/api";
4
4
  import { MarkdownEmbed } from "@lapis-notes/markdown/embed";
5
5
  import * as Chat from "@lapismd/design-core/ai/chat";
6
6
  import { Reasoning } from "@lapismd/design-core/ai/experimental";
7
7
  import { Button } from "@lapismd/design-core/shadcn/button";
8
- import { CodeBlock } from "@lapismd/design-core/shadcn/code-block";
9
8
  import * as CommandView from "@lapismd/design-core/shadcn/command-view";
10
9
  import * as DropdownMenu from "@lapismd/design-core/shadcn/dropdown-menu";
11
10
  import * as Empty from "@lapismd/design-core/shadcn/empty";
@@ -92,6 +91,11 @@
92
91
  import type { AutomaticMemoryRecall } from "../memory/types";
93
92
  import type { HandoffSummaryCoordinator } from "../conversations/handoff-summary";
94
93
 
94
+ import {
95
+ ComposerModelCatalog,
96
+ type ComposerModelCatalogState,
97
+ } from "./composer-model-catalog";
98
+
95
99
  let {
96
100
  app,
97
101
  runtime,
@@ -122,6 +126,7 @@
122
126
  fileSearch,
123
127
  models = [],
124
128
  modelCatalogError = null,
129
+ loadModels,
125
130
  settings,
126
131
  onSettingsChange,
127
132
  }: {
@@ -158,6 +163,10 @@
158
163
  fileSearch?: ComposerSearchSource;
159
164
  models?: ModelRef[];
160
165
  modelCatalogError?: string | null;
166
+ loadModels?: (
167
+ agent: AcpAgentId,
168
+ runtime: AiPluginSettings["defaultRuntime"],
169
+ ) => Promise<ModelRef[]>;
161
170
  settings?: Partial<AiPluginSettings>;
162
171
  onSettingsChange?: (
163
172
  patch: Partial<AiPluginSettings>,
@@ -196,7 +205,9 @@
196
205
  skillSnapshots,
197
206
  slashRouter,
198
207
  appToolHost,
199
- memoryRecall: settings?.memoryAutomaticRecall ? memoryRecall : undefined,
208
+ memoryRecall: settings?.memoryAutomaticRecall
209
+ ? memoryRecall
210
+ : undefined,
200
211
  handoffSummaries,
201
212
  skillContext,
202
213
  readVaultText: app
@@ -207,18 +218,52 @@
207
218
  : undefined,
208
219
  onComposerDefaults: ({ agent, runtimePreference }) => {
209
220
  if (runtimePreference === "fake") {
221
+ catalog.cancel();
222
+ catalogState = {
223
+ agent,
224
+ runtime: "fake",
225
+ models: [],
226
+ model: localModel ?? "",
227
+ loading: false,
228
+ error: null,
229
+ };
210
230
  localAgent = agent;
211
231
  localRuntime = "fake";
212
- persistComposerDefaults(agent, "fake", localModel, selectedThinking);
232
+ persistComposerDefaults(
233
+ agent,
234
+ "fake",
235
+ localModel,
236
+ selectedThinking,
237
+ );
213
238
  return;
214
239
  }
215
240
  changeAgent(agent, runtimePreference);
216
241
  },
217
242
  }),
218
243
  );
244
+ let catalogState = $state<ComposerModelCatalogState | null>(null);
245
+ const catalog = new ComposerModelCatalog(
246
+ (agent, runtime) => loadModels?.(agent, runtime) ?? Promise.resolve([]),
247
+ (state) => {
248
+ catalogState = state;
249
+ if (!state.loading && !state.error) {
250
+ localModel = state.model;
251
+ persistComposerDefaults(
252
+ state.agent,
253
+ state.runtime === "auto" ? "acp" : state.runtime,
254
+ state.model,
255
+ selectedThinking,
256
+ );
257
+ }
258
+ },
259
+ );
260
+ onDestroy(() => catalog.cancel());
261
+ const catalogModels = $derived(catalogState?.models ?? models);
219
262
  let draft = $state("");
220
263
  let localAgent = $state<AcpAgentId | null>(null);
221
- let localRuntime = $state<"acp" | "codex-native" | "fake" | null>(null);
264
+ let localRuntime = $state<
265
+ "acp" | "codex-native" | "opencode-native" | "fake" | null
266
+ >(null);
222
267
  let localModel = $state<string | null>(null);
223
268
  let localThinking = $state<AiThinkingLevel | null>(null);
224
269
  let attachments = $state<{ path: string; name: string }[]>([]);
@@ -243,9 +288,11 @@
243
288
  localRuntime ??
244
289
  (settings?.defaultRuntime === "codex-native"
245
290
  ? "codex-native"
246
- : settings?.defaultRuntime === "fake"
247
- ? "fake"
248
- : "acp"),
291
+ : settings?.defaultRuntime === "opencode-native"
292
+ ? "opencode-native"
293
+ : settings?.defaultRuntime === "fake"
294
+ ? "fake"
295
+ : "acp"),
249
296
  );
250
297
  const selectedModel = $derived(
251
298
  localModel ?? settings?.defaultModel ?? DEFAULT_AI_SETTINGS.defaultModel,
@@ -253,8 +300,63 @@
253
300
  const selectedThinking = $derived(
254
301
  localThinking ?? settings?.thinking ?? DEFAULT_AI_SETTINGS.thinking,
255
302
  );
303
+ // Settings notifications also reach a mounted composer. Own menu updates
304
+ // already match the local harness and must not trigger a second discovery.
305
+ let observedDefaults: {
306
+ agent: AcpAgentId;
307
+ runtime: typeof selectedRuntime;
308
+ model: string;
309
+ thinking: AiThinkingLevel;
310
+ } | null = null;
311
+ $effect(() => {
312
+ if (initializing) return;
313
+ const next = {
314
+ agent: normalizeAcpAgent(settings?.acpAgent),
315
+ runtime:
316
+ settings?.defaultRuntime === "codex-native"
317
+ ? ("codex-native" as const)
318
+ : settings?.defaultRuntime === "opencode-native"
319
+ ? ("opencode-native" as const)
320
+ : settings?.defaultRuntime === "fake"
321
+ ? ("fake" as const)
322
+ : ("acp" as const),
323
+ model: settings?.defaultModel ?? DEFAULT_AI_SETTINGS.defaultModel,
324
+ thinking: settings?.thinking ?? DEFAULT_AI_SETTINGS.thinking,
325
+ };
326
+ untrack(() => {
327
+ const previous = observedDefaults;
328
+ observedDefaults = next;
329
+ if (!previous) return;
330
+ const harnessChanged =
331
+ previous.agent !== next.agent || previous.runtime !== next.runtime;
332
+ if (
333
+ harnessChanged &&
334
+ ((localAgent ?? previous.agent) !== next.agent ||
335
+ (localRuntime ?? previous.runtime) !== next.runtime)
336
+ ) {
337
+ localAgent = next.agent;
338
+ localRuntime = next.runtime;
339
+ localModel = next.model;
340
+ catalog.cancel();
341
+ if (next.runtime === "fake") {
342
+ catalogState = {
343
+ ...next,
344
+ runtime: "fake",
345
+ models: [],
346
+ loading: false,
347
+ error: null,
348
+ };
349
+ } else if (loadModels) {
350
+ void catalog.refresh(next.agent, next.runtime, next.model);
351
+ }
352
+ } else if (previous.model !== next.model) {
353
+ localModel = next.model;
354
+ }
355
+ if (previous.thinking !== next.thinking) localThinking = next.thinking;
356
+ });
357
+ });
256
358
  const modelOptions = $derived.by<ModelRef[]>(() => {
257
- const available = catalogModelsForAgent(selectedAgent, models);
359
+ const available = catalogModelsForAgent(selectedAgent, catalogModels);
258
360
  if (
259
361
  selectedModel &&
260
362
  !available.some((model) => model.model === selectedModel)
@@ -299,21 +401,26 @@
299
401
  }
300
402
  return triggers;
301
403
  });
302
- const agentLabels = $derived.by(() =>
303
- new Map(
304
- controller.bindings.map((binding) => {
305
- const label =
306
- binding.runtime === "codex-native"
307
- ? "Codex Native"
308
- : binding.agent === "cursor"
309
- ? "Cursor ACP"
310
- : "Codex ACP";
311
- return [
312
- binding.id,
313
- binding.model?.model ? `${label} · ${binding.model.model}` : label,
314
- ] as const;
315
- }),
316
- ),
404
+ const agentLabels = $derived.by(
405
+ () =>
406
+ new Map(
407
+ controller.bindings.map((binding) => {
408
+ const label =
409
+ binding.runtime === "codex-native"
410
+ ? "Codex Native"
411
+ : binding.runtime === "opencode-native"
412
+ ? "OpenCode Native"
413
+ : binding.agent === "cursor"
414
+ ? "Cursor ACP"
415
+ : binding.agent === "opencode"
416
+ ? "OpenCode ACP"
417
+ : "Codex ACP";
418
+ return [
419
+ binding.id,
420
+ binding.model?.model ? `${label} · ${binding.model.model}` : label,
421
+ ] as const;
422
+ }),
423
+ ),
317
424
  );
318
425
  const timeline = $derived(
319
426
  groupChatItemsByDate(controller.items, new Date(), agentLabels),
@@ -340,8 +447,9 @@
340
447
  );
341
448
  const composerError = $derived(
342
449
  controller.error ??
343
- modelCatalogError ??
344
- unavailableReason ??
450
+ (catalogState
451
+ ? catalogState.error
452
+ : (modelCatalogError ?? unavailableReason)) ??
345
453
  controller.appToolsUnavailableReason,
346
454
  );
347
455
  const composerStatus = $derived<ComposerStatus | undefined>(
@@ -379,8 +487,8 @@
379
487
  });
380
488
 
381
489
  async function submit(prompt: string): Promise<void> {
382
- if (initializing) return;
383
- const selected = catalogModelsForAgent(selectedAgent, models).find(
490
+ if (initializing || catalogState?.loading) return;
491
+ const selected = catalogModelsForAgent(selectedAgent, catalogModels).find(
384
492
  (model) => model.model === selectedModel,
385
493
  );
386
494
  const extra = attachments.map((file) => file.path);
@@ -406,7 +514,7 @@
406
514
 
407
515
  function persistComposerDefaults(
408
516
  agent: AcpAgentId,
409
- runtimePreference: "acp" | "codex-native" | "fake",
517
+ runtimePreference: "acp" | "codex-native" | "opencode-native" | "fake",
410
518
  model: string | null,
411
519
  thinking: AiThinkingLevel,
412
520
  ): void {
@@ -420,7 +528,7 @@
420
528
 
421
529
  function changeAgent(
422
530
  agent: AcpAgentId,
423
- runtimePreference: "acp" | "codex-native",
531
+ runtimePreference: "acp" | "codex-native" | "opencode-native",
424
532
  ): void {
425
533
  localAgent = agent;
426
534
  localRuntime = runtimePreference;
@@ -430,13 +538,19 @@
430
538
  catalogModelsForAgent(agent, models).find((model) => model.isDefault)
431
539
  ?.model ||
432
540
  catalogModelsForAgent(agent, models)[0]?.model ||
433
- null;
541
+ "";
434
542
  persistComposerDefaults(
435
543
  agent,
436
544
  runtimePreference,
437
545
  localModel,
438
546
  selectedThinking,
439
547
  );
548
+ if (loadModels)
549
+ void catalog.refresh(
550
+ agent,
551
+ runtimePreference,
552
+ configured ?? localModel ?? "",
553
+ );
440
554
  }
441
555
 
442
556
  function changeModel(value: string): void {
@@ -549,7 +663,9 @@
549
663
  function toolCallProps(item: AiChatToolItem) {
550
664
  const hint = { toolName: item.name, input: item.input };
551
665
  const presentedError =
552
- item.state === "error" ? presentToolPayload(item.output, hint) : undefined;
666
+ item.state === "error"
667
+ ? presentToolPayload(item.output, hint)
668
+ : undefined;
553
669
  return {
554
670
  id: item.toolId,
555
671
  name: item.name,
@@ -689,18 +805,8 @@
689
805
  {@const detail = call.data as
690
806
  | { input?: string; output?: string; error?: string; name?: string }
691
807
  | undefined}
692
- {@const hint = {
693
- toolName: detail?.name ?? call.name,
694
- input: detail?.input,
695
- }}
696
808
  {@const ResultView = resolveToolResultView(app, detail?.name ?? call.name)}
697
809
  {@const parsedOutput = parseToolResultPayload(detail?.output)}
698
- {@const inputPayload = presentToolPayload(detail?.input)}
699
- {@const outputPayload = presentToolPayload(detail?.output, hint)}
700
- {@const errorPayload = presentToolPayload(detail?.error, hint)}
701
- {@const showErrorBlock = Boolean(
702
- errorPayload && !isOneLineAlert(errorPayload),
703
- )}
704
810
  {#if ResultView && app && parsedOutput != null && !detail?.error}
705
811
  <ResultView
706
812
  {app}
@@ -711,41 +817,12 @@
711
817
  state="completed"
712
818
  />
713
819
  {:else}
714
- <div class="ai-chat-panel__tool-detail">
715
- {#if inputPayload}
716
- <CodeBlock
717
- code={inputPayload.code}
718
- language={inputPayload.language}
719
- title="Input"
720
- size="sm"
721
- width="full"
722
- isWrapped
723
- maxHeight="16rem"
724
- />
725
- {/if}
726
- {#if outputPayload}
727
- <CodeBlock
728
- code={outputPayload.code}
729
- language={outputPayload.language}
730
- title="Output"
731
- size="sm"
732
- width="full"
733
- isWrapped
734
- maxHeight="16rem"
735
- />
736
- {/if}
737
- {#if showErrorBlock && errorPayload}
738
- <CodeBlock
739
- code={errorPayload.code}
740
- language={errorPayload.language}
741
- title="Error"
742
- size="sm"
743
- width="full"
744
- isWrapped
745
- maxHeight="16rem"
746
- />
747
- {/if}
748
- </div>
820
+ <Chat.ToolCallDetail
821
+ input={detail?.input}
822
+ output={detail?.output}
823
+ error={detail?.error}
824
+ toolName={detail?.name ?? call.name}
825
+ />
749
826
  {/if}
750
827
  {/snippet}
751
828
 
@@ -754,6 +831,7 @@
754
831
  data-ui-component="ai-chat-panel"
755
832
  data-testid="ai-chat-panel"
756
833
  data-initializing={initializing}
834
+ data-model-catalog-loading={catalogState?.loading ?? false}
757
835
  >
758
836
  <Chat.Layout density="compact" {isEmpty} aria-label="AI chat">
759
837
  {#snippet emptyState()}
@@ -786,7 +864,9 @@
786
864
  <Chat.Composer
787
865
  bind:value={draft}
788
866
  placeholder="Ask anything…"
789
- disabled={initializing || controller.busy}
867
+ disabled={initializing ||
868
+ Boolean(catalogState?.loading) ||
869
+ controller.busy}
790
870
  interactiveDrawerWhenDisabled={Boolean(pendingInteraction)}
791
871
  isStopShown={controller.busy}
792
872
  status={composerStatus}
@@ -890,7 +970,9 @@
890
970
  value={`${item.label} ${item.id}`}
891
971
  onSelect={() => addAttachment(item)}
892
972
  >
893
- <CommandView.ItemLabel>{item.label}</CommandView.ItemLabel>
973
+ <CommandView.ItemLabel
974
+ >{item.label}</CommandView.ItemLabel
975
+ >
894
976
  </CommandView.Item>
895
977
  {/each}
896
978
  </CommandView.List>
@@ -1029,15 +1111,29 @@
1029
1111
  <DropdownMenu.RadioItem
1030
1112
  value="acp:codex"
1031
1113
  onclick={() => changeAgent("codex", "acp")}
1032
- >Codex ACP</DropdownMenu.RadioItem>
1114
+ >Codex ACP</DropdownMenu.RadioItem
1115
+ >
1033
1116
  <DropdownMenu.RadioItem
1034
1117
  value="acp:cursor"
1035
1118
  onclick={() => changeAgent("cursor", "acp")}
1036
- >Cursor ACP</DropdownMenu.RadioItem>
1119
+ >Cursor ACP</DropdownMenu.RadioItem
1120
+ >
1037
1121
  <DropdownMenu.RadioItem
1038
1122
  value="codex-native:codex"
1039
1123
  onclick={() => changeAgent("codex", "codex-native")}
1040
- >Codex Native</DropdownMenu.RadioItem>
1124
+ >Codex Native</DropdownMenu.RadioItem
1125
+ >
1126
+ <DropdownMenu.RadioItem
1127
+ value="acp:opencode"
1128
+ onclick={() => changeAgent("opencode", "acp")}
1129
+ >OpenCode ACP</DropdownMenu.RadioItem
1130
+ >
1131
+ <DropdownMenu.RadioItem
1132
+ value="opencode-native:opencode"
1133
+ onclick={() =>
1134
+ changeAgent("opencode", "opencode-native")}
1135
+ >OpenCode Native</DropdownMenu.RadioItem
1136
+ >
1041
1137
  </DropdownMenu.RadioGroup>
1042
1138
  </DropdownMenu.SubContent>
1043
1139
  </DropdownMenu.Sub>
@@ -1047,7 +1143,13 @@
1047
1143
  Model
1048
1144
  </DropdownMenu.SubTrigger>
1049
1145
  <DropdownMenu.SubContent>
1050
- {#if modelOptions.length > 0}
1146
+ {#if catalogState?.loading}
1147
+ <DropdownMenu.Group
1148
+ ><DropdownMenu.Item disabled
1149
+ >Loading models…</DropdownMenu.Item
1150
+ ></DropdownMenu.Group
1151
+ >
1152
+ {:else if modelOptions.length > 0}
1051
1153
  <DropdownMenu.RadioGroup value={selectedModel}>
1052
1154
  {#each modelOptions as option (option.model)}
1053
1155
  <DropdownMenu.RadioItem
@@ -1154,8 +1256,7 @@
1154
1256
  <Chat.ToolCalls
1155
1257
  calls={entry.items.map((item) => ({
1156
1258
  ...toolCallProps(item),
1157
- detail:
1158
- item.input || item.output ? toolDetail : undefined,
1259
+ detail: item.input || item.output ? toolDetail : undefined,
1159
1260
  }))}
1160
1261
  defaultExpanded={false}
1161
1262
  />
@@ -1255,8 +1356,7 @@
1255
1356
  </Chat.MessageMetadata>
1256
1357
  {/snippet}
1257
1358
  </Chat.Message>
1258
- {:else if entry.item.type === "status" &&
1259
- entry.item.layout === "inventory"}
1359
+ {:else if entry.item.type === "status" && entry.item.layout === "inventory"}
1260
1360
  <div
1261
1361
  class="ai-chat-panel__command-notice"
1262
1362
  data-testid="ai-chat-command-notice"
@@ -4,6 +4,7 @@ import type { AgentRequest, AgentRuntime, ModelRef, McpServerContribution } from
4
4
  import type { AgentSessionStore } from "../sessions/session-store";
5
5
  import type { ConversationRepository, CreateConversationInput } from "../conversations/conversation-repository";
6
6
  import type { ConversationLocation } from "../conversations/types";
7
+ import { type AcpAgentId } from "../settings/acp-agents";
7
8
  import { type AiPluginSettings } from "../settings/ai-settings";
8
9
  import type { AppToolBridgeCoordinator } from "../tools/desktop-app-tool-bridge";
9
10
  import type { SkillRegistry, SkillSnapshotStore } from "../skills/registry";
@@ -42,6 +43,7 @@ type $$ComponentProps = {
42
43
  fileSearch?: ComposerSearchSource;
43
44
  models?: ModelRef[];
44
45
  modelCatalogError?: string | null;
46
+ loadModels?: (agent: AcpAgentId, runtime: AiPluginSettings["defaultRuntime"]) => Promise<ModelRef[]>;
45
47
  settings?: Partial<AiPluginSettings>;
46
48
  onSettingsChange?: (patch: Partial<AiPluginSettings>) => void | Promise<void>;
47
49
  };
@@ -11,3 +11,4 @@ export type AiViewBootstrap = {
11
11
  };
12
12
  export declare function initialAiViewBootstrap(host: AiViewHost): AiViewBootstrap;
13
13
  export declare function prepareAiViewBootstrap(host: AiViewHost, initialLocation: ConversationLocation | null, mcpServers: McpServerContribution[]): Promise<AiViewBootstrap>;
14
+ export declare function applyAiViewSettingsPatch(settings: AiPluginSettings, patch: Partial<AiPluginSettings>, current: AiPluginSettings): AiPluginSettings;
@@ -1,4 +1,3 @@
1
- import { ACP_AGENT_IDS } from "../settings/acp-agents";
2
1
  export function initialAiViewBootstrap(host) {
3
2
  return {
4
3
  runtime: host.fallbackRuntime(),
@@ -13,32 +12,36 @@ export async function prepareAiViewBootstrap(host, initialLocation, mcpServers)
13
12
  const conversation = initialLocation
14
13
  ? host.conversations.read(initialLocation)
15
14
  : Promise.resolve(null);
16
- const catalogs = ACP_AGENT_IDS.map((agent) => host.models.listModels(agent));
17
- const [snapshot, settledCatalogs] = await Promise.all([
18
- conversation.catch(() => null),
19
- Promise.allSettled(catalogs),
20
- ]);
15
+ const snapshot = await conversation.catch(() => null);
21
16
  if (snapshot) {
22
17
  const binding = snapshot.agents.find((record) => record.type === "binding.created" &&
23
18
  record.id === snapshot.metadata.activeAgentBindingId);
24
19
  if (binding?.type === "binding.created") {
25
- const agent = binding.agent === "cursor" ? "cursor" : "codex";
20
+ const agent = binding.agent === "cursor"
21
+ ? "cursor"
22
+ : binding.agent === "opencode"
23
+ ? "opencode"
24
+ : "codex";
26
25
  settings = {
27
26
  ...settings,
28
27
  acpAgent: agent,
29
28
  defaultRuntime: binding.runtime === "codex-native"
30
29
  ? "codex-native"
31
- : binding.runtime === "fake"
32
- ? "fake"
33
- : "acp",
30
+ : binding.runtime === "opencode-native"
31
+ ? "opencode-native"
32
+ : binding.runtime === "fake"
33
+ ? "fake"
34
+ : "acp",
34
35
  defaultModel: binding.model?.model ?? settings.defaultModels[agent],
35
36
  thinking: binding.thinking ?? settings.thinking,
36
37
  };
37
38
  }
38
39
  }
39
- const models = settledCatalogs.flatMap((catalog) => catalog.status === "fulfilled" ? catalog.value : []);
40
+ const [selectedCatalog] = await Promise.allSettled([
41
+ host.models.listModels(settings.acpAgent, settings.defaultRuntime),
42
+ ]);
43
+ const models = selectedCatalog?.status === "fulfilled" ? selectedCatalog.value : [];
40
44
  let modelCatalogError = null;
41
- const selectedCatalog = settledCatalogs[ACP_AGENT_IDS.indexOf(settings.acpAgent)];
42
45
  const agentModels = models.filter((model) => model.provider === settings.acpAgent);
43
46
  if (selectedCatalog?.status === "rejected") {
44
47
  modelCatalogError = errorMessage(selectedCatalog.reason);
@@ -56,6 +59,7 @@ export async function prepareAiViewBootstrap(host, initialLocation, mcpServers)
56
59
  try {
57
60
  runtime = await host.selectRuntime({
58
61
  prompt: "",
62
+ workspace: host.workspace,
59
63
  mcpServers,
60
64
  agent: settings.acpAgent,
61
65
  model: { provider: settings.acpAgent, model: settings.defaultModel },
@@ -84,3 +88,19 @@ export async function prepareAiViewBootstrap(host, initialLocation, mcpServers)
84
88
  function errorMessage(error) {
85
89
  return error instanceof Error ? error.message : String(error);
86
90
  }
91
+ export function applyAiViewSettingsPatch(settings, patch, current) {
92
+ const acpAgent = patch.acpAgent ?? settings.acpAgent;
93
+ const defaultModels = { ...settings.defaultModels, ...patch.defaultModels };
94
+ if (patch.defaultModel !== undefined)
95
+ defaultModels[acpAgent] = patch.defaultModel;
96
+ return {
97
+ ...settings,
98
+ ...patch,
99
+ acpAgent,
100
+ defaultModels,
101
+ defaultModel: patch.defaultModel ??
102
+ (patch.acpAgent !== undefined
103
+ ? current.defaultModels[acpAgent]
104
+ : (patch.defaultModels?.[acpAgent] ?? settings.defaultModel)),
105
+ };
106
+ }
@@ -6,6 +6,7 @@
6
6
  import type { AiViewHost } from "./ai-view";
7
7
  import {
8
8
  initialAiViewBootstrap,
9
+ applyAiViewSettingsPatch,
9
10
  prepareAiViewBootstrap,
10
11
  type AiViewBootstrap,
11
12
  } from "./ai-view-bootstrap";
@@ -34,6 +35,17 @@
34
35
 
35
36
  onMount(() => {
36
37
  let disposed = false;
38
+ const unsubscribe = host.subscribeSettings?.((patch) => {
39
+ if (disposed || !preparedBootstrap) return;
40
+ preparedBootstrap = {
41
+ ...preparedBootstrap,
42
+ settings: applyAiViewSettingsPatch(
43
+ preparedBootstrap.settings,
44
+ patch,
45
+ host.getSettings(),
46
+ ),
47
+ };
48
+ });
37
49
  void prepareAiViewBootstrap(host, initialLocation, mcpServers)
38
50
  .then((prepared) => {
39
51
  if (disposed) return;
@@ -51,6 +63,7 @@
51
63
  });
52
64
  return () => {
53
65
  disposed = true;
66
+ unsubscribe?.();
54
67
  };
55
68
  });
56
69
  </script>
@@ -83,6 +96,7 @@
83
96
  {workspaceLeaf}
84
97
  fileSearch={host.searchVaultFiles}
85
98
  models={bootstrap.models}
99
+ loadModels={(agent, runtime) => host.models.listModels(agent, runtime)}
86
100
  modelCatalogError={bootstrap.modelCatalogError}
87
101
  settings={bootstrap.settings}
88
102
  onSettingsChange={(patch) => host.updateSettings(patch)}
@@ -31,7 +31,7 @@ export type AiViewHost = {
31
31
  updateSettings(patch: Partial<AiPluginSettings>): Promise<void>;
32
32
  subscribeSettings?(listener: (patch: Partial<AiPluginSettings>) => void): () => void;
33
33
  models: {
34
- listModels(provider: string): Promise<ModelRef[]>;
34
+ listModels(provider: string, runtime?: string): Promise<ModelRef[]>;
35
35
  };
36
36
  workspace?: string;
37
37
  };
@@ -57,7 +57,7 @@ export declare class AiChatController {
57
57
  handoffSummaries?: Pick<HandoffSummaryCoordinator, "afterTerminal">;
58
58
  onComposerDefaults?: (next: {
59
59
  agent: AcpAgentId;
60
- runtimePreference: "acp" | "codex-native" | "fake";
60
+ runtimePreference: "acp" | "codex-native" | "opencode-native" | "fake";
61
61
  }) => void;
62
62
  });
63
63
  restore(): Promise<void>;