@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.
- package/.claude/hooks/gh-setup.sh +49 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/release-notes/SKILL.md +331 -0
- package/.editorconfig +10 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
- package/.github/copilot-instructions.md +45 -0
- package/.github/pull_request_template.md +32 -0
- package/.github/workflows/ci.yaml +25 -0
- package/.github/workflows/docs.yml +58 -0
- package/.github/workflows/relay_to_openclaw.yml +59 -0
- package/.github/workflows/release.yaml +45 -0
- package/.prettierignore +10 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +37 -0
- package/.zed/settings.json +42 -0
- package/AGENTS.md +330 -0
- package/ARCHITECTURE.md +390 -0
- package/CONTRIBUTING.md +216 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.ja.md +121 -0
- package/README.md +125 -0
- package/docs/.vitepress/config.mts +124 -0
- package/docs/.vitepress/theme/custom.css +111 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/agent-setup/claude-code.md +84 -0
- package/docs/agent-setup/codex.md +76 -0
- package/docs/agent-setup/custom-agents.md +67 -0
- package/docs/agent-setup/gemini-cli.md +99 -0
- package/docs/agent-setup/index.md +34 -0
- package/docs/announcements/gemini-cli-deprecation.md +73 -0
- package/docs/getting-started/index.md +78 -0
- package/docs/getting-started/quick-start.md +38 -0
- package/docs/help/faq.md +181 -0
- package/docs/help/troubleshooting.md +221 -0
- package/docs/index.md +63 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/demo.mp4 +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/public/images/editing.webp +0 -0
- package/docs/public/images/export.webp +0 -0
- package/docs/public/images/floating-chat-button.webp +0 -0
- package/docs/public/images/floating-chat-instance-menu.webp +0 -0
- package/docs/public/images/floating-chat-view.webp +0 -0
- package/docs/public/images/mode-selection.webp +0 -0
- package/docs/public/images/model-selection.webp +0 -0
- package/docs/public/images/multi-session.webp +0 -0
- package/docs/public/images/remove-image.webp +0 -0
- package/docs/public/images/ribbon-icon.webp +0 -0
- package/docs/public/images/selection-context.gif +0 -0
- package/docs/public/images/sending-images.webp +0 -0
- package/docs/public/images/sending-messages.webp +0 -0
- package/docs/public/images/session-history-button.webp +0 -0
- package/docs/public/images/slash-commands-1.webp +0 -0
- package/docs/public/images/slash-commands-2.webp +0 -0
- package/docs/public/images/switch-agent.webp +0 -0
- package/docs/public/images/switch-default-agent.webp +0 -0
- package/docs/public/images/temporary-disable.gif +0 -0
- package/docs/reference/acp-support.md +110 -0
- package/docs/usage/chat-export.md +80 -0
- package/docs/usage/commands.md +51 -0
- package/docs/usage/context-files.md +57 -0
- package/docs/usage/editing.md +69 -0
- package/docs/usage/floating-chat.md +84 -0
- package/docs/usage/index.md +97 -0
- package/docs/usage/mcp-tools.md +33 -0
- package/docs/usage/mentions.md +70 -0
- package/docs/usage/mode-selection.md +28 -0
- package/docs/usage/model-selection.md +32 -0
- package/docs/usage/multi-session.md +68 -0
- package/docs/usage/sending-images.md +64 -0
- package/docs/usage/session-history.md +91 -0
- package/docs/usage/slash-commands.md +44 -0
- package/esbuild.config.mjs +49 -0
- package/eslint.config.mjs +25 -0
- package/main.js +228 -0
- package/manifest.json +11 -0
- package/package.json +52 -0
- package/src/acp/acp-client.ts +921 -0
- package/src/acp/acp-handler.ts +252 -0
- package/src/acp/permission-handler.ts +282 -0
- package/src/acp/terminal-handler.ts +264 -0
- package/src/acp/type-converter.ts +272 -0
- package/src/hooks/useAgent.ts +250 -0
- package/src/hooks/useAgentMessages.ts +470 -0
- package/src/hooks/useAgentSession.ts +544 -0
- package/src/hooks/useChatActions.ts +400 -0
- package/src/hooks/useHistoryModal.ts +219 -0
- package/src/hooks/useSessionHistory.ts +863 -0
- package/src/hooks/useSettings.ts +19 -0
- package/src/hooks/useSuggestions.ts +342 -0
- package/src/main.ts +9 -0
- package/src/plugin.ts +1126 -0
- package/src/services/chat-exporter.ts +552 -0
- package/src/services/message-sender.ts +755 -0
- package/src/services/message-state.ts +375 -0
- package/src/services/session-helpers.ts +211 -0
- package/src/services/session-state.ts +130 -0
- package/src/services/session-storage.ts +267 -0
- package/src/services/settings-normalizer.ts +255 -0
- package/src/services/settings-service.ts +285 -0
- package/src/services/update-checker.ts +128 -0
- package/src/services/vault-service.ts +558 -0
- package/src/services/view-registry.ts +345 -0
- package/src/types/agent.ts +92 -0
- package/src/types/chat.ts +351 -0
- package/src/types/errors.ts +136 -0
- package/src/types/obsidian-internals.d.ts +14 -0
- package/src/types/session.ts +731 -0
- package/src/ui/ChangeDirectoryModal.ts +137 -0
- package/src/ui/ChatContext.ts +25 -0
- package/src/ui/ChatHeader.tsx +295 -0
- package/src/ui/ChatPanel.tsx +1162 -0
- package/src/ui/ChatView.tsx +348 -0
- package/src/ui/ErrorBanner.tsx +104 -0
- package/src/ui/FloatingButton.tsx +351 -0
- package/src/ui/FloatingChatView.tsx +531 -0
- package/src/ui/InputArea.tsx +1107 -0
- package/src/ui/InputToolbar.tsx +371 -0
- package/src/ui/MessageBubble.tsx +442 -0
- package/src/ui/MessageList.tsx +265 -0
- package/src/ui/PermissionBanner.tsx +61 -0
- package/src/ui/SessionHistoryModal.tsx +821 -0
- package/src/ui/SettingsTab.ts +1337 -0
- package/src/ui/SuggestionPopup.tsx +138 -0
- package/src/ui/TerminalBlock.tsx +107 -0
- package/src/ui/ToolCallBlock.tsx +456 -0
- package/src/ui/shared/AttachmentStrip.tsx +57 -0
- package/src/ui/shared/IconButton.tsx +55 -0
- package/src/ui/shared/MarkdownRenderer.tsx +103 -0
- package/src/ui/view-host.ts +56 -0
- package/src/utils/error-utils.ts +274 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/mention-parser.ts +129 -0
- package/src/utils/paths.ts +246 -0
- package/src/utils/platform.ts +425 -0
- package/styles.css +2322 -0
- package/tsconfig.json +18 -0
- package/version-bump.mjs +18 -0
- 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
|
+
}
|