@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,57 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { setIcon } from "obsidian";
|
|
3
|
+
import type { AttachedFile } from "../../types/chat";
|
|
4
|
+
|
|
5
|
+
interface AttachmentStripProps {
|
|
6
|
+
files: AttachedFile[];
|
|
7
|
+
onRemove: (id: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Horizontal strip of attachment previews with remove buttons.
|
|
12
|
+
* - Images: show thumbnail
|
|
13
|
+
* - Files: show file icon with filename
|
|
14
|
+
*/
|
|
15
|
+
export function AttachmentStrip({ files, onRemove }: AttachmentStripProps) {
|
|
16
|
+
if (files.length === 0) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="agent-client-attachment-preview-strip">
|
|
20
|
+
{files.map((file) => (
|
|
21
|
+
<div
|
|
22
|
+
key={file.id}
|
|
23
|
+
className="agent-client-attachment-preview-item"
|
|
24
|
+
>
|
|
25
|
+
{file.kind === "image" && file.data ? (
|
|
26
|
+
<img
|
|
27
|
+
src={`data:${file.mimeType};base64,${file.data}`}
|
|
28
|
+
alt="Attached image"
|
|
29
|
+
className="agent-client-attachment-preview-thumbnail"
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
<div className="agent-client-attachment-preview-file">
|
|
33
|
+
<span
|
|
34
|
+
className="agent-client-attachment-preview-file-icon"
|
|
35
|
+
ref={(el) => {
|
|
36
|
+
if (el) setIcon(el, "file");
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
<span className="agent-client-attachment-preview-file-name">
|
|
40
|
+
{file.name ?? "file"}
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
<button
|
|
45
|
+
className="agent-client-attachment-preview-remove"
|
|
46
|
+
onClick={() => onRemove(file.id)}
|
|
47
|
+
title="Remove attachment"
|
|
48
|
+
type="button"
|
|
49
|
+
ref={(el) => {
|
|
50
|
+
if (el) setIcon(el, "x");
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
const { useRef, useEffect, useImperativeHandle, forwardRef } = React;
|
|
3
|
+
import { setIcon } from "obsidian";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders an Obsidian Lucide icon via setIcon().
|
|
7
|
+
* Used as a replacement for emoji icons to match Obsidian's native UI.
|
|
8
|
+
*/
|
|
9
|
+
export function LucideIcon({
|
|
10
|
+
name,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
name: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}) {
|
|
16
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (ref.current) {
|
|
20
|
+
setIcon(ref.current, name);
|
|
21
|
+
}
|
|
22
|
+
}, [name]);
|
|
23
|
+
|
|
24
|
+
return <span ref={ref} className={className} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HeaderButtonProps {
|
|
28
|
+
iconName: string;
|
|
29
|
+
tooltip: string;
|
|
30
|
+
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const HeaderButton = forwardRef<HTMLButtonElement, HeaderButtonProps>(
|
|
34
|
+
function HeaderButton({ iconName, tooltip, onClick }, ref) {
|
|
35
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
36
|
+
|
|
37
|
+
// Expose the button ref to parent components
|
|
38
|
+
useImperativeHandle(ref, () => buttonRef.current!, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (buttonRef.current) {
|
|
42
|
+
setIcon(buttonRef.current, iconName);
|
|
43
|
+
}
|
|
44
|
+
}, [iconName]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
ref={buttonRef}
|
|
49
|
+
title={tooltip}
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
className="clickable-icon agent-client-header-button"
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
const { useRef, useEffect } = React;
|
|
3
|
+
import {
|
|
4
|
+
Component,
|
|
5
|
+
FileSystemAdapter,
|
|
6
|
+
MarkdownRenderer as ObsidianMarkdownRenderer,
|
|
7
|
+
Platform,
|
|
8
|
+
} from "obsidian";
|
|
9
|
+
import { convertWslPathToWindows } from "../../utils/platform";
|
|
10
|
+
import { isAbsolutePath } from "../../utils/paths";
|
|
11
|
+
import type AgentClientPlugin from "../../plugin";
|
|
12
|
+
|
|
13
|
+
interface MarkdownRendererProps {
|
|
14
|
+
text: string;
|
|
15
|
+
plugin: AgentClientPlugin;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function MarkdownRenderer({ text, plugin }: MarkdownRendererProps) {
|
|
19
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const el = containerRef.current;
|
|
23
|
+
if (!el) return;
|
|
24
|
+
el.empty?.();
|
|
25
|
+
el.classList.add("markdown-rendered");
|
|
26
|
+
|
|
27
|
+
// Create a temporary component for the markdown renderer lifecycle
|
|
28
|
+
const component = new Component();
|
|
29
|
+
component.load();
|
|
30
|
+
|
|
31
|
+
// Render markdown
|
|
32
|
+
void ObsidianMarkdownRenderer.render(
|
|
33
|
+
plugin.app,
|
|
34
|
+
text,
|
|
35
|
+
el,
|
|
36
|
+
"",
|
|
37
|
+
component,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Handle internal link clicks
|
|
41
|
+
const vaultBasePath =
|
|
42
|
+
plugin.app.vault.adapter instanceof FileSystemAdapter
|
|
43
|
+
? plugin.app.vault.adapter.getBasePath()
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
// Prepare normalized vault base path for comparison (forward slashes)
|
|
47
|
+
const isWslMode = Platform.isWin && plugin.settings.windowsWslMode;
|
|
48
|
+
const normalizedVaultBase = vaultBasePath
|
|
49
|
+
? vaultBasePath.replace(/\\/g, "/").replace(/\/+$/, "")
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
const handleInternalLinkClick = (e: MouseEvent) => {
|
|
53
|
+
const target = e.target as HTMLElement;
|
|
54
|
+
const link = target.closest("a.internal-link");
|
|
55
|
+
if (link) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
const rawHref = link.getAttribute("data-href");
|
|
58
|
+
if (rawHref) {
|
|
59
|
+
let href = decodeURIComponent(rawHref);
|
|
60
|
+
|
|
61
|
+
// WSL mode: convert /mnt/c/... paths to Windows format
|
|
62
|
+
if (isWslMode && href.startsWith("/mnt/")) {
|
|
63
|
+
href = convertWslPathToWindows(href);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Normalize for comparison (forward slashes)
|
|
67
|
+
const normalizedHref = href.replace(/\\/g, "/");
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
normalizedVaultBase &&
|
|
71
|
+
normalizedHref.startsWith(normalizedVaultBase + "/")
|
|
72
|
+
) {
|
|
73
|
+
// Absolute vault path → convert to relative
|
|
74
|
+
const relativePath = normalizedHref.slice(
|
|
75
|
+
normalizedVaultBase.length + 1,
|
|
76
|
+
);
|
|
77
|
+
void plugin.app.workspace.openLinkText(
|
|
78
|
+
relativePath,
|
|
79
|
+
"",
|
|
80
|
+
);
|
|
81
|
+
} else if (!isAbsolutePath(href)) {
|
|
82
|
+
// Already relative or wiki-link style — pass through
|
|
83
|
+
void plugin.app.workspace.openLinkText(href, "");
|
|
84
|
+
}
|
|
85
|
+
// Absolute path outside vault — ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
el.addEventListener("click", handleInternalLinkClick);
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
el.removeEventListener("click", handleInternalLinkClick);
|
|
93
|
+
component.unload();
|
|
94
|
+
};
|
|
95
|
+
}, [text, plugin]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
ref={containerRef}
|
|
100
|
+
className="agent-client-markdown-text-renderer"
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal view interface for chat components.
|
|
3
|
+
*
|
|
4
|
+
* This interface extracts the minimal set of methods that ChatMessages,
|
|
5
|
+
* ChatInput, and other components need from a view. By depending on this
|
|
6
|
+
* interface instead of ChatView directly, these components can work with
|
|
7
|
+
* both sidebar ChatView and FloatingChatView.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { App } from "obsidian";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal interface for components that need view-level DOM event registration.
|
|
14
|
+
*
|
|
15
|
+
* ChatMessages, ChatInput, SuggestionPopup, and ErrorBanner use this
|
|
16
|
+
* for registering scroll and click-outside handlers.
|
|
17
|
+
*
|
|
18
|
+
* Note on `this: HTMLElement` in callback signatures:
|
|
19
|
+
* - This matches Obsidian's Component.registerDomEvent signature for compatibility
|
|
20
|
+
* - In practice, callbacks use arrow functions and don't reference `this`
|
|
21
|
+
* - We maintain this signature to allow ChatView to implement IChatViewHost
|
|
22
|
+
* without type casting (ChatView extends Component which has this signature)
|
|
23
|
+
*/
|
|
24
|
+
export interface IChatViewHost {
|
|
25
|
+
/** Obsidian App instance for API access */
|
|
26
|
+
app: App;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a DOM event listener that will be cleaned up when the view closes.
|
|
30
|
+
*
|
|
31
|
+
* In sidebar ChatView, this delegates to Obsidian's Component.registerDomEvent.
|
|
32
|
+
* In floating views, this adds the listener and tracks it for cleanup on unmount.
|
|
33
|
+
*
|
|
34
|
+
* Note: Only Window, Document, and HTMLElement are supported as targets.
|
|
35
|
+
* This matches the actual usage in components (document for click-outside,
|
|
36
|
+
* HTMLElement for scroll handlers).
|
|
37
|
+
*/
|
|
38
|
+
registerDomEvent<K extends keyof WindowEventMap>(
|
|
39
|
+
el: Window,
|
|
40
|
+
type: K,
|
|
41
|
+
callback: (this: HTMLElement, ev: WindowEventMap[K]) => unknown,
|
|
42
|
+
options?: boolean | AddEventListenerOptions,
|
|
43
|
+
): void;
|
|
44
|
+
registerDomEvent<K extends keyof DocumentEventMap>(
|
|
45
|
+
el: Document,
|
|
46
|
+
type: K,
|
|
47
|
+
callback: (this: HTMLElement, ev: DocumentEventMap[K]) => unknown,
|
|
48
|
+
options?: boolean | AddEventListenerOptions,
|
|
49
|
+
): void;
|
|
50
|
+
registerDomEvent<K extends keyof HTMLElementEventMap>(
|
|
51
|
+
el: HTMLElement,
|
|
52
|
+
type: K,
|
|
53
|
+
callback: (this: HTMLElement, ev: HTMLElementEventMap[K]) => unknown,
|
|
54
|
+
options?: boolean | AddEventListenerOptions,
|
|
55
|
+
): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Error Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for handling ACP protocol errors and converting them
|
|
5
|
+
* to user-friendly ErrorInfo for UI display.
|
|
6
|
+
*
|
|
7
|
+
* These functions extract error information from ACP JSON-RPC errors
|
|
8
|
+
* and provide appropriate titles and suggestions based on error codes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Platform } from "obsidian";
|
|
12
|
+
import { AcpErrorCode, type AcpError, type ErrorInfo } from "../types/errors";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Error Extraction Functions
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract error code from unknown error object.
|
|
20
|
+
*/
|
|
21
|
+
export function extractErrorCode(error: unknown): number | undefined {
|
|
22
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
23
|
+
const code = error.code;
|
|
24
|
+
if (typeof code === "number") return code;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract error message from ACP error object.
|
|
31
|
+
* Checks both `message` field and `data.details` for compatibility.
|
|
32
|
+
*/
|
|
33
|
+
export function extractErrorMessage(error: unknown): string {
|
|
34
|
+
if (!error || typeof error !== "object") {
|
|
35
|
+
return "An unexpected error occurred.";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check data.details first (some agents use this format)
|
|
39
|
+
if ("data" in error) {
|
|
40
|
+
const data = error.data;
|
|
41
|
+
if (data && typeof data === "object" && "details" in data) {
|
|
42
|
+
const details = data.details;
|
|
43
|
+
if (typeof details === "string") return details;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Then check message
|
|
48
|
+
if ("message" in error) {
|
|
49
|
+
const msg = error.message;
|
|
50
|
+
if (typeof msg === "string") return msg;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return "An unexpected error occurred.";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract error data from ACP error object.
|
|
58
|
+
*/
|
|
59
|
+
export function extractErrorData(error: unknown): unknown {
|
|
60
|
+
if (error && typeof error === "object" && "data" in error) {
|
|
61
|
+
return error.data;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Error Classification Functions
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get user-friendly title for ACP error code.
|
|
72
|
+
*/
|
|
73
|
+
export function getErrorTitle(code: number | undefined): string {
|
|
74
|
+
switch (code) {
|
|
75
|
+
case AcpErrorCode.PARSE_ERROR:
|
|
76
|
+
return "Protocol Error";
|
|
77
|
+
case AcpErrorCode.INVALID_REQUEST:
|
|
78
|
+
return "Invalid Request";
|
|
79
|
+
case AcpErrorCode.METHOD_NOT_FOUND:
|
|
80
|
+
return "Method Not Supported";
|
|
81
|
+
case AcpErrorCode.INVALID_PARAMS:
|
|
82
|
+
return "Invalid Parameters";
|
|
83
|
+
case AcpErrorCode.INTERNAL_ERROR:
|
|
84
|
+
return "Internal Error";
|
|
85
|
+
case AcpErrorCode.AUTHENTICATION_REQUIRED:
|
|
86
|
+
return "Authentication Required";
|
|
87
|
+
case AcpErrorCode.RESOURCE_NOT_FOUND:
|
|
88
|
+
return "Resource Not Found";
|
|
89
|
+
default:
|
|
90
|
+
return "Agent Error";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get suggestion for ACP error code.
|
|
96
|
+
* Uses error message content to provide more specific suggestions.
|
|
97
|
+
*/
|
|
98
|
+
export function getErrorSuggestion(
|
|
99
|
+
code: number | undefined,
|
|
100
|
+
message: string,
|
|
101
|
+
): string {
|
|
102
|
+
// Check for context exhaustion in message (Internal Error)
|
|
103
|
+
if (code === AcpErrorCode.INTERNAL_ERROR) {
|
|
104
|
+
const lowerMsg = message.toLowerCase();
|
|
105
|
+
if (
|
|
106
|
+
lowerMsg.includes("context") ||
|
|
107
|
+
lowerMsg.includes("token") ||
|
|
108
|
+
lowerMsg.includes("max_tokens") ||
|
|
109
|
+
lowerMsg.includes("too long")
|
|
110
|
+
) {
|
|
111
|
+
return "The conversation is too long. Try using a compact command if available, or start a new chat.";
|
|
112
|
+
}
|
|
113
|
+
if (lowerMsg.includes("overloaded") || lowerMsg.includes("capacity")) {
|
|
114
|
+
return "The service is busy. Please wait a moment and try again.";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (code) {
|
|
119
|
+
case AcpErrorCode.PARSE_ERROR:
|
|
120
|
+
case AcpErrorCode.INVALID_REQUEST:
|
|
121
|
+
case AcpErrorCode.METHOD_NOT_FOUND:
|
|
122
|
+
return "Try restarting the agent session.";
|
|
123
|
+
case AcpErrorCode.INVALID_PARAMS:
|
|
124
|
+
return "Check your agent configuration in settings.";
|
|
125
|
+
case AcpErrorCode.INTERNAL_ERROR:
|
|
126
|
+
return "Try again or restart the agent session.";
|
|
127
|
+
case AcpErrorCode.AUTHENTICATION_REQUIRED:
|
|
128
|
+
return "Check if you are logged in or if your API key is set correctly.";
|
|
129
|
+
case AcpErrorCode.RESOURCE_NOT_FOUND:
|
|
130
|
+
return "Check if the file or resource exists.";
|
|
131
|
+
default:
|
|
132
|
+
return "Try again or restart the agent session.";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Error Conversion Functions
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convert unknown error to AcpError.
|
|
142
|
+
* The error's message field is used directly for user display.
|
|
143
|
+
*/
|
|
144
|
+
export function toAcpError(
|
|
145
|
+
error: unknown,
|
|
146
|
+
sessionId?: string | null,
|
|
147
|
+
): AcpError {
|
|
148
|
+
const code = extractErrorCode(error) ?? -1;
|
|
149
|
+
const message = extractErrorMessage(error);
|
|
150
|
+
const data = extractErrorData(error);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
code,
|
|
154
|
+
message, // Agent's message is used directly
|
|
155
|
+
data,
|
|
156
|
+
sessionId,
|
|
157
|
+
originalError: error,
|
|
158
|
+
title: getErrorTitle(code),
|
|
159
|
+
suggestion: getErrorSuggestion(code, message),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert AcpError to ErrorInfo for UI display.
|
|
165
|
+
*/
|
|
166
|
+
export function toErrorInfo(acpError: AcpError): ErrorInfo {
|
|
167
|
+
return {
|
|
168
|
+
title: acpError.title,
|
|
169
|
+
message: acpError.message,
|
|
170
|
+
suggestion: acpError.suggestion,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Error Check Functions
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if error is the "empty response text" error that should be ignored.
|
|
180
|
+
*/
|
|
181
|
+
export function isEmptyResponseError(error: unknown): boolean {
|
|
182
|
+
const code = extractErrorCode(error);
|
|
183
|
+
if (code !== AcpErrorCode.INTERNAL_ERROR) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const message = extractErrorMessage(error);
|
|
188
|
+
return message.includes("empty response text");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Extract a user-friendly error hint from stderr output.
|
|
193
|
+
* Detects common failure patterns like missing API keys.
|
|
194
|
+
*/
|
|
195
|
+
export function extractStderrErrorHint(stderr: string): string | null {
|
|
196
|
+
if (!stderr) return null;
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
stderr.includes("API key is missing") ||
|
|
200
|
+
stderr.includes("LoadAPIKeyError")
|
|
201
|
+
) {
|
|
202
|
+
return "The agent's API key may be missing. For custom agents, add the required API key (e.g., ANTHROPIC_API_KEY) in the agent's Environment Variables setting.";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
stderr.includes("authentication") ||
|
|
207
|
+
stderr.includes("unauthorized") ||
|
|
208
|
+
stderr.includes("401")
|
|
209
|
+
) {
|
|
210
|
+
return "The agent reported an authentication error. Check that your API key or credentials are valid.";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if error is a "user aborted" error that should be ignored.
|
|
218
|
+
*/
|
|
219
|
+
export function isUserAbortedError(error: unknown): boolean {
|
|
220
|
+
const code = extractErrorCode(error);
|
|
221
|
+
if (code !== AcpErrorCode.INTERNAL_ERROR) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const message = extractErrorMessage(error);
|
|
226
|
+
return message.includes("user aborted");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ============================================================================
|
|
230
|
+
// Process Error Functions
|
|
231
|
+
// ============================================================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get error information for process spawn errors.
|
|
235
|
+
*/
|
|
236
|
+
export function getSpawnErrorInfo(
|
|
237
|
+
error: Error,
|
|
238
|
+
command: string,
|
|
239
|
+
agentLabel: string,
|
|
240
|
+
wslMode: boolean,
|
|
241
|
+
): { title: string; message: string; suggestion: string } {
|
|
242
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
243
|
+
return {
|
|
244
|
+
title: "Command Not Found",
|
|
245
|
+
message: `The command "${command}" could not be found. Please check the path configuration for ${agentLabel}.`,
|
|
246
|
+
suggestion: getCommandNotFoundSuggestion(command, wslMode),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
title: "Agent Startup Error",
|
|
252
|
+
message: `Failed to start ${agentLabel}: ${error.message}`,
|
|
253
|
+
suggestion: "Please check the agent configuration in settings.",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get platform-specific suggestions for command not found errors.
|
|
259
|
+
*/
|
|
260
|
+
export function getCommandNotFoundSuggestion(
|
|
261
|
+
command: string,
|
|
262
|
+
wslMode: boolean,
|
|
263
|
+
): string {
|
|
264
|
+
const commandName =
|
|
265
|
+
command.split("/").pop()?.split("\\").pop() || "command";
|
|
266
|
+
|
|
267
|
+
if (Platform.isWin && wslMode) {
|
|
268
|
+
return `1. Verify the agent path: Use "which ${commandName}" in your WSL terminal to find the correct path. 2. If the agent requires Node.js, also check that Node.js path is correctly set in General Settings (use "which node" to find it).`;
|
|
269
|
+
} else if (Platform.isWin) {
|
|
270
|
+
return `1. Verify the agent path: Use "where ${commandName}" in Command Prompt to find the correct path. 2. If the agent requires Node.js, also check that Node.js path is correctly set in General Settings (use "where node" to find it).`;
|
|
271
|
+
} else {
|
|
272
|
+
return `1. Verify the agent path: Use "which ${commandName}" in Terminal to find the correct path. 2. If the agent requires Node.js, also check that Node.js path is correctly set in General Settings (use "which node" to find it).`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface LoggerConfig {
|
|
2
|
+
debugMode: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
let globalLogger: Logger | null = null;
|
|
6
|
+
|
|
7
|
+
export function initializeLogger(config: LoggerConfig): void {
|
|
8
|
+
globalLogger = new Logger(config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getLogger(): Logger {
|
|
12
|
+
if (!globalLogger) {
|
|
13
|
+
return new Logger({ debugMode: false });
|
|
14
|
+
}
|
|
15
|
+
return globalLogger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Logger {
|
|
19
|
+
constructor(private config: LoggerConfig) {}
|
|
20
|
+
|
|
21
|
+
log(...args: unknown[]): void {
|
|
22
|
+
if (this.config.debugMode) {
|
|
23
|
+
console.debug(...args);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
error(...args: unknown[]): void {
|
|
28
|
+
if (this.config.debugMode) {
|
|
29
|
+
console.error(...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
warn(...args: unknown[]): void {
|
|
34
|
+
if (this.config.debugMode) {
|
|
35
|
+
console.warn(...args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
info(...args: unknown[]): void {
|
|
40
|
+
if (this.config.debugMode) {
|
|
41
|
+
console.debug(...args);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|