@lapis-notes/ai 0.1.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.
Files changed (266) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE.md +8 -0
  3. package/README.md +20 -0
  4. package/dist/ai-plugin.d.ts +88 -0
  5. package/dist/ai-plugin.js +742 -0
  6. package/dist/bootstrap/build.d.ts +15 -0
  7. package/dist/bootstrap/build.js +28 -0
  8. package/dist/bootstrap/folder-instructions.d.ts +4 -0
  9. package/dist/bootstrap/folder-instructions.js +49 -0
  10. package/dist/bootstrap/format.d.ts +2 -0
  11. package/dist/bootstrap/format.js +46 -0
  12. package/dist/bootstrap/instructions.d.ts +1 -0
  13. package/dist/bootstrap/instructions.js +8 -0
  14. package/dist/bootstrap/types.d.ts +36 -0
  15. package/dist/bootstrap/types.js +3 -0
  16. package/dist/catalog/ai-catalog-panel.svelte +742 -0
  17. package/dist/catalog/ai-catalog-panel.svelte.d.ts +13 -0
  18. package/dist/catalog/ai-catalog-view-type.d.ts +1 -0
  19. package/dist/catalog/ai-catalog-view-type.js +1 -0
  20. package/dist/catalog/ai-catalog-view.d.ts +22 -0
  21. package/dist/catalog/ai-catalog-view.js +48 -0
  22. package/dist/catalog/filter.d.ts +3 -0
  23. package/dist/catalog/filter.js +24 -0
  24. package/dist/catalog/inventory.d.ts +21 -0
  25. package/dist/catalog/inventory.js +255 -0
  26. package/dist/catalog/tree.d.ts +12 -0
  27. package/dist/catalog/tree.js +60 -0
  28. package/dist/catalog/types.d.ts +44 -0
  29. package/dist/catalog/types.js +1 -0
  30. package/dist/chat/ai-approval-card.svelte +54 -0
  31. package/dist/chat/ai-approval-card.svelte.d.ts +9 -0
  32. package/dist/chat/ai-chat-panel.svelte +1288 -0
  33. package/dist/chat/ai-chat-panel.svelte.d.ts +50 -0
  34. package/dist/chat/ai-inventory-result.css +55 -0
  35. package/dist/chat/ai-inventory-result.svelte +63 -0
  36. package/dist/chat/ai-inventory-result.svelte.d.ts +5 -0
  37. package/dist/chat/ai-question-card.svelte +132 -0
  38. package/dist/chat/ai-question-card.svelte.d.ts +9 -0
  39. package/dist/chat/ai-view-bootstrap.d.ts +13 -0
  40. package/dist/chat/ai-view-bootstrap.js +86 -0
  41. package/dist/chat/ai-view-panel.svelte +89 -0
  42. package/dist/chat/ai-view-panel.svelte.d.ts +13 -0
  43. package/dist/chat/ai-view-type.d.ts +1 -0
  44. package/dist/chat/ai-view-type.js +1 -0
  45. package/dist/chat/ai-view.d.ts +60 -0
  46. package/dist/chat/ai-view.js +121 -0
  47. package/dist/chat/chat-controller.svelte.d.ts +82 -0
  48. package/dist/chat/chat-controller.svelte.js +1926 -0
  49. package/dist/chat/chat-items.d.ts +88 -0
  50. package/dist/chat/chat-items.js +9 -0
  51. package/dist/chat/chat-mentions.d.ts +15 -0
  52. package/dist/chat/chat-mentions.js +77 -0
  53. package/dist/chat/chat-result-views.d.ts +5 -0
  54. package/dist/chat/chat-result-views.js +15 -0
  55. package/dist/chat/chat-session.d.ts +28 -0
  56. package/dist/chat/chat-session.js +50 -0
  57. package/dist/chat/chat-status.d.ts +1 -0
  58. package/dist/chat/chat-status.js +3 -0
  59. package/dist/chat/chat-time.d.ts +18 -0
  60. package/dist/chat/chat-time.js +90 -0
  61. package/dist/chat/chat-tool-display.d.ts +17 -0
  62. package/dist/chat/chat-tool-display.js +213 -0
  63. package/dist/chat/chat-tool-identity.d.ts +4 -0
  64. package/dist/chat/chat-tool-identity.js +42 -0
  65. package/dist/chat/chat-trace.d.ts +6 -0
  66. package/dist/chat/chat-trace.js +256 -0
  67. package/dist/chat/live-runtime-unavailable.d.ts +1 -0
  68. package/dist/chat/live-runtime-unavailable.js +1 -0
  69. package/dist/commands/agent.d.ts +8 -0
  70. package/dist/commands/agent.js +38 -0
  71. package/dist/commands/catalog.d.ts +18 -0
  72. package/dist/commands/catalog.js +115 -0
  73. package/dist/commands/discovery.d.ts +36 -0
  74. package/dist/commands/discovery.js +151 -0
  75. package/dist/commands/groups.d.ts +16 -0
  76. package/dist/commands/groups.js +121 -0
  77. package/dist/commands/inspect.d.ts +18 -0
  78. package/dist/commands/inspect.js +29 -0
  79. package/dist/commands/markdown.d.ts +21 -0
  80. package/dist/commands/markdown.js +117 -0
  81. package/dist/commands/parser.d.ts +4 -0
  82. package/dist/commands/parser.js +24 -0
  83. package/dist/commands/reserved.d.ts +4 -0
  84. package/dist/commands/reserved.js +107 -0
  85. package/dist/commands/router.d.ts +17 -0
  86. package/dist/commands/router.js +170 -0
  87. package/dist/commands/seed.d.ts +4 -0
  88. package/dist/commands/seed.js +8 -0
  89. package/dist/commands/types.d.ts +54 -0
  90. package/dist/commands/types.js +1 -0
  91. package/dist/commands/user-agents-node.d.ts +3 -0
  92. package/dist/commands/user-agents-node.js +52 -0
  93. package/dist/commands/user-agents.d.ts +22 -0
  94. package/dist/commands/user-agents.js +40 -0
  95. package/dist/conversations/approval-grants.d.ts +20 -0
  96. package/dist/conversations/approval-grants.js +75 -0
  97. package/dist/conversations/binding-state.d.ts +6 -0
  98. package/dist/conversations/binding-state.js +35 -0
  99. package/dist/conversations/context-handoff.d.ts +25 -0
  100. package/dist/conversations/context-handoff.js +207 -0
  101. package/dist/conversations/conversation-index-coordinator.d.ts +18 -0
  102. package/dist/conversations/conversation-index-coordinator.js +81 -0
  103. package/dist/conversations/conversation-index.d.ts +16 -0
  104. package/dist/conversations/conversation-index.js +125 -0
  105. package/dist/conversations/conversation-locator.d.ts +2 -0
  106. package/dist/conversations/conversation-locator.js +20 -0
  107. package/dist/conversations/conversation-palette.d.ts +18 -0
  108. package/dist/conversations/conversation-palette.js +29 -0
  109. package/dist/conversations/conversation-repository.d.ts +39 -0
  110. package/dist/conversations/conversation-repository.js +213 -0
  111. package/dist/conversations/handoff-summary.d.ts +48 -0
  112. package/dist/conversations/handoff-summary.js +269 -0
  113. package/dist/conversations/hashes.d.ts +5 -0
  114. package/dist/conversations/hashes.js +40 -0
  115. package/dist/conversations/history-creation-scope.d.ts +12 -0
  116. package/dist/conversations/history-creation-scope.js +44 -0
  117. package/dist/conversations/jsonl.d.ts +10 -0
  118. package/dist/conversations/jsonl.js +41 -0
  119. package/dist/conversations/memory-transcript-store.d.ts +15 -0
  120. package/dist/conversations/memory-transcript-store.js +119 -0
  121. package/dist/conversations/paths.d.ts +14 -0
  122. package/dist/conversations/paths.js +92 -0
  123. package/dist/conversations/redaction.d.ts +13 -0
  124. package/dist/conversations/redaction.js +94 -0
  125. package/dist/conversations/reveal-scope.d.ts +3 -0
  126. package/dist/conversations/reveal-scope.js +8 -0
  127. package/dist/conversations/scope-resolver.d.ts +11 -0
  128. package/dist/conversations/scope-resolver.js +18 -0
  129. package/dist/conversations/scope-tree.d.ts +10 -0
  130. package/dist/conversations/scope-tree.js +71 -0
  131. package/dist/conversations/transcript-projection.d.ts +9 -0
  132. package/dist/conversations/transcript-projection.js +409 -0
  133. package/dist/conversations/transcript-store.d.ts +24 -0
  134. package/dist/conversations/transcript-store.js +27 -0
  135. package/dist/conversations/types.d.ts +224 -0
  136. package/dist/conversations/types.js +12 -0
  137. package/dist/conversations/validation.d.ts +4 -0
  138. package/dist/conversations/validation.js +383 -0
  139. package/dist/conversations/vault-transcript-store.d.ts +22 -0
  140. package/dist/conversations/vault-transcript-store.js +219 -0
  141. package/dist/core/event-queue.d.ts +6 -0
  142. package/dist/core/event-queue.js +39 -0
  143. package/dist/core/types.d.ts +241 -0
  144. package/dist/core/types.js +50 -0
  145. package/dist/history/ai-history-panel.svelte +937 -0
  146. package/dist/history/ai-history-panel.svelte.d.ts +17 -0
  147. package/dist/history/ai-history-view-type.d.ts +1 -0
  148. package/dist/history/ai-history-view-type.js +1 -0
  149. package/dist/history/ai-history-view.d.ts +26 -0
  150. package/dist/history/ai-history-view.js +51 -0
  151. package/dist/history/conversation-history-tree.d.ts +15 -0
  152. package/dist/history/conversation-history-tree.js +71 -0
  153. package/dist/host/agent-workspace.d.ts +6 -0
  154. package/dist/host/agent-workspace.js +16 -0
  155. package/dist/host/create-host-runtimes.d.ts +2 -0
  156. package/dist/host/create-host-runtimes.js +14 -0
  157. package/dist/host/desktop-process-host.d.ts +6 -0
  158. package/dist/host/desktop-process-host.js +74 -0
  159. package/dist/host/host-runtime-availability.d.ts +1 -0
  160. package/dist/host/host-runtime-availability.js +3 -0
  161. package/dist/host/process-host.d.ts +31 -0
  162. package/dist/host/process-host.js +6 -0
  163. package/dist/index.d.ts +69 -0
  164. package/dist/index.js +49 -0
  165. package/dist/jsonl/ai-jsonl-preview.d.ts +27 -0
  166. package/dist/jsonl/ai-jsonl-preview.js +68 -0
  167. package/dist/jsonl/ai-jsonl-view-type.d.ts +1 -0
  168. package/dist/jsonl/ai-jsonl-view-type.js +1 -0
  169. package/dist/jsonl/ai-jsonl-view.d.ts +16 -0
  170. package/dist/jsonl/ai-jsonl-view.js +57 -0
  171. package/dist/jsonl/ai-jsonl-view.svelte +260 -0
  172. package/dist/jsonl/ai-jsonl-view.svelte.d.ts +9 -0
  173. package/dist/memory/candidate-extractor.d.ts +25 -0
  174. package/dist/memory/candidate-extractor.js +191 -0
  175. package/dist/memory/forget-confirmation.d.ts +3 -0
  176. package/dist/memory/forget-confirmation.js +45 -0
  177. package/dist/memory/hashes.d.ts +5 -0
  178. package/dist/memory/hashes.js +37 -0
  179. package/dist/memory/memory-ingestion-coordinator.d.ts +19 -0
  180. package/dist/memory/memory-ingestion-coordinator.js +108 -0
  181. package/dist/memory/memory-record-store.d.ts +62 -0
  182. package/dist/memory/memory-record-store.js +369 -0
  183. package/dist/memory/memory-scheduler.d.ts +21 -0
  184. package/dist/memory/memory-scheduler.js +143 -0
  185. package/dist/memory/memory-tools.d.ts +61 -0
  186. package/dist/memory/memory-tools.js +99 -0
  187. package/dist/memory/native-memory-service.d.ts +72 -0
  188. package/dist/memory/native-memory-service.js +1256 -0
  189. package/dist/memory/paths.d.ts +13 -0
  190. package/dist/memory/paths.js +60 -0
  191. package/dist/memory/runtime-consolidation-provider.d.ts +18 -0
  192. package/dist/memory/runtime-consolidation-provider.js +154 -0
  193. package/dist/memory/types.d.ts +139 -0
  194. package/dist/memory/types.js +1 -0
  195. package/dist/providers/acp-model-provider.d.ts +11 -0
  196. package/dist/providers/acp-model-provider.js +99 -0
  197. package/dist/providers/codex-model-catalog.d.ts +7 -0
  198. package/dist/providers/codex-model-catalog.js +179 -0
  199. package/dist/providers/codex-model-provider.d.ts +12 -0
  200. package/dist/providers/codex-model-provider.js +42 -0
  201. package/dist/providers/model-provider.d.ts +26 -0
  202. package/dist/providers/model-provider.js +26 -0
  203. package/dist/public-components.d.ts +3 -0
  204. package/dist/public-components.js +3 -0
  205. package/dist/registry/runtime-registry.d.ts +11 -0
  206. package/dist/registry/runtime-registry.js +71 -0
  207. package/dist/registry/select-runtime.d.ts +9 -0
  208. package/dist/registry/select-runtime.js +31 -0
  209. package/dist/runtime-adapters.d.ts +16 -0
  210. package/dist/runtime-adapters.js +9 -0
  211. package/dist/runtimes/acp/acp-event-mapper.d.ts +56 -0
  212. package/dist/runtimes/acp/acp-event-mapper.js +237 -0
  213. package/dist/runtimes/acp/acp-runtime.d.ts +46 -0
  214. package/dist/runtimes/acp/acp-runtime.js +165 -0
  215. package/dist/runtimes/acp/desktop-acp-backend.d.ts +16 -0
  216. package/dist/runtimes/acp/desktop-acp-backend.js +243 -0
  217. package/dist/runtimes/codex/app-server-protocol.d.ts +28 -0
  218. package/dist/runtimes/codex/app-server-protocol.js +274 -0
  219. package/dist/runtimes/codex/codex-runtime.d.ts +24 -0
  220. package/dist/runtimes/codex/codex-runtime.js +318 -0
  221. package/dist/runtimes/fake/fake-runtime.d.ts +51 -0
  222. package/dist/runtimes/fake/fake-runtime.js +257 -0
  223. package/dist/sessions/plugin-data.d.ts +8 -0
  224. package/dist/sessions/plugin-data.js +29 -0
  225. package/dist/sessions/session-store.d.ts +47 -0
  226. package/dist/sessions/session-store.js +117 -0
  227. package/dist/settings/acp-agents.d.ts +8 -0
  228. package/dist/settings/acp-agents.js +11 -0
  229. package/dist/settings/ai-settings-tab.d.ts +8 -0
  230. package/dist/settings/ai-settings-tab.js +249 -0
  231. package/dist/settings/ai-settings.d.ts +43 -0
  232. package/dist/settings/ai-settings.js +147 -0
  233. package/dist/settings/app-tool-setting-rows.d.ts +16 -0
  234. package/dist/settings/app-tool-setting-rows.js +30 -0
  235. package/dist/settings/register-ai-settings.d.ts +3 -0
  236. package/dist/settings/register-ai-settings.js +393 -0
  237. package/dist/skills/activation.d.ts +2 -0
  238. package/dist/skills/activation.js +20 -0
  239. package/dist/skills/bundled/lapis-notes.d.ts +3 -0
  240. package/dist/skills/bundled/lapis-notes.js +52 -0
  241. package/dist/skills/bundled/research.d.ts +6 -0
  242. package/dist/skills/bundled/research.js +29 -0
  243. package/dist/skills/discovery.d.ts +21 -0
  244. package/dist/skills/discovery.js +227 -0
  245. package/dist/skills/manifest.d.ts +3 -0
  246. package/dist/skills/manifest.js +39 -0
  247. package/dist/skills/parser.d.ts +12 -0
  248. package/dist/skills/parser.js +124 -0
  249. package/dist/skills/registry.d.ts +21 -0
  250. package/dist/skills/registry.js +89 -0
  251. package/dist/skills/seed.d.ts +5 -0
  252. package/dist/skills/seed.js +18 -0
  253. package/dist/skills/skill-tools.d.ts +8 -0
  254. package/dist/skills/skill-tools.js +113 -0
  255. package/dist/skills/types.d.ts +67 -0
  256. package/dist/skills/types.js +1 -0
  257. package/dist/styles.css +514 -0
  258. package/dist/tools/app-tool-host.d.ts +47 -0
  259. package/dist/tools/app-tool-host.js +419 -0
  260. package/dist/tools/desktop-app-tool-bridge.d.ts +23 -0
  261. package/dist/tools/desktop-app-tool-bridge.js +223 -0
  262. package/dist/tools/mcp-server-registry.d.ts +7 -0
  263. package/dist/tools/mcp-server-registry.js +30 -0
  264. package/manifest.json +34 -0
  265. package/package.json +83 -0
  266. package/styles.css +1 -0
@@ -0,0 +1,1288 @@
1
+ <script lang="ts">
2
+ import { onMount, untrack } from "svelte";
3
+ import type { App, WorkspaceLeaf } from "@lapis-notes/api";
4
+ import { MarkdownEmbed } from "@lapis-notes/markdown/embed";
5
+ import * as Chat from "@lapismd/design-core/ai/chat";
6
+ import { Reasoning } from "@lapismd/design-core/ai/experimental";
7
+ import { Button } from "@lapismd/design-core/shadcn/button";
8
+ import { CodeBlock } from "@lapismd/design-core/shadcn/code-block";
9
+ import * as CommandView from "@lapismd/design-core/shadcn/command-view";
10
+ import * as DropdownMenu from "@lapismd/design-core/shadcn/dropdown-menu";
11
+ import * as Empty from "@lapismd/design-core/shadcn/empty";
12
+ import * as Popover from "@lapismd/design-core/shadcn/popover";
13
+ import { Spinner } from "@lapismd/design-core/shadcn/spinner";
14
+ import type {
15
+ ComposerSearchSource,
16
+ ComposerStatus,
17
+ ComposerTrigger,
18
+ ComposerTriggerItem,
19
+ } from "@lapismd/design-core/ai/chat";
20
+ import ArchiveIcon from "@lucide/svelte/icons/archive";
21
+ import ArchiveRestoreIcon from "@lucide/svelte/icons/archive-restore";
22
+ import BrainIcon from "@lucide/svelte/icons/brain";
23
+ import CopyIcon from "@lucide/svelte/icons/copy";
24
+ import FolderIcon from "@lucide/svelte/icons/folder";
25
+ import PinIcon from "@lucide/svelte/icons/pin";
26
+ import PinOffIcon from "@lucide/svelte/icons/pin-off";
27
+ import SparklesIcon from "@lucide/svelte/icons/sparkles";
28
+ import HistoryIcon from "@lucide/svelte/icons/history";
29
+ import MoreHorizontalIcon from "@lucide/svelte/icons/ellipsis";
30
+ import PaperclipIcon from "@lucide/svelte/icons/paperclip";
31
+ import PlusIcon from "@lucide/svelte/icons/plus";
32
+ import RedoIcon from "@lucide/svelte/icons/redo-2";
33
+ import TrashIcon from "@lucide/svelte/icons/trash-2";
34
+ import XIcon from "@lucide/svelte/icons/x";
35
+ import type {
36
+ AgentRequest,
37
+ AgentRuntime,
38
+ AiThinkingLevel,
39
+ ModelRef,
40
+ McpServerContribution,
41
+ } from "../core/types";
42
+ import type { AgentSessionStore } from "../sessions/session-store";
43
+ import type {
44
+ ConversationRepository,
45
+ CreateConversationInput,
46
+ } from "../conversations/conversation-repository";
47
+ import type { ConversationLocation } from "../conversations/types";
48
+ import { normalizeConversationScope } from "../conversations/paths";
49
+ import {
50
+ formatDirectoryContextLabel,
51
+ groupConversationsByRelativeScope,
52
+ } from "../conversations/scope-tree";
53
+ import { revealConversationScope } from "../conversations/reveal-scope";
54
+ import {
55
+ catalogModelsForAgent,
56
+ normalizeAcpAgent,
57
+ type AcpAgentId,
58
+ } from "../settings/acp-agents";
59
+ import {
60
+ DEFAULT_AI_SETTINGS,
61
+ type AiPluginSettings,
62
+ } from "../settings/ai-settings";
63
+ import type { AppToolBridgeCoordinator } from "../tools/desktop-app-tool-bridge";
64
+ import { formatFileMention, mentionTokensFromText } from "./chat-mentions";
65
+ import { isSlashCommandNotice } from "./chat-items";
66
+ import {
67
+ parseToolResultPayload,
68
+ resolveToolResultView,
69
+ } from "./chat-result-views";
70
+ import AiInventoryResult from "./ai-inventory-result.svelte";
71
+ import { formatChatTimestamp, groupChatItemsByDate } from "./chat-time";
72
+ import { shouldShowWorkingIndicator } from "./chat-status";
73
+ import {
74
+ isOneLineAlert,
75
+ presentToolPayload,
76
+ toolCallStatus,
77
+ toolCallTarget,
78
+ type AiChatToolItem,
79
+ } from "./chat-tool-display";
80
+ import AiApprovalCard from "./ai-approval-card.svelte";
81
+ import AiQuestionCard from "./ai-question-card.svelte";
82
+ import { AiChatController } from "./chat-controller.svelte";
83
+ import type { SkillRegistry, SkillSnapshotStore } from "../skills/registry";
84
+ import type { SkillDiscoveryContext } from "../skills/types";
85
+ import type { SlashCommandRouter } from "../commands/router";
86
+ import { composerAgentLabel } from "../commands/agent";
87
+ import {
88
+ composerSlashItems,
89
+ filterComposerSlashItems,
90
+ } from "../commands/groups";
91
+ import type { AppToolHost } from "../tools/app-tool-host";
92
+ import type { AutomaticMemoryRecall } from "../memory/types";
93
+ import type { HandoffSummaryCoordinator } from "../conversations/handoff-summary";
94
+
95
+ let {
96
+ app,
97
+ runtime,
98
+ selectRuntime,
99
+ unavailableReason = null,
100
+ initializing = false,
101
+ workspace,
102
+ mcpServers = [],
103
+ appToolBridge,
104
+ skills,
105
+ skillSnapshots,
106
+ slashRouter,
107
+ appToolHost,
108
+ memoryRecall,
109
+ handoffSummaries,
110
+ skillContext,
111
+ sessionStore,
112
+ sessionId,
113
+ repository,
114
+ initialLocation = null,
115
+ createConversation,
116
+ subscribeConversationMoves,
117
+ onRevealHistory,
118
+ onConversationLocationChange,
119
+ currentConversationScope,
120
+ listConversationFolders,
121
+ workspaceLeaf,
122
+ fileSearch,
123
+ models = [],
124
+ modelCatalogError = null,
125
+ settings,
126
+ onSettingsChange,
127
+ }: {
128
+ app?: App;
129
+ runtime: AgentRuntime;
130
+ selectRuntime?: (request: AgentRequest) => Promise<AgentRuntime>;
131
+ unavailableReason?: string | null;
132
+ initializing?: boolean;
133
+ workspace?: string;
134
+ mcpServers?: McpServerContribution[];
135
+ appToolBridge?: AppToolBridgeCoordinator;
136
+ skills?: SkillRegistry;
137
+ skillSnapshots?: SkillSnapshotStore;
138
+ slashRouter?: SlashCommandRouter;
139
+ appToolHost?: AppToolHost;
140
+ memoryRecall?: AutomaticMemoryRecall;
141
+ handoffSummaries?: Pick<HandoffSummaryCoordinator, "afterTerminal">;
142
+ skillContext?: () => SkillDiscoveryContext;
143
+ sessionStore?: AgentSessionStore;
144
+ sessionId?: string;
145
+ repository?: ConversationRepository;
146
+ initialLocation?: ConversationLocation | null;
147
+ createConversation?: (explicitFolder?: string) => CreateConversationInput;
148
+ subscribeConversationMoves?: (
149
+ listener: (oldPath: string, newPath: string) => void,
150
+ ) => () => void;
151
+ onRevealHistory?: () => void | Promise<void>;
152
+ onConversationLocationChange?: (
153
+ location: ConversationLocation | null,
154
+ ) => void;
155
+ currentConversationScope?: () => string;
156
+ listConversationFolders?: () => string[];
157
+ workspaceLeaf?: WorkspaceLeaf;
158
+ fileSearch?: ComposerSearchSource;
159
+ models?: ModelRef[];
160
+ modelCatalogError?: string | null;
161
+ settings?: Partial<AiPluginSettings>;
162
+ onSettingsChange?: (
163
+ patch: Partial<AiPluginSettings>,
164
+ ) => void | Promise<void>;
165
+ } = $props();
166
+
167
+ const controller = untrack(
168
+ () =>
169
+ new AiChatController(runtime, unavailableReason, mcpServers, {
170
+ store: sessionStore,
171
+ sessionId,
172
+ workspace,
173
+ request: {
174
+ agent: settings?.acpAgent,
175
+ model: settings?.defaultModel
176
+ ? {
177
+ provider: normalizeAcpAgent(settings?.acpAgent),
178
+ model: settings.defaultModel,
179
+ }
180
+ : undefined,
181
+ thinking: settings?.thinking,
182
+ metadata: {
183
+ runtime: settings?.defaultRuntime,
184
+ },
185
+ },
186
+ repository,
187
+ location: initialLocation,
188
+ createConversation: (explicitFolder) =>
189
+ createConversation?.(explicitFolder) ?? {
190
+ scopeDir: explicitFolder ?? "",
191
+ },
192
+ onLocationChange: onConversationLocationChange,
193
+ selectRuntime,
194
+ appToolBridge,
195
+ skills,
196
+ skillSnapshots,
197
+ slashRouter,
198
+ appToolHost,
199
+ memoryRecall: settings?.memoryAutomaticRecall ? memoryRecall : undefined,
200
+ handoffSummaries,
201
+ skillContext,
202
+ readVaultText: app
203
+ ? async (path) => {
204
+ const file = app.vault.getFileByPath(path);
205
+ return file ? app.vault.cachedRead(file) : undefined;
206
+ }
207
+ : undefined,
208
+ onComposerDefaults: ({ agent, runtimePreference }) => {
209
+ if (runtimePreference === "fake") {
210
+ localAgent = agent;
211
+ localRuntime = "fake";
212
+ persistComposerDefaults(agent, "fake", localModel, selectedThinking);
213
+ return;
214
+ }
215
+ changeAgent(agent, runtimePreference);
216
+ },
217
+ }),
218
+ );
219
+ let draft = $state("");
220
+ let localAgent = $state<AcpAgentId | null>(null);
221
+ let localRuntime = $state<"acp" | "codex-native" | "fake" | null>(null);
222
+ let localModel = $state<string | null>(null);
223
+ let localThinking = $state<AiThinkingLevel | null>(null);
224
+ let attachments = $state<{ path: string; name: string }[]>([]);
225
+ let drawerCollapsed = $state(false);
226
+ let drawerHost = $state<HTMLDivElement | null>(null);
227
+ let visibleInteractionId = $state<string | null>(null);
228
+ let attachOpen = $state(false);
229
+ let attachItems = $state<ComposerTriggerItem[]>([]);
230
+ let scopePickerOpen = $state(false);
231
+ let scopeFolders = $state<string[]>([""]);
232
+ const attachSideOffset = $derived.by(() => {
233
+ void attachments.length;
234
+ void drawerCollapsed;
235
+ if (!drawerHost) return 8;
236
+ const height = drawerHost.getBoundingClientRect().height;
237
+ return height > 0 ? Math.round(height) + 8 : 8;
238
+ });
239
+ const selectedAgent = $derived(
240
+ localAgent ?? normalizeAcpAgent(settings?.acpAgent),
241
+ );
242
+ const selectedRuntime = $derived(
243
+ localRuntime ??
244
+ (settings?.defaultRuntime === "codex-native"
245
+ ? "codex-native"
246
+ : settings?.defaultRuntime === "fake"
247
+ ? "fake"
248
+ : "acp"),
249
+ );
250
+ const selectedModel = $derived(
251
+ localModel ?? settings?.defaultModel ?? DEFAULT_AI_SETTINGS.defaultModel,
252
+ );
253
+ const selectedThinking = $derived(
254
+ localThinking ?? settings?.thinking ?? DEFAULT_AI_SETTINGS.thinking,
255
+ );
256
+ const modelOptions = $derived.by<ModelRef[]>(() => {
257
+ const available = catalogModelsForAgent(selectedAgent, models);
258
+ if (
259
+ selectedModel &&
260
+ !available.some((model) => model.model === selectedModel)
261
+ ) {
262
+ return [{ provider: selectedAgent, model: selectedModel }, ...available];
263
+ }
264
+ return available;
265
+ });
266
+ const mentionTriggers = $derived.by<ComposerTrigger[]>(() => {
267
+ const triggers: ComposerTrigger[] = [];
268
+ if (slashRouter) {
269
+ triggers.push({
270
+ character: "/",
271
+ menuLabel: "Commands",
272
+ emptySearchResultsText: "No commands",
273
+ searchSource: (query) =>
274
+ filterComposerSlashItems(
275
+ composerSlashItems(
276
+ slashRouter.catalog.list(controller.activeBindingId),
277
+ composerAgentLabel(selectedAgent, selectedRuntime),
278
+ ),
279
+ query,
280
+ ),
281
+ onSelect: (item) =>
282
+ item.submitOnSelect
283
+ ? (item.value ?? `/${item.id}`)
284
+ : `${item.value ?? `/${item.id}`} `,
285
+ });
286
+ }
287
+ if (fileSearch) {
288
+ triggers.push({
289
+ character: "@",
290
+ menuLabel: "Files",
291
+ emptySearchResultsText: "No vault files",
292
+ searchSource: fileSearch,
293
+ onSelect: (item) => ({
294
+ value: item.value ?? formatFileMention(item.id),
295
+ label: item.label,
296
+ variant: "secondary",
297
+ }),
298
+ });
299
+ }
300
+ return triggers;
301
+ });
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
+ ),
317
+ );
318
+ const timeline = $derived(
319
+ groupChatItemsByDate(controller.items, new Date(), agentLabels),
320
+ );
321
+ const resultConversation = $derived({
322
+ conversationId: controller.location?.conversationId,
323
+ scopeDirectory: controller.location?.scopeDir,
324
+ });
325
+ const latestMessageId = $derived(controller.items.at(-1)?.id);
326
+ const showPicker = $derived(controller.pickerEntries.length >= 2);
327
+ const pickerGroups = $derived(
328
+ groupConversationsByRelativeScope(
329
+ controller.pickerEntries,
330
+ controller.directoryContext,
331
+ ),
332
+ );
333
+ const scopePathLabel = $derived(
334
+ formatDirectoryContextLabel(
335
+ controller.directoryContext || controller.location?.scopeDir || "",
336
+ ),
337
+ );
338
+ const isEmpty = $derived(
339
+ !showPicker && controller.items.length === 0 && !controller.busy,
340
+ );
341
+ const composerError = $derived(
342
+ controller.error ??
343
+ modelCatalogError ??
344
+ unavailableReason ??
345
+ controller.appToolsUnavailableReason,
346
+ );
347
+ const composerStatus = $derived<ComposerStatus | undefined>(
348
+ composerError ? { type: "error", message: composerError } : undefined,
349
+ );
350
+ const showWorkingIndicator = $derived(
351
+ shouldShowWorkingIndicator(initializing, controller.busy, composerError),
352
+ );
353
+ const workingLabel = $derived(
354
+ initializing
355
+ ? "Preparing AI…"
356
+ : controller.commandWorking
357
+ ? "Preparing command…"
358
+ : "Agent is working…",
359
+ );
360
+ const contextPercent = $derived(
361
+ controller.usage
362
+ ? Math.min(
363
+ 100,
364
+ Math.round((controller.usage.used / controller.usage.limit) * 100),
365
+ )
366
+ : 0,
367
+ );
368
+ const pendingInteraction = $derived.by(() => {
369
+ for (let index = controller.items.length - 1; index >= 0; index -= 1) {
370
+ const item = controller.items[index];
371
+ if (
372
+ (item?.type === "approval" || item?.type === "question") &&
373
+ item.status === "pending"
374
+ ) {
375
+ return item;
376
+ }
377
+ }
378
+ return undefined;
379
+ });
380
+
381
+ async function submit(prompt: string): Promise<void> {
382
+ if (initializing) return;
383
+ const selected = catalogModelsForAgent(selectedAgent, models).find(
384
+ (model) => model.model === selectedModel,
385
+ );
386
+ const extra = attachments.map((file) => file.path);
387
+ attachments = [];
388
+ drawerCollapsed = false;
389
+ await controller.submit(prompt, {
390
+ workspace,
391
+ mcpServers,
392
+ agent: selectedAgent,
393
+ model: selectedModel
394
+ ? {
395
+ provider: selected?.provider ?? selectedAgent,
396
+ model: selectedModel,
397
+ }
398
+ : undefined,
399
+ thinking: selectedThinking,
400
+ metadata: {
401
+ ...(extra.length > 0 ? { attachments: extra } : {}),
402
+ runtime: selectedRuntime,
403
+ },
404
+ });
405
+ }
406
+
407
+ function persistComposerDefaults(
408
+ agent: AcpAgentId,
409
+ runtimePreference: "acp" | "codex-native" | "fake",
410
+ model: string | null,
411
+ thinking: AiThinkingLevel,
412
+ ): void {
413
+ void onSettingsChange?.({
414
+ acpAgent: agent,
415
+ defaultRuntime: runtimePreference,
416
+ ...(model ? { defaultModel: model } : {}),
417
+ thinking,
418
+ });
419
+ }
420
+
421
+ function changeAgent(
422
+ agent: AcpAgentId,
423
+ runtimePreference: "acp" | "codex-native",
424
+ ): void {
425
+ localAgent = agent;
426
+ localRuntime = runtimePreference;
427
+ const configured = settings?.defaultModels?.[agent];
428
+ localModel =
429
+ configured ||
430
+ catalogModelsForAgent(agent, models).find((model) => model.isDefault)
431
+ ?.model ||
432
+ catalogModelsForAgent(agent, models)[0]?.model ||
433
+ null;
434
+ persistComposerDefaults(
435
+ agent,
436
+ runtimePreference,
437
+ localModel,
438
+ selectedThinking,
439
+ );
440
+ }
441
+
442
+ function changeModel(value: string): void {
443
+ localModel = value;
444
+ persistComposerDefaults(
445
+ selectedAgent,
446
+ selectedRuntime === "fake" ? "acp" : selectedRuntime,
447
+ value,
448
+ selectedThinking,
449
+ );
450
+ }
451
+
452
+ function changeThinking(value: string): void {
453
+ localThinking = value as AiThinkingLevel;
454
+ persistComposerDefaults(
455
+ selectedAgent,
456
+ selectedRuntime === "fake" ? "acp" : selectedRuntime,
457
+ selectedModel,
458
+ localThinking,
459
+ );
460
+ }
461
+
462
+ function addAttachment(item: ComposerTriggerItem): void {
463
+ const path = item.id;
464
+ if (!path || attachments.some((file) => file.path === path)) {
465
+ attachOpen = false;
466
+ return;
467
+ }
468
+ attachments = [...attachments, { path, name: item.label || path }];
469
+ drawerCollapsed = false;
470
+ attachOpen = false;
471
+ }
472
+
473
+ function removeAttachment(path: string): void {
474
+ attachments = attachments.filter((file) => file.path !== path);
475
+ }
476
+
477
+ async function copyResponse(text: string): Promise<void> {
478
+ await navigator.clipboard.writeText(text);
479
+ }
480
+
481
+ function promptForError(errorId: string): string | null {
482
+ const errorIndex = controller.items.findIndex(
483
+ (item) => item.id === errorId,
484
+ );
485
+ for (let index = errorIndex - 1; index >= 0; index -= 1) {
486
+ const item = controller.items[index];
487
+ if (item?.type === "message" && item.role === "user") {
488
+ return item.text;
489
+ }
490
+ }
491
+ return null;
492
+ }
493
+
494
+ async function loadVaultFiles(): Promise<void> {
495
+ if (!fileSearch) {
496
+ attachItems = [];
497
+ return;
498
+ }
499
+ attachItems = await fileSearch("", new AbortController().signal);
500
+ }
501
+
502
+ function onAttachOpenChange(open: boolean): void {
503
+ attachOpen = open;
504
+ if (open) {
505
+ void loadVaultFiles();
506
+ return;
507
+ }
508
+ attachItems = [];
509
+ }
510
+
511
+ function loadConversationFolders(): void {
512
+ const current =
513
+ controller.directoryContext || controller.location?.scopeDir || "";
514
+ const folders = new Set<string>();
515
+ for (const candidate of [
516
+ "",
517
+ current,
518
+ ...(listConversationFolders?.() ?? []),
519
+ ]) {
520
+ try {
521
+ folders.add(normalizeConversationScope(candidate));
522
+ } catch {
523
+ // Ignore stale or invalid host catalogue entries.
524
+ }
525
+ }
526
+ scopeFolders = [...folders].sort((left, right) => {
527
+ if (!left) return -1;
528
+ if (!right) return 1;
529
+ return left.localeCompare(right);
530
+ });
531
+ }
532
+
533
+ function onScopePickerOpenChange(open: boolean): void {
534
+ scopePickerOpen = open;
535
+ if (open) loadConversationFolders();
536
+ }
537
+
538
+ async function changeConversationScope(scopeDir: string): Promise<void> {
539
+ if (initializing || controller.busy || !repository) return;
540
+ scopePickerOpen = false;
541
+ revealConversationScope(app, scopeDir);
542
+ await controller.followDirectoryScope(scopeDir, { force: true });
543
+ }
544
+
545
+ function formatTokenCount(value: number): string {
546
+ return new Intl.NumberFormat().format(value);
547
+ }
548
+
549
+ function toolCallProps(item: AiChatToolItem) {
550
+ const hint = { toolName: item.name, input: item.input };
551
+ const presentedError =
552
+ item.state === "error" ? presentToolPayload(item.output, hint) : undefined;
553
+ return {
554
+ id: item.toolId,
555
+ name: item.name,
556
+ status: toolCallStatus(item.state),
557
+ errorMessage:
558
+ presentedError && isOneLineAlert(presentedError)
559
+ ? presentedError.code
560
+ : undefined,
561
+ target: toolCallTarget(item.input, item.server),
562
+ data: {
563
+ input: item.input,
564
+ output: item.state === "error" ? undefined : item.output,
565
+ error: item.state === "error" ? item.output : undefined,
566
+ name: item.name,
567
+ },
568
+ };
569
+ }
570
+
571
+ $effect(() => {
572
+ controller.runtime = runtime;
573
+ controller.mcpServers = mcpServers;
574
+ });
575
+
576
+ $effect(() => {
577
+ if (initializing) return;
578
+ untrack(() => {
579
+ void controller.restore();
580
+ });
581
+ return () => {
582
+ void controller.close();
583
+ };
584
+ });
585
+
586
+ $effect(() => {
587
+ if (initializing) return;
588
+ void controller.location;
589
+ void controller.activeBindingId;
590
+ void controller.syncComposerCommands();
591
+ });
592
+
593
+ $effect(() => {
594
+ if (!subscribeConversationMoves) return;
595
+ return subscribeConversationMoves((oldPath, newPath) => {
596
+ controller.relocateScope(oldPath, newPath);
597
+ });
598
+ });
599
+
600
+ $effect(() => {
601
+ const requestId = pendingInteraction?.request.id ?? null;
602
+ if (requestId && requestId !== visibleInteractionId) {
603
+ visibleInteractionId = requestId;
604
+ drawerCollapsed = false;
605
+ }
606
+ if (!requestId) visibleInteractionId = null;
607
+ });
608
+
609
+ function directoryScope(): string {
610
+ return currentConversationScope?.() ?? "";
611
+ }
612
+
613
+ onMount(() => {
614
+ if (!app || !repository) return;
615
+ const follow = (leaf?: WorkspaceLeaf | null) => {
616
+ if (workspaceLeaf && leaf === workspaceLeaf) return;
617
+ void controller.followDirectoryScope(directoryScope());
618
+ };
619
+ const activeLeaf = app.workspace.on("active-leaf-change", follow);
620
+ let cancelled = false;
621
+ const start = !initialLocation
622
+ ? window.setTimeout(() => {
623
+ if (!cancelled) follow();
624
+ }, 0)
625
+ : undefined;
626
+ return () => {
627
+ cancelled = true;
628
+ if (start !== undefined) window.clearTimeout(start);
629
+ app.workspace.offref(activeLeaf);
630
+ };
631
+ });
632
+ </script>
633
+
634
+ {#snippet scopeFooter()}
635
+ <div class="ai-chat-panel__scope-path">
636
+ <Popover.Root
637
+ bind:open={scopePickerOpen}
638
+ onOpenChange={onScopePickerOpenChange}
639
+ >
640
+ <Popover.Trigger>
641
+ {#snippet child({ props }: { props: Record<string, unknown> })}
642
+ <Button
643
+ {...props}
644
+ class="ai-chat-panel__scope-button"
645
+ size="sm"
646
+ variant="ghost"
647
+ data-testid="ai-chat-scope-path"
648
+ aria-label={`Change chat folder: ${scopePathLabel}`}
649
+ disabled={initializing || controller.busy || !repository}
650
+ >
651
+ <FolderIcon data-icon="inline-start" aria-hidden="true" />
652
+ <span>{scopePathLabel}</span>
653
+ </Button>
654
+ {/snippet}
655
+ </Popover.Trigger>
656
+ <Popover.Content
657
+ data-ai-part="scope-picker-popover"
658
+ side="top"
659
+ align="center"
660
+ sideOffset={8}
661
+ >
662
+ <CommandView.Root>
663
+ <CommandView.Input placeholder="Search chat folders" />
664
+ <CommandView.List aria-label="Chat folders">
665
+ <CommandView.Empty>No folders</CommandView.Empty>
666
+ <CommandView.Group>
667
+ {#each scopeFolders as folder (folder || "vault-root")}
668
+ <CommandView.Item
669
+ value={`${formatDirectoryContextLabel(folder)} ${folder}`}
670
+ onSelect={() => void changeConversationScope(folder)}
671
+ >
672
+ <CommandView.ItemIcon>
673
+ <FolderIcon aria-hidden="true" />
674
+ </CommandView.ItemIcon>
675
+ <CommandView.ItemLabel>
676
+ {formatDirectoryContextLabel(folder)}
677
+ </CommandView.ItemLabel>
678
+ </CommandView.Item>
679
+ {/each}
680
+ </CommandView.Group>
681
+ </CommandView.List>
682
+ </CommandView.Root>
683
+ </Popover.Content>
684
+ </Popover.Root>
685
+ </div>
686
+ {/snippet}
687
+
688
+ {#snippet toolDetail(call: { data?: unknown; name?: string })}
689
+ {@const detail = call.data as
690
+ | { input?: string; output?: string; error?: string; name?: string }
691
+ | undefined}
692
+ {@const hint = {
693
+ toolName: detail?.name ?? call.name,
694
+ input: detail?.input,
695
+ }}
696
+ {@const ResultView = resolveToolResultView(app, detail?.name ?? call.name)}
697
+ {@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
+ {#if ResultView && app && parsedOutput != null && !detail?.error}
705
+ <ResultView
706
+ {app}
707
+ conversation={resultConversation}
708
+ name={detail?.name ?? call.name ?? ""}
709
+ input={parseToolResultPayload(detail?.input)}
710
+ output={parsedOutput}
711
+ state="completed"
712
+ />
713
+ {: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>
749
+ {/if}
750
+ {/snippet}
751
+
752
+ <div
753
+ class="ai-chat-panel"
754
+ data-ui-component="ai-chat-panel"
755
+ data-testid="ai-chat-panel"
756
+ data-initializing={initializing}
757
+ >
758
+ <Chat.Layout density="compact" {isEmpty} aria-label="AI chat">
759
+ {#snippet emptyState()}
760
+ <Empty.Root>
761
+ <Empty.Header>
762
+ <Empty.Media variant="icon">
763
+ <SparklesIcon aria-hidden="true" />
764
+ </Empty.Media>
765
+ <Empty.Title>Start a conversation</Empty.Title>
766
+ <Empty.Description>
767
+ Ask a question or describe the change you want to make.
768
+ </Empty.Description>
769
+ </Empty.Header>
770
+ </Empty.Root>
771
+ {@render scopeFooter()}
772
+ {/snippet}
773
+ {#snippet composer()}
774
+ {#if showWorkingIndicator}
775
+ <div
776
+ class="ai-chat-panel__working"
777
+ data-testid="ai-chat-working"
778
+ role="status"
779
+ aria-live="polite"
780
+ aria-atomic="true"
781
+ >
782
+ <Spinner />
783
+ <span>{workingLabel}</span>
784
+ </div>
785
+ {/if}
786
+ <Chat.Composer
787
+ bind:value={draft}
788
+ placeholder="Ask anything…"
789
+ disabled={initializing || controller.busy}
790
+ interactiveDrawerWhenDisabled={Boolean(pendingInteraction)}
791
+ isStopShown={controller.busy}
792
+ status={composerStatus}
793
+ statusPosition="top"
794
+ triggers={mentionTriggers}
795
+ onSubmit={(value) => void submit(value)}
796
+ onStop={() => {
797
+ void controller.cancel();
798
+ }}
799
+ >
800
+ {#snippet drawer()}
801
+ {#if pendingInteraction || attachments.length > 0}
802
+ <div bind:this={drawerHost}>
803
+ <Chat.ComposerDrawer
804
+ bind:collapsed={drawerCollapsed}
805
+ count={attachments.length + (pendingInteraction ? 1 : 0)}
806
+ label={pendingInteraction?.type === "approval"
807
+ ? "Permission requested"
808
+ : pendingInteraction?.type === "question"
809
+ ? "User input requested"
810
+ : "Attachments"}
811
+ >
812
+ {#if pendingInteraction?.type === "approval"}
813
+ {@const requestId = pendingInteraction.request.id}
814
+ <AiApprovalCard
815
+ request={pendingInteraction.request}
816
+ onRespond={(optionId) =>
817
+ void controller.respondToApproval(requestId, optionId)}
818
+ />
819
+ {:else if pendingInteraction?.type === "question"}
820
+ {@const requestId = pendingInteraction.request.id}
821
+ <AiQuestionCard
822
+ request={pendingInteraction.request}
823
+ onRespond={(answers) =>
824
+ void controller.respondToQuestion(requestId, answers)}
825
+ />
826
+ {/if}
827
+ {#if attachments.length > 0}
828
+ {#each attachments as file (file.path)}
829
+ <span data-ui-part="attachment-chip">
830
+ <span>{file.name}</span>
831
+ <button
832
+ type="button"
833
+ data-ui-part="attachment-remove"
834
+ aria-label={`Remove ${file.name}`}
835
+ onclick={() => removeAttachment(file.path)}
836
+ >
837
+ <XIcon aria-hidden="true" />
838
+ </button>
839
+ </span>
840
+ {/each}
841
+ {/if}
842
+ </Chat.ComposerDrawer>
843
+ </div>
844
+ {/if}
845
+ {/snippet}
846
+ {#snippet headerActions()}
847
+ {#if repository}
848
+ <Button
849
+ size="icon-sm"
850
+ variant="ghost"
851
+ aria-label="Show conversation history"
852
+ data-testid="ai-chat-history"
853
+ onclick={() => void onRevealHistory?.()}
854
+ >
855
+ <HistoryIcon aria-hidden="true" />
856
+ </Button>
857
+ {/if}
858
+ {#if fileSearch}
859
+ <Popover.Root
860
+ bind:open={attachOpen}
861
+ onOpenChange={onAttachOpenChange}
862
+ >
863
+ <Popover.Trigger>
864
+ {#snippet child({ props }: { props: Record<string, unknown> })}
865
+ <Button
866
+ {...props}
867
+ size="icon-sm"
868
+ variant="ghost"
869
+ aria-label="Attach file"
870
+ data-testid="ai-chat-attach"
871
+ disabled={initializing}
872
+ >
873
+ <PaperclipIcon aria-hidden="true" />
874
+ </Button>
875
+ {/snippet}
876
+ </Popover.Trigger>
877
+ <Popover.Content
878
+ data-ai-part="attach-popover"
879
+ side="top"
880
+ align="start"
881
+ sideOffset={attachSideOffset}
882
+ avoidCollisions={false}
883
+ >
884
+ <CommandView.Root>
885
+ <CommandView.Input placeholder="Search vault files" />
886
+ <CommandView.List aria-label="Vault files">
887
+ <CommandView.Empty>No vault files</CommandView.Empty>
888
+ {#each attachItems as item (item.id)}
889
+ <CommandView.Item
890
+ value={`${item.label} ${item.id}`}
891
+ onSelect={() => addAttachment(item)}
892
+ >
893
+ <CommandView.ItemLabel>{item.label}</CommandView.ItemLabel>
894
+ </CommandView.Item>
895
+ {/each}
896
+ </CommandView.List>
897
+ </CommandView.Root>
898
+ </Popover.Content>
899
+ </Popover.Root>
900
+ {/if}
901
+ {#if repository}
902
+ <DropdownMenu.Root>
903
+ <DropdownMenu.Trigger>
904
+ {#snippet child({ props }: { props: Record<string, unknown> })}
905
+ <Button
906
+ {...props}
907
+ size="icon"
908
+ variant="ghost"
909
+ aria-label="Conversation actions"
910
+ data-testid="ai-chat-conversation-menu"
911
+ >
912
+ <MoreHorizontalIcon aria-hidden="true" />
913
+ </Button>
914
+ {/snippet}
915
+ </DropdownMenu.Trigger>
916
+ <DropdownMenu.Content
917
+ data-ai-part="conversation-menu"
918
+ align="end"
919
+ >
920
+ <DropdownMenu.Group>
921
+ <DropdownMenu.Item
922
+ disabled={!controller.location}
923
+ onclick={() =>
924
+ void controller.archiveCurrent(
925
+ controller.conversationStatus !== "archived",
926
+ )}
927
+ >
928
+ {#if controller.conversationStatus === "archived"}
929
+ <ArchiveRestoreIcon
930
+ data-icon="inline-start"
931
+ aria-hidden="true"
932
+ />
933
+ Restore Chat
934
+ {:else}
935
+ <ArchiveIcon
936
+ data-icon="inline-start"
937
+ aria-hidden="true"
938
+ />
939
+ Archive Chat
940
+ {/if}
941
+ </DropdownMenu.Item>
942
+ <DropdownMenu.Item
943
+ variant="destructive"
944
+ disabled={!controller.location}
945
+ onclick={() => void controller.deleteCurrent()}
946
+ >
947
+ <TrashIcon data-icon="inline-start" aria-hidden="true" />
948
+ Delete Chat
949
+ </DropdownMenu.Item>
950
+ <DropdownMenu.Item
951
+ onclick={() =>
952
+ void controller.newConversation(
953
+ createConversation?.(controller.directoryContext) ?? {
954
+ scopeDir:
955
+ controller.directoryContext ||
956
+ controller.location?.scopeDir ||
957
+ "",
958
+ },
959
+ )}
960
+ >
961
+ <PlusIcon data-icon="inline-start" aria-hidden="true" />
962
+ New Chat
963
+ </DropdownMenu.Item>
964
+ </DropdownMenu.Group>
965
+ </DropdownMenu.Content>
966
+ </DropdownMenu.Root>
967
+ {/if}
968
+ {/snippet}
969
+ {#snippet headerContext()}
970
+ {#if controller.usage}
971
+ <label
972
+ class="ai-chat-panel__context-usage"
973
+ title={`${formatTokenCount(controller.usage.used)} of ${formatTokenCount(controller.usage.limit)} tokens used`}
974
+ >
975
+ <span>Context</span>
976
+ <progress
977
+ aria-label="Context window usage"
978
+ value={Math.min(controller.usage.used, controller.usage.limit)}
979
+ max={controller.usage.limit}
980
+ ></progress>
981
+ <span>{contextPercent}%</span>
982
+ </label>
983
+ {/if}
984
+ {/snippet}
985
+ {#snippet footerActions()}
986
+ <Button
987
+ size="icon-sm"
988
+ variant="ghost"
989
+ aria-label={controller.conversationPinned
990
+ ? "Unpin conversation"
991
+ : "Pin conversation"}
992
+ aria-pressed={controller.conversationPinned}
993
+ data-testid="ai-chat-pin"
994
+ disabled={initializing || !controller.location}
995
+ onclick={() =>
996
+ void controller.setPinned(!controller.conversationPinned)}
997
+ >
998
+ {#if controller.conversationPinned}
999
+ <PinOffIcon aria-hidden="true" />
1000
+ {:else}
1001
+ <PinIcon aria-hidden="true" />
1002
+ {/if}
1003
+ </Button>
1004
+ <DropdownMenu.Root>
1005
+ <DropdownMenu.Trigger>
1006
+ {#snippet child({ props }: { props: Record<string, unknown> })}
1007
+ <Button
1008
+ {...props}
1009
+ size="icon-sm"
1010
+ variant="ghost"
1011
+ aria-label="Effort and model"
1012
+ data-testid="ai-chat-effort"
1013
+ disabled={initializing}
1014
+ >
1015
+ <BrainIcon aria-hidden="true" />
1016
+ </Button>
1017
+ {/snippet}
1018
+ </DropdownMenu.Trigger>
1019
+ <DropdownMenu.Content data-ui-part="effort-popover" align="start">
1020
+ {#if selectRuntime}
1021
+ <DropdownMenu.Sub>
1022
+ <DropdownMenu.SubTrigger data-testid="ai-chat-agent">
1023
+ Agent
1024
+ </DropdownMenu.SubTrigger>
1025
+ <DropdownMenu.SubContent>
1026
+ <DropdownMenu.RadioGroup
1027
+ value={`${selectedRuntime}:${selectedAgent}`}
1028
+ >
1029
+ <DropdownMenu.RadioItem
1030
+ value="acp:codex"
1031
+ onclick={() => changeAgent("codex", "acp")}
1032
+ >Codex ACP</DropdownMenu.RadioItem>
1033
+ <DropdownMenu.RadioItem
1034
+ value="acp:cursor"
1035
+ onclick={() => changeAgent("cursor", "acp")}
1036
+ >Cursor ACP</DropdownMenu.RadioItem>
1037
+ <DropdownMenu.RadioItem
1038
+ value="codex-native:codex"
1039
+ onclick={() => changeAgent("codex", "codex-native")}
1040
+ >Codex Native</DropdownMenu.RadioItem>
1041
+ </DropdownMenu.RadioGroup>
1042
+ </DropdownMenu.SubContent>
1043
+ </DropdownMenu.Sub>
1044
+ {/if}
1045
+ <DropdownMenu.Sub>
1046
+ <DropdownMenu.SubTrigger data-testid="ai-chat-model">
1047
+ Model
1048
+ </DropdownMenu.SubTrigger>
1049
+ <DropdownMenu.SubContent>
1050
+ {#if modelOptions.length > 0}
1051
+ <DropdownMenu.RadioGroup value={selectedModel}>
1052
+ {#each modelOptions as option (option.model)}
1053
+ <DropdownMenu.RadioItem
1054
+ value={option.model}
1055
+ onclick={() => changeModel(option.model)}
1056
+ >
1057
+ {option.displayName ?? option.model}
1058
+ {#if option.badges?.length}
1059
+ <span data-ai-part="model-badge">
1060
+ {option.badges.join(" ")}
1061
+ </span>
1062
+ {/if}
1063
+ </DropdownMenu.RadioItem>
1064
+ {/each}
1065
+ </DropdownMenu.RadioGroup>
1066
+ {:else}
1067
+ <DropdownMenu.Item disabled
1068
+ >No models available</DropdownMenu.Item
1069
+ >
1070
+ {/if}
1071
+ </DropdownMenu.SubContent>
1072
+ </DropdownMenu.Sub>
1073
+ <DropdownMenu.Sub>
1074
+ <DropdownMenu.SubTrigger data-testid="ai-chat-thinking">
1075
+ Thinking
1076
+ </DropdownMenu.SubTrigger>
1077
+ <DropdownMenu.SubContent>
1078
+ <DropdownMenu.RadioGroup value={selectedThinking}>
1079
+ <DropdownMenu.RadioItem
1080
+ value="off"
1081
+ onclick={() => changeThinking("off")}
1082
+ >Off</DropdownMenu.RadioItem
1083
+ >
1084
+ <DropdownMenu.RadioItem
1085
+ value="low"
1086
+ onclick={() => changeThinking("low")}
1087
+ >Low</DropdownMenu.RadioItem
1088
+ >
1089
+ <DropdownMenu.RadioItem
1090
+ value="medium"
1091
+ onclick={() => changeThinking("medium")}
1092
+ >Medium</DropdownMenu.RadioItem
1093
+ >
1094
+ <DropdownMenu.RadioItem
1095
+ value="high"
1096
+ onclick={() => changeThinking("high")}
1097
+ >High</DropdownMenu.RadioItem
1098
+ >
1099
+ </DropdownMenu.RadioGroup>
1100
+ </DropdownMenu.SubContent>
1101
+ </DropdownMenu.Sub>
1102
+ </DropdownMenu.Content>
1103
+ </DropdownMenu.Root>
1104
+ {/snippet}
1105
+ </Chat.Composer>
1106
+ {/snippet}
1107
+ <Chat.MessageList
1108
+ density="compact"
1109
+ {latestMessageId}
1110
+ isStreaming={controller.busy}
1111
+ {isEmpty}
1112
+ >
1113
+ {#if showPicker}
1114
+ <div
1115
+ class="ai-chat-panel__conversation-picker"
1116
+ data-testid="ai-chat-conversation-picker"
1117
+ >
1118
+ <CommandView.Root>
1119
+ <CommandView.Input placeholder="Search conversations" />
1120
+ <CommandView.List aria-label="Conversations in this folder">
1121
+ <CommandView.Empty>No conversations</CommandView.Empty>
1122
+ {#each pickerGroups as group (group.heading)}
1123
+ <CommandView.Group heading={group.heading}>
1124
+ {#each group.items as entry (`${entry.location.scopeDir}:${entry.location.conversationId}`)}
1125
+ <CommandView.Item
1126
+ value={`${entry.metadata?.title ?? "Untitled"} ${entry.location.scopeDir} ${entry.location.conversationId}`}
1127
+ onSelect={() => {
1128
+ revealConversationScope(app, entry.location.scopeDir);
1129
+ void controller.openConversation(entry.location);
1130
+ }}
1131
+ >
1132
+ <CommandView.ItemLabel>
1133
+ {entry.metadata?.title ?? "Untitled"}
1134
+ </CommandView.ItemLabel>
1135
+ {#if entry.preview}
1136
+ <CommandView.ItemDescription>
1137
+ {entry.preview}
1138
+ </CommandView.ItemDescription>
1139
+ {/if}
1140
+ </CommandView.Item>
1141
+ {/each}
1142
+ </CommandView.Group>
1143
+ {/each}
1144
+ </CommandView.List>
1145
+ </CommandView.Root>
1146
+ </div>
1147
+ {/if}
1148
+ {#each timeline as entry (entry.kind === "item" ? entry.item.id : entry.id)}
1149
+ {#if entry.kind === "divider"}
1150
+ <Chat.SystemMessage variant="divider"
1151
+ >{entry.label}</Chat.SystemMessage
1152
+ >
1153
+ {:else if entry.kind === "tools"}
1154
+ <Chat.ToolCalls
1155
+ calls={entry.items.map((item) => ({
1156
+ ...toolCallProps(item),
1157
+ detail:
1158
+ item.input || item.output ? toolDetail : undefined,
1159
+ }))}
1160
+ defaultExpanded={false}
1161
+ />
1162
+ {:else if entry.item.type === "message"}
1163
+ {@const message = entry.item}
1164
+ <Chat.Message sender={message.role === "user" ? "user" : "assistant"}>
1165
+ <Chat.MessageBubble>
1166
+ {#if message.role === "assistant" && app}
1167
+ <MarkdownEmbed
1168
+ {app}
1169
+ value={message.text}
1170
+ htmlPolicy="safe"
1171
+ class="ai-chat-panel__markdown"
1172
+ />
1173
+ {:else}
1174
+ <Chat.TokenizedText
1175
+ text={message.text}
1176
+ tokens={mentionTokensFromText(message.text)}
1177
+ />
1178
+ {/if}
1179
+ </Chat.MessageBubble>
1180
+ {#snippet metadata()}
1181
+ {#if message.role === "assistant"}
1182
+ <Chat.MessageMetadata
1183
+ timestamp={message.createdAt
1184
+ ? formatChatTimestamp(message.createdAt)
1185
+ : undefined}
1186
+ >
1187
+ {#snippet footer()}
1188
+ <span class="ai-chat-panel__message-actions">
1189
+ <Button
1190
+ size="icon-xs"
1191
+ variant="ghost"
1192
+ aria-label="Copy response"
1193
+ onclick={() => void copyResponse(message.text)}
1194
+ >
1195
+ <CopyIcon aria-hidden="true" />
1196
+ </Button>
1197
+ </span>
1198
+ {/snippet}
1199
+ </Chat.MessageMetadata>
1200
+ {:else if message.createdAt}
1201
+ <Chat.MessageMetadata
1202
+ timestamp={formatChatTimestamp(message.createdAt)}
1203
+ />
1204
+ {/if}
1205
+ {/snippet}
1206
+ </Chat.Message>
1207
+ {:else if entry.item.type === "thinking"}
1208
+ <Reasoning
1209
+ streaming={entry.item.state === "streaming"}
1210
+ preview={entry.item.text}
1211
+ expanded={entry.item.state === "streaming"}
1212
+ >
1213
+ {entry.item.text}
1214
+ </Reasoning>
1215
+ {:else if entry.item.type === "approval"}
1216
+ {#if entry.item.status !== "pending"}
1217
+ <Chat.SystemMessage>
1218
+ Approval {entry.item.status}
1219
+ {entry.item.responseOptionId
1220
+ ? ` (${entry.item.responseOptionId})`
1221
+ : ""}
1222
+ </Chat.SystemMessage>
1223
+ {/if}
1224
+ {:else if entry.item.type === "question"}
1225
+ {#if entry.item.status !== "pending"}
1226
+ <Chat.SystemMessage>
1227
+ Question {entry.item.status}
1228
+ </Chat.SystemMessage>
1229
+ {/if}
1230
+ {:else if entry.item.type === "error"}
1231
+ {@const errorItem = entry.item}
1232
+ {@const retryPrompt = promptForError(errorItem.id)}
1233
+ <Chat.Message sender="assistant">
1234
+ <Chat.MessageBubble>{errorItem.text}</Chat.MessageBubble>
1235
+ {#snippet metadata()}
1236
+ <Chat.MessageMetadata
1237
+ timestamp={errorItem.createdAt
1238
+ ? formatChatTimestamp(errorItem.createdAt)
1239
+ : undefined}
1240
+ status="error"
1241
+ >
1242
+ {#snippet footer()}
1243
+ <span class="ai-chat-panel__message-actions">
1244
+ <Button
1245
+ size="icon-xs"
1246
+ variant="ghost"
1247
+ aria-label="Retry message"
1248
+ disabled={initializing || controller.busy || !retryPrompt}
1249
+ onclick={() => retryPrompt && void submit(retryPrompt)}
1250
+ >
1251
+ <RedoIcon aria-hidden="true" />
1252
+ </Button>
1253
+ </span>
1254
+ {/snippet}
1255
+ </Chat.MessageMetadata>
1256
+ {/snippet}
1257
+ </Chat.Message>
1258
+ {:else if entry.item.type === "status" &&
1259
+ entry.item.layout === "inventory"}
1260
+ <div
1261
+ class="ai-chat-panel__command-notice"
1262
+ data-testid="ai-chat-command-notice"
1263
+ data-layout="inventory"
1264
+ role="status"
1265
+ >
1266
+ <AiInventoryResult
1267
+ {app}
1268
+ conversation={resultConversation}
1269
+ name={entry.item.inventory?.kind ?? "skills"}
1270
+ output={entry.item.inventory}
1271
+ />
1272
+ </div>
1273
+ {:else if entry.item.type === "status" && isSlashCommandNotice(entry.item)}
1274
+ <div
1275
+ class="ai-chat-panel__command-notice"
1276
+ data-testid="ai-chat-command-notice"
1277
+ role="status"
1278
+ >
1279
+ {entry.item.text}
1280
+ </div>
1281
+ {:else}
1282
+ <Chat.SystemMessage>{entry.item.text}</Chat.SystemMessage>
1283
+ {/if}
1284
+ {/each}
1285
+ {@render scopeFooter()}
1286
+ </Chat.MessageList>
1287
+ </Chat.Layout>
1288
+ </div>