@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,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
|
+
}
|