@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,137 @@
1
+ /**
2
+ * Modal for selecting a working directory for a new chat session.
3
+ *
4
+ * Provides a text input for manual path entry and a Browse button
5
+ * that opens the native OS folder picker via Electron's dialog API.
6
+ * Calls onSelect callback with the chosen path when user clicks Start.
7
+ */
8
+
9
+ import { Modal, App } from "obsidian";
10
+
11
+ export class ChangeDirectoryModal extends Modal {
12
+ private currentPath: string;
13
+ private onSelect: (path: string) => void | Promise<void>;
14
+
15
+ constructor(
16
+ app: App,
17
+ currentPath: string,
18
+ onSelect: (path: string) => void | Promise<void>,
19
+ ) {
20
+ super(app);
21
+ this.currentPath = currentPath;
22
+ this.onSelect = onSelect;
23
+ }
24
+
25
+ onOpen() {
26
+ const { contentEl } = this;
27
+ contentEl.empty();
28
+
29
+ contentEl.createEl("h2", { text: "New chat in directory" });
30
+
31
+ contentEl.createEl("p", {
32
+ text: "Start a new chat session with the agent working in the specified directory.",
33
+ cls: "agent-client-change-dir-description",
34
+ });
35
+
36
+ // Path input row (text input + browse button)
37
+ const inputRow = contentEl.createDiv({
38
+ cls: "agent-client-change-dir-input-row",
39
+ });
40
+
41
+ const inputEl = inputRow.createEl("input", {
42
+ type: "text",
43
+ cls: "agent-client-change-dir-input",
44
+ placeholder: "/path/to/directory",
45
+ });
46
+ inputEl.value = this.currentPath;
47
+
48
+ const browseButton = inputRow.createEl("button", {
49
+ text: "Browse...",
50
+ });
51
+ browseButton.addEventListener("click", () => {
52
+ void this.openFolderPicker().then((selectedPath) => {
53
+ if (selectedPath) {
54
+ inputEl.value = selectedPath;
55
+ }
56
+ });
57
+ });
58
+
59
+ // Focus and select all text
60
+ window.setTimeout(() => {
61
+ inputEl.focus();
62
+ inputEl.select();
63
+ }, 10);
64
+
65
+ // Enter key to start
66
+ inputEl.addEventListener("keydown", (e) => {
67
+ if (e.key === "Enter") {
68
+ e.preventDefault();
69
+ this.selectAndClose(inputEl.value);
70
+ }
71
+ });
72
+
73
+ // Buttons
74
+ const buttonContainer = contentEl.createDiv({
75
+ cls: "agent-client-change-dir-buttons",
76
+ });
77
+
78
+ buttonContainer
79
+ .createEl("button", { text: "Cancel" })
80
+ .addEventListener("click", () => {
81
+ this.close();
82
+ });
83
+
84
+ buttonContainer
85
+ .createEl("button", {
86
+ text: "Start",
87
+ cls: "mod-cta",
88
+ })
89
+ .addEventListener("click", () => {
90
+ this.selectAndClose(inputEl.value);
91
+ });
92
+ }
93
+
94
+ private async openFolderPicker(): Promise<string | null> {
95
+ try {
96
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- electron is a runtime-only module provided by Obsidian's host environment
97
+ const { remote } = require("electron") as {
98
+ remote: {
99
+ dialog: {
100
+ showOpenDialog: (options: {
101
+ properties: string[];
102
+ title: string;
103
+ defaultPath?: string;
104
+ }) => Promise<{
105
+ canceled: boolean;
106
+ filePaths: string[];
107
+ }>;
108
+ };
109
+ };
110
+ };
111
+ const result = await remote.dialog.showOpenDialog({
112
+ properties: ["openDirectory"],
113
+ title: "Select working directory",
114
+ defaultPath: this.currentPath,
115
+ });
116
+ if (!result.canceled && result.filePaths.length > 0) {
117
+ return result.filePaths[0];
118
+ }
119
+ } catch {
120
+ // Electron remote not available — ignore silently
121
+ // User can still type the path manually
122
+ }
123
+ return null;
124
+ }
125
+
126
+ private selectAndClose(rawValue: string) {
127
+ const value = rawValue.trim();
128
+ if (!value) return;
129
+ this.close();
130
+ void this.onSelect(value);
131
+ }
132
+
133
+ onClose() {
134
+ const { contentEl } = this;
135
+ contentEl.empty();
136
+ }
137
+ }
@@ -0,0 +1,25 @@
1
+ import { createContext, useContext } from "react";
2
+ import type AgentClientPlugin from "../plugin";
3
+ import type { AcpClient } from "../acp/acp-client";
4
+ import type { VaultService } from "../services/vault-service";
5
+ import type { SettingsService } from "../services/settings-service";
6
+
7
+ export interface ChatContextValue {
8
+ plugin: AgentClientPlugin;
9
+ acpClient: AcpClient;
10
+ vaultService: VaultService;
11
+ settingsService: SettingsService;
12
+ }
13
+
14
+ const ChatContext = createContext<ChatContextValue | null>(null);
15
+
16
+ export const ChatContextProvider = ChatContext.Provider;
17
+
18
+ export function useChatContext(): ChatContextValue {
19
+ const ctx = useContext(ChatContext);
20
+ if (!ctx)
21
+ throw new Error(
22
+ "useChatContext must be used within ChatContextProvider",
23
+ );
24
+ return ctx;
25
+ }
@@ -0,0 +1,295 @@
1
+ import * as React from "react";
2
+ const { useRef, useEffect } = React;
3
+ import { setIcon, DropdownComponent } from "obsidian";
4
+ import { HeaderButton } from "./shared/IconButton";
5
+ import type { AgentDisplayInfo } from "../services/session-helpers";
6
+
7
+ // ============================================================================
8
+ // Props Types
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Props for the sidebar variant of ChatHeader
13
+ */
14
+ export interface SidebarHeaderProps {
15
+ variant: "sidebar";
16
+ /** Display name of the active agent */
17
+ agentLabel: string;
18
+ /** Whether a plugin update is available */
19
+ isUpdateAvailable: boolean;
20
+ /** Callback to create a new chat session */
21
+ onNewChat: () => void;
22
+ /** Callback to export the chat */
23
+ onExportChat: () => void;
24
+ /** Callback to show the header menu at the click position */
25
+ onShowMenu: (e: React.MouseEvent<HTMLDivElement>) => void;
26
+ /** Callback to open session history */
27
+ onOpenHistory?: () => void;
28
+ }
29
+
30
+ /**
31
+ * Props for the floating variant of ChatHeader
32
+ */
33
+ export interface FloatingHeaderProps {
34
+ variant: "floating";
35
+ /** Display name of the active agent */
36
+ agentLabel: string;
37
+ /** Available agents for switching */
38
+ availableAgents: AgentDisplayInfo[];
39
+ /** Current agent ID */
40
+ currentAgentId: string;
41
+ /** Whether a plugin update is available */
42
+ isUpdateAvailable: boolean;
43
+ /** Callback to switch agent */
44
+ onAgentChange: (agentId: string) => void;
45
+ /** Callback to show the More menu at the click position */
46
+ onShowMenu: (e: React.MouseEvent<HTMLElement>) => void;
47
+ /** Callback to minimize window (floating only) */
48
+ onMinimize?: () => void;
49
+ /** Callback to close and terminate window (floating only) */
50
+ onClose?: () => void;
51
+ }
52
+
53
+ /**
54
+ * Union type for ChatHeader props - dispatches based on variant
55
+ */
56
+ export type ChatHeaderProps = SidebarHeaderProps | FloatingHeaderProps;
57
+
58
+ // ============================================================================
59
+ // Internal Components
60
+ // ============================================================================
61
+
62
+ /**
63
+ * A single action button matching Obsidian's nav-action-button pattern.
64
+ * Uses setIcon() to render Lucide icons identically to native sidebar buttons.
65
+ */
66
+ function NavActionButton({
67
+ icon,
68
+ label,
69
+ onClick,
70
+ }: {
71
+ icon: string;
72
+ label: string;
73
+ onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
74
+ }) {
75
+ const ref = useRef<HTMLDivElement>(null);
76
+
77
+ useEffect(() => {
78
+ if (ref.current) {
79
+ setIcon(ref.current, icon);
80
+ }
81
+ }, [icon]);
82
+
83
+ return (
84
+ <div
85
+ ref={ref}
86
+ className="clickable-icon nav-action-button"
87
+ aria-label={label}
88
+ onClick={onClick}
89
+ />
90
+ );
91
+ }
92
+
93
+ // ============================================================================
94
+ // Sidebar Header
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Header component for the sidebar chat view.
99
+ *
100
+ * Uses Obsidian's native .nav-header + .nav-buttons-container pattern
101
+ * to match the look of File Explorer, Bookmarks, and other sidebar panes.
102
+ */
103
+ function SidebarHeader({
104
+ agentLabel,
105
+ isUpdateAvailable,
106
+ onNewChat,
107
+ onExportChat,
108
+ onShowMenu,
109
+ onOpenHistory,
110
+ }: SidebarHeaderProps) {
111
+ return (
112
+ <div className="nav-header agent-client-chat-view-header">
113
+ <div className="nav-buttons-container">
114
+ <span className="agent-client-chat-view-header-title">
115
+ {agentLabel}
116
+ </span>
117
+ {isUpdateAvailable && (
118
+ <span className="agent-client-chat-view-header-update">
119
+ Plugin update available!
120
+ </span>
121
+ )}
122
+ <NavActionButton
123
+ icon="plus"
124
+ label="New chat"
125
+ onClick={onNewChat}
126
+ />
127
+ {onOpenHistory && (
128
+ <NavActionButton
129
+ icon="history"
130
+ label="Session history"
131
+ onClick={onOpenHistory}
132
+ />
133
+ )}
134
+ <NavActionButton
135
+ icon="save"
136
+ label="Export chat to Markdown"
137
+ onClick={onExportChat}
138
+ />
139
+ <NavActionButton
140
+ icon="more-vertical"
141
+ label="More"
142
+ onClick={onShowMenu}
143
+ />
144
+ </div>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ // ============================================================================
150
+ // Floating Header
151
+ // ============================================================================
152
+
153
+ /**
154
+ * Inline header component for Floating and CodeBlock chat views.
155
+ *
156
+ * Features:
157
+ * - Agent selector
158
+ * - Update notification (if available)
159
+ * - Action buttons with Lucide icons (new chat, history, export, restart)
160
+ * - Minimize and close buttons (floating variant only)
161
+ */
162
+ function FloatingHeader({
163
+ agentLabel,
164
+ availableAgents,
165
+ currentAgentId,
166
+ isUpdateAvailable,
167
+ onAgentChange,
168
+ onShowMenu,
169
+ onMinimize,
170
+ onClose,
171
+ }: FloatingHeaderProps) {
172
+ // Refs for agent dropdown
173
+ const agentDropdownRef = useRef<HTMLDivElement>(null);
174
+ const agentDropdownInstance = useRef<DropdownComponent | null>(null);
175
+
176
+ // Stable ref for onAgentChange callback
177
+ const onAgentChangeRef = useRef(onAgentChange);
178
+ onAgentChangeRef.current = onAgentChange;
179
+
180
+ // Initialize agent dropdown
181
+ useEffect(() => {
182
+ const containerEl = agentDropdownRef.current;
183
+ if (!containerEl) return;
184
+
185
+ // Only show dropdown if there are multiple agents
186
+ if (availableAgents.length <= 1) {
187
+ if (agentDropdownInstance.current) {
188
+ containerEl.empty();
189
+ agentDropdownInstance.current = null;
190
+ }
191
+ return;
192
+ }
193
+
194
+ // Create dropdown if not exists
195
+ if (!agentDropdownInstance.current) {
196
+ const dropdown = new DropdownComponent(containerEl);
197
+ agentDropdownInstance.current = dropdown;
198
+
199
+ // Add options
200
+ for (const agent of availableAgents) {
201
+ dropdown.addOption(agent.id, agent.displayName);
202
+ }
203
+
204
+ // Set initial value
205
+ if (currentAgentId) {
206
+ dropdown.setValue(currentAgentId);
207
+ }
208
+
209
+ // Handle change
210
+ dropdown.onChange((value) => {
211
+ onAgentChangeRef.current?.(value);
212
+ });
213
+ }
214
+
215
+ // Cleanup on unmount or when availableAgents change
216
+ return () => {
217
+ if (agentDropdownInstance.current) {
218
+ containerEl.empty();
219
+ agentDropdownInstance.current = null;
220
+ }
221
+ };
222
+ }, [availableAgents]);
223
+
224
+ // Update dropdown value when currentAgentId changes
225
+ useEffect(() => {
226
+ if (agentDropdownInstance.current && currentAgentId) {
227
+ agentDropdownInstance.current.setValue(currentAgentId);
228
+ }
229
+ }, [currentAgentId]);
230
+
231
+ return (
232
+ <div
233
+ className={`agent-client-inline-header agent-client-inline-header-floating`}
234
+ >
235
+ <div className="agent-client-inline-header-main">
236
+ {availableAgents.length > 1 ? (
237
+ <div className="agent-client-agent-selector">
238
+ <div ref={agentDropdownRef} />
239
+ <span
240
+ className="agent-client-agent-selector-icon"
241
+ ref={(el) => {
242
+ if (el) setIcon(el, "chevron-down");
243
+ }}
244
+ />
245
+ </div>
246
+ ) : (
247
+ <span className="agent-client-agent-label">
248
+ {agentLabel}
249
+ </span>
250
+ )}
251
+ </div>
252
+ {isUpdateAvailable && (
253
+ <p className="agent-client-chat-view-header-update">
254
+ Plugin update available!
255
+ </p>
256
+ )}
257
+ <div className="agent-client-inline-header-actions">
258
+ <HeaderButton
259
+ iconName="more-vertical"
260
+ tooltip="More"
261
+ onClick={onShowMenu}
262
+ />
263
+ {onMinimize && (
264
+ <HeaderButton
265
+ iconName="minimize-2"
266
+ tooltip="Minimize"
267
+ onClick={onMinimize}
268
+ />
269
+ )}
270
+ {onClose && (
271
+ <HeaderButton
272
+ iconName="x"
273
+ tooltip="Close"
274
+ onClick={onClose}
275
+ />
276
+ )}
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ // ============================================================================
283
+ // Exported ChatHeader (Dispatcher)
284
+ // ============================================================================
285
+
286
+ /**
287
+ * ChatHeader component that dispatches to SidebarHeader or FloatingHeader
288
+ * based on the `variant` prop.
289
+ */
290
+ export function ChatHeader(props: ChatHeaderProps) {
291
+ if (props.variant === "floating") {
292
+ return <FloatingHeader {...props} />;
293
+ }
294
+ return <SidebarHeader {...props} />;
295
+ }