@mseep/obsidian-agent-client 0.10.6

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 (146) hide show
  1. package/.claude/hooks/gh-setup.sh +49 -0
  2. package/.claude/settings.json +15 -0
  3. package/.claude/skills/release-notes/SKILL.md +331 -0
  4. package/.editorconfig +10 -0
  5. package/.github/FUNDING.yml +2 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
  9. package/.github/copilot-instructions.md +45 -0
  10. package/.github/pull_request_template.md +32 -0
  11. package/.github/workflows/ci.yaml +25 -0
  12. package/.github/workflows/docs.yml +58 -0
  13. package/.github/workflows/relay_to_openclaw.yml +59 -0
  14. package/.github/workflows/release.yaml +45 -0
  15. package/.prettierignore +10 -0
  16. package/.prettierrc +13 -0
  17. package/.vscode/extensions.json +7 -0
  18. package/.vscode/settings.json +37 -0
  19. package/.zed/settings.json +42 -0
  20. package/AGENTS.md +330 -0
  21. package/ARCHITECTURE.md +390 -0
  22. package/CONTRIBUTING.md +216 -0
  23. package/LICENSE +202 -0
  24. package/NOTICE +2 -0
  25. package/README.ja.md +121 -0
  26. package/README.md +125 -0
  27. package/docs/.vitepress/config.mts +124 -0
  28. package/docs/.vitepress/theme/custom.css +111 -0
  29. package/docs/.vitepress/theme/index.ts +4 -0
  30. package/docs/agent-setup/claude-code.md +84 -0
  31. package/docs/agent-setup/codex.md +76 -0
  32. package/docs/agent-setup/custom-agents.md +67 -0
  33. package/docs/agent-setup/gemini-cli.md +99 -0
  34. package/docs/agent-setup/index.md +34 -0
  35. package/docs/announcements/gemini-cli-deprecation.md +73 -0
  36. package/docs/getting-started/index.md +78 -0
  37. package/docs/getting-started/quick-start.md +38 -0
  38. package/docs/help/faq.md +181 -0
  39. package/docs/help/troubleshooting.md +221 -0
  40. package/docs/index.md +63 -0
  41. package/docs/public/apple-touch-icon.png +0 -0
  42. package/docs/public/demo.mp4 +0 -0
  43. package/docs/public/favicon-16x16.png +0 -0
  44. package/docs/public/favicon-32x32.png +0 -0
  45. package/docs/public/favicon.ico +0 -0
  46. package/docs/public/images/editing.webp +0 -0
  47. package/docs/public/images/export.webp +0 -0
  48. package/docs/public/images/floating-chat-button.webp +0 -0
  49. package/docs/public/images/floating-chat-instance-menu.webp +0 -0
  50. package/docs/public/images/floating-chat-view.webp +0 -0
  51. package/docs/public/images/mode-selection.webp +0 -0
  52. package/docs/public/images/model-selection.webp +0 -0
  53. package/docs/public/images/multi-session.webp +0 -0
  54. package/docs/public/images/remove-image.webp +0 -0
  55. package/docs/public/images/ribbon-icon.webp +0 -0
  56. package/docs/public/images/selection-context.gif +0 -0
  57. package/docs/public/images/sending-images.webp +0 -0
  58. package/docs/public/images/sending-messages.webp +0 -0
  59. package/docs/public/images/session-history-button.webp +0 -0
  60. package/docs/public/images/slash-commands-1.webp +0 -0
  61. package/docs/public/images/slash-commands-2.webp +0 -0
  62. package/docs/public/images/switch-agent.webp +0 -0
  63. package/docs/public/images/switch-default-agent.webp +0 -0
  64. package/docs/public/images/temporary-disable.gif +0 -0
  65. package/docs/reference/acp-support.md +110 -0
  66. package/docs/usage/chat-export.md +80 -0
  67. package/docs/usage/commands.md +51 -0
  68. package/docs/usage/context-files.md +57 -0
  69. package/docs/usage/editing.md +69 -0
  70. package/docs/usage/floating-chat.md +84 -0
  71. package/docs/usage/index.md +97 -0
  72. package/docs/usage/mcp-tools.md +33 -0
  73. package/docs/usage/mentions.md +70 -0
  74. package/docs/usage/mode-selection.md +28 -0
  75. package/docs/usage/model-selection.md +32 -0
  76. package/docs/usage/multi-session.md +68 -0
  77. package/docs/usage/sending-images.md +64 -0
  78. package/docs/usage/session-history.md +91 -0
  79. package/docs/usage/slash-commands.md +44 -0
  80. package/esbuild.config.mjs +49 -0
  81. package/eslint.config.mjs +25 -0
  82. package/main.js +228 -0
  83. package/manifest.json +11 -0
  84. package/package.json +52 -0
  85. package/src/acp/acp-client.ts +921 -0
  86. package/src/acp/acp-handler.ts +252 -0
  87. package/src/acp/permission-handler.ts +282 -0
  88. package/src/acp/terminal-handler.ts +264 -0
  89. package/src/acp/type-converter.ts +272 -0
  90. package/src/hooks/useAgent.ts +250 -0
  91. package/src/hooks/useAgentMessages.ts +470 -0
  92. package/src/hooks/useAgentSession.ts +544 -0
  93. package/src/hooks/useChatActions.ts +400 -0
  94. package/src/hooks/useHistoryModal.ts +219 -0
  95. package/src/hooks/useSessionHistory.ts +863 -0
  96. package/src/hooks/useSettings.ts +19 -0
  97. package/src/hooks/useSuggestions.ts +342 -0
  98. package/src/main.ts +9 -0
  99. package/src/plugin.ts +1126 -0
  100. package/src/services/chat-exporter.ts +552 -0
  101. package/src/services/message-sender.ts +755 -0
  102. package/src/services/message-state.ts +375 -0
  103. package/src/services/session-helpers.ts +211 -0
  104. package/src/services/session-state.ts +130 -0
  105. package/src/services/session-storage.ts +267 -0
  106. package/src/services/settings-normalizer.ts +255 -0
  107. package/src/services/settings-service.ts +285 -0
  108. package/src/services/update-checker.ts +128 -0
  109. package/src/services/vault-service.ts +558 -0
  110. package/src/services/view-registry.ts +345 -0
  111. package/src/types/agent.ts +92 -0
  112. package/src/types/chat.ts +351 -0
  113. package/src/types/errors.ts +136 -0
  114. package/src/types/obsidian-internals.d.ts +14 -0
  115. package/src/types/session.ts +731 -0
  116. package/src/ui/ChangeDirectoryModal.ts +137 -0
  117. package/src/ui/ChatContext.ts +25 -0
  118. package/src/ui/ChatHeader.tsx +295 -0
  119. package/src/ui/ChatPanel.tsx +1162 -0
  120. package/src/ui/ChatView.tsx +348 -0
  121. package/src/ui/ErrorBanner.tsx +104 -0
  122. package/src/ui/FloatingButton.tsx +351 -0
  123. package/src/ui/FloatingChatView.tsx +531 -0
  124. package/src/ui/InputArea.tsx +1107 -0
  125. package/src/ui/InputToolbar.tsx +371 -0
  126. package/src/ui/MessageBubble.tsx +442 -0
  127. package/src/ui/MessageList.tsx +265 -0
  128. package/src/ui/PermissionBanner.tsx +61 -0
  129. package/src/ui/SessionHistoryModal.tsx +821 -0
  130. package/src/ui/SettingsTab.ts +1337 -0
  131. package/src/ui/SuggestionPopup.tsx +138 -0
  132. package/src/ui/TerminalBlock.tsx +107 -0
  133. package/src/ui/ToolCallBlock.tsx +456 -0
  134. package/src/ui/shared/AttachmentStrip.tsx +57 -0
  135. package/src/ui/shared/IconButton.tsx +55 -0
  136. package/src/ui/shared/MarkdownRenderer.tsx +103 -0
  137. package/src/ui/view-host.ts +56 -0
  138. package/src/utils/error-utils.ts +274 -0
  139. package/src/utils/logger.ts +44 -0
  140. package/src/utils/mention-parser.ts +129 -0
  141. package/src/utils/paths.ts +246 -0
  142. package/src/utils/platform.ts +425 -0
  143. package/styles.css +2322 -0
  144. package/tsconfig.json +18 -0
  145. package/version-bump.mjs +18 -0
  146. package/versions.json +3 -0
@@ -0,0 +1,348 @@
1
+ import { ItemView, WorkspaceLeaf } from "obsidian";
2
+ import type {
3
+ IChatViewContainer,
4
+ ChatViewType,
5
+ } from "../services/view-registry";
6
+ import * as React from "react";
7
+ const { useState, useEffect, useMemo } = React;
8
+ import { createRoot, Root } from "react-dom/client";
9
+
10
+ import type AgentClientPlugin from "../plugin";
11
+ import type { ChatInputState } from "../types/chat";
12
+
13
+ // Utility imports
14
+ import { getLogger, Logger } from "../utils/logger";
15
+
16
+ // Context imports
17
+ import { ChatContextProvider } from "./ChatContext";
18
+
19
+ // Component imports
20
+ import { ChatPanel, type ChatPanelCallbacks } from "./ChatPanel";
21
+
22
+ // Service imports
23
+ import { VaultService } from "../services/vault-service";
24
+
25
+ export const VIEW_TYPE_CHAT = "agent-client-chat-view";
26
+
27
+ function ChatComponent({
28
+ plugin,
29
+ view,
30
+ viewId,
31
+ }: {
32
+ plugin: AgentClientPlugin;
33
+ view: ChatView;
34
+ viewId: string;
35
+ }) {
36
+ // ============================================================
37
+ // Agent ID State (synced with Obsidian view state)
38
+ // ============================================================
39
+ const [restoredAgentId, setRestoredAgentId] = useState<string | undefined>(
40
+ view.getInitialAgentId() ?? undefined,
41
+ );
42
+
43
+ // ============================================================
44
+ // Context Value
45
+ // ============================================================
46
+ const contextValue = useMemo(
47
+ () => ({
48
+ plugin,
49
+ acpClient: view.acpClient,
50
+ vaultService: view.vaultService,
51
+ settingsService: plugin.settingsService,
52
+ }),
53
+ [plugin, view.acpClient, view.vaultService],
54
+ );
55
+
56
+ // ============================================================
57
+ // Agent ID Restoration (ChatView-specific)
58
+ // Subscribe to agentId restoration from Obsidian's setState
59
+ // ============================================================
60
+ useEffect(() => {
61
+ const unsubscribe = view.onAgentIdRestored((agentId) => {
62
+ setRestoredAgentId(agentId);
63
+ });
64
+ return unsubscribe;
65
+ }, [view]);
66
+
67
+ // ============================================================
68
+ // Render
69
+ // ============================================================
70
+ return (
71
+ <ChatContextProvider value={contextValue}>
72
+ <ChatPanel
73
+ variant="sidebar"
74
+ viewId={viewId}
75
+ initialAgentId={restoredAgentId}
76
+ viewHost={view}
77
+ onRegisterCallbacks={(callbacks) =>
78
+ view.setCallbacks(callbacks)
79
+ }
80
+ onAgentIdChanged={(agentId) => view.setAgentId(agentId)}
81
+ />
82
+ </ChatContextProvider>
83
+ );
84
+ }
85
+
86
+ /** State stored for view persistence */
87
+ interface ChatViewState extends Record<string, unknown> {
88
+ initialAgentId?: string;
89
+ }
90
+
91
+ export class ChatView extends ItemView implements IChatViewContainer {
92
+ private root: Root | null = null;
93
+ private plugin: AgentClientPlugin;
94
+ private logger: Logger;
95
+ /** Unique identifier for this view instance (for multi-session support) */
96
+ readonly viewId: string;
97
+ /** View type for IChatViewContainer */
98
+ readonly viewType: ChatViewType = "sidebar";
99
+ /** Initial agent ID passed via state (for openNewChatViewWithAgent) */
100
+ private initialAgentId: string | null = null;
101
+ /** Callbacks to notify React when agentId is restored from workspace state */
102
+ private agentIdRestoredCallbacks: Set<(agentId: string) => void> =
103
+ new Set();
104
+
105
+ // Services owned by this class (lifecycle managed here)
106
+ /** @internal Exposed to ChatComponent for context creation */
107
+ acpClient!: ReturnType<AgentClientPlugin["getOrCreateAcpClient"]>;
108
+ /** @internal Exposed to ChatComponent for context creation */
109
+ vaultService!: VaultService;
110
+
111
+ // Callbacks from ChatPanel for IChatViewContainer delegation
112
+ private callbacks: ChatPanelCallbacks | null = null;
113
+
114
+ constructor(leaf: WorkspaceLeaf, plugin: AgentClientPlugin) {
115
+ super(leaf);
116
+ this.plugin = plugin;
117
+ this.logger = getLogger();
118
+ // Static sidebar view (not navigable) — hides .view-header
119
+ this.navigation = false;
120
+ // Use leaf.id if available, otherwise generate UUID
121
+ this.viewId = (leaf as { id?: string }).id ?? crypto.randomUUID();
122
+ }
123
+
124
+ getViewType() {
125
+ return VIEW_TYPE_CHAT;
126
+ }
127
+
128
+ getDisplayText() {
129
+ return "Agent client";
130
+ }
131
+
132
+ getIcon() {
133
+ return "bot-message-square";
134
+ }
135
+
136
+ /**
137
+ * Get the view state for persistence.
138
+ */
139
+ getState(): ChatViewState {
140
+ return {
141
+ initialAgentId: this.initialAgentId ?? undefined,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Restore the view state from persistence.
147
+ * Notifies React when agentId is restored so it can re-create the session.
148
+ */
149
+ async setState(
150
+ state: ChatViewState,
151
+ result: { history: boolean },
152
+ ): Promise<void> {
153
+ const previousAgentId = this.initialAgentId;
154
+ this.initialAgentId = state.initialAgentId ?? null;
155
+ await super.setState(state, result);
156
+
157
+ // Notify React when agentId is restored and differs from previous value
158
+ if (this.initialAgentId && this.initialAgentId !== previousAgentId) {
159
+ this.agentIdRestoredCallbacks.forEach((cb) =>
160
+ cb(this.initialAgentId!),
161
+ );
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Get the initial agent ID for this view.
167
+ * Used by ChatComponent to determine which agent to initialize.
168
+ */
169
+ getInitialAgentId(): string | null {
170
+ return this.initialAgentId;
171
+ }
172
+
173
+ /**
174
+ * Set the agent ID for this view.
175
+ * Called when agent is switched to persist the change.
176
+ */
177
+ setAgentId(agentId: string): void {
178
+ this.initialAgentId = agentId;
179
+ // Request workspace to save the updated state
180
+ this.app.workspace.requestSaveLayout();
181
+ }
182
+
183
+ /**
184
+ * Register a callback to be notified when agentId is restored from workspace state.
185
+ * Used by React components to sync with Obsidian's setState lifecycle.
186
+ * @returns Unsubscribe function
187
+ */
188
+ onAgentIdRestored(callback: (agentId: string) => void): () => void {
189
+ this.agentIdRestoredCallbacks.add(callback);
190
+ return () => {
191
+ this.agentIdRestoredCallbacks.delete(callback);
192
+ };
193
+ }
194
+
195
+ // ============================================================
196
+ // Callbacks from ChatPanel
197
+ // ============================================================
198
+
199
+ /**
200
+ * Register callbacks from ChatPanel for IChatViewContainer delegation.
201
+ */
202
+ setCallbacks(callbacks: ChatPanelCallbacks): void {
203
+ this.callbacks = callbacks;
204
+ }
205
+
206
+ getDisplayName(): string {
207
+ return this.callbacks?.getDisplayName() ?? "Chat";
208
+ }
209
+
210
+ /**
211
+ * Get current input state (text + images).
212
+ * Returns null if React component not mounted.
213
+ */
214
+ getInputState(): ChatInputState | null {
215
+ return this.callbacks?.getInputState() ?? null;
216
+ }
217
+
218
+ /**
219
+ * Set input state (text + images).
220
+ */
221
+ setInputState(state: ChatInputState): void {
222
+ this.callbacks?.setInputState(state);
223
+ }
224
+
225
+ /**
226
+ * Trigger send message. Returns true if message was sent.
227
+ */
228
+ async sendMessage(): Promise<boolean> {
229
+ return (await this.callbacks?.sendMessage()) ?? false;
230
+ }
231
+
232
+ /**
233
+ * Check if this view can send a message.
234
+ */
235
+ canSend(): boolean {
236
+ return this.callbacks?.canSend() ?? false;
237
+ }
238
+
239
+ /**
240
+ * Cancel current operation.
241
+ */
242
+ async cancelOperation(): Promise<void> {
243
+ await this.callbacks?.cancelOperation();
244
+ }
245
+
246
+ // ============================================================
247
+ // IChatViewContainer Implementation
248
+ // ============================================================
249
+
250
+ /**
251
+ * Called when this view becomes the active/focused view.
252
+ */
253
+ onActivate(): void {
254
+ this.logger.log(`[ChatView] Activated: ${this.viewId}`);
255
+ }
256
+
257
+ /**
258
+ * Called when this view loses active/focused status.
259
+ */
260
+ onDeactivate(): void {
261
+ this.logger.log(`[ChatView] Deactivated: ${this.viewId}`);
262
+ }
263
+
264
+ /**
265
+ * Programmatically focus this view's input.
266
+ * Reveals the leaf first so that Obsidian switches to this tab
267
+ * before focusing the textarea (required for sidebar tabs).
268
+ */
269
+ focus(): void {
270
+ void this.app.workspace.revealLeaf(this.leaf).then(() => {
271
+ const textarea = this.containerEl.querySelector(
272
+ "textarea.agent-client-chat-input-textarea",
273
+ );
274
+ if (textarea instanceof HTMLTextAreaElement) {
275
+ textarea.focus();
276
+ }
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Check if this view currently has focus.
282
+ */
283
+ hasFocus(): boolean {
284
+ return this.containerEl.contains(activeDocument.activeElement);
285
+ }
286
+
287
+ /**
288
+ * Expand the view if it's in a collapsed state.
289
+ * Sidebar views don't have expand/collapse state - no-op.
290
+ */
291
+ expand(): void {
292
+ // Sidebar views don't have expand/collapse state - no-op
293
+ }
294
+
295
+ collapse(): void {
296
+ // Sidebar views don't have expand/collapse state - no-op
297
+ }
298
+
299
+ /**
300
+ * Get the DOM container element for this view.
301
+ */
302
+ getContainerEl(): HTMLElement {
303
+ return this.containerEl;
304
+ }
305
+
306
+ onOpen() {
307
+ const container = this.containerEl.children[1];
308
+ container.empty();
309
+
310
+ // Create services owned by this class
311
+ this.acpClient = this.plugin.getOrCreateAcpClient(this.viewId);
312
+ this.vaultService = new VaultService(this.plugin);
313
+
314
+ this.root = createRoot(container);
315
+ this.root.render(
316
+ <ChatComponent
317
+ plugin={this.plugin}
318
+ view={this}
319
+ viewId={this.viewId}
320
+ />,
321
+ );
322
+
323
+ // Register with plugin's view registry
324
+ this.plugin.viewRegistry.register(this);
325
+
326
+ return Promise.resolve();
327
+ }
328
+
329
+ async onClose(): Promise<void> {
330
+ this.logger.log("[ChatView] onClose() called");
331
+
332
+ // Unregister from plugin's view registry
333
+ this.plugin.viewRegistry.unregister(this.viewId);
334
+
335
+ // Cleanup is handled by React useEffect cleanup in ChatPanel
336
+ // which performs auto-export and closeSession
337
+ if (this.root) {
338
+ this.root.unmount();
339
+ this.root = null;
340
+ }
341
+
342
+ // Cleanup services owned by this class
343
+ this.vaultService?.destroy();
344
+
345
+ // Remove adapter for this view (disconnect process)
346
+ await this.plugin.removeAcpClient(this.viewId);
347
+ }
348
+ }
@@ -0,0 +1,104 @@
1
+ import * as React from "react";
2
+ const { useEffect } = React;
3
+ import { setIcon } from "obsidian";
4
+ import type { ErrorInfo, OverlayVariant } from "../types/errors";
5
+ import { LucideIcon } from "./shared/IconButton";
6
+ import type { IChatViewHost } from "./view-host";
7
+
8
+ export interface ErrorBannerProps {
9
+ /** Error information to display */
10
+ errorInfo: ErrorInfo;
11
+ /** Callback to close/clear the error */
12
+ onClose: () => void;
13
+ /** Whether to show emojis */
14
+ showEmojis: boolean;
15
+ /** View instance for event registration */
16
+ view: IChatViewHost;
17
+ /** Visual variant. Defaults to "error" for backward compatibility. */
18
+ variant?: OverlayVariant;
19
+ }
20
+
21
+ /**
22
+ * Banner component displayed above the input field.
23
+ *
24
+ * Supports visual variants:
25
+ * - "error" (default): Red border/title — for process errors and failures
26
+ * - "info": Subtle border/title — for update notifications
27
+ *
28
+ * Design decisions:
29
+ * - Uses same positioning pattern as SuggestionPopup (position: absolute; bottom: 100%)
30
+ * - Closes on Escape key or close button
31
+ * - Does not block chat messages from being visible
32
+ */
33
+ export function ErrorBanner({
34
+ errorInfo,
35
+ onClose,
36
+ showEmojis,
37
+ view,
38
+ variant = "error",
39
+ }: ErrorBannerProps) {
40
+ // Handle Escape key to close
41
+ useEffect(() => {
42
+ const handleKeyDown = (event: KeyboardEvent) => {
43
+ if (event.key === "Escape") {
44
+ onClose();
45
+ event.preventDefault();
46
+ }
47
+ };
48
+
49
+ view.registerDomEvent(activeDocument, "keydown", handleKeyDown);
50
+ }, [onClose, view]);
51
+
52
+ return (
53
+ <div
54
+ className={`agent-client-error-overlay agent-client-error-overlay--${variant}`}
55
+ >
56
+ <div className="agent-client-error-overlay-header">
57
+ <h4 className="agent-client-error-overlay-title">
58
+ {errorInfo.title}
59
+ </h4>
60
+ <button
61
+ className="agent-client-error-overlay-close"
62
+ onClick={onClose}
63
+ aria-label="Close"
64
+ type="button"
65
+ ref={(el) => {
66
+ if (el) {
67
+ setIcon(el, "x");
68
+ }
69
+ }}
70
+ />
71
+ </div>
72
+ <p className="agent-client-error-overlay-message">
73
+ {errorInfo.message}
74
+ </p>
75
+ {errorInfo.suggestion && (
76
+ <div className="agent-client-error-overlay-suggestion">
77
+ {showEmojis && variant === "error" && (
78
+ <LucideIcon
79
+ name="circle-alert"
80
+ className="agent-client-error-overlay-suggestion-icon"
81
+ />
82
+ )}
83
+ {variant !== "error" ? (
84
+ <code className="agent-client-error-overlay-code">
85
+ {errorInfo.suggestion}
86
+ </code>
87
+ ) : (
88
+ errorInfo.suggestion
89
+ )}
90
+ </div>
91
+ )}
92
+ {errorInfo.link && (
93
+ <a
94
+ className="agent-client-error-overlay-link"
95
+ href={errorInfo.link.url}
96
+ target="_blank"
97
+ rel="noopener noreferrer"
98
+ >
99
+ {errorInfo.link.text}
100
+ </a>
101
+ )}
102
+ </div>
103
+ );
104
+ }