summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-24 15:35:23 +0900
committerAdam Malczewski <[email protected]>2026-03-24 15:35:23 +0900
commitcab2ab3f848874bcceb9cadd6257056ba50cf8bb (patch)
tree23c501f624f01b5ea2939156fb214adb389f357c
parent20f409a21c16c18e614497aebf461282352d03ae (diff)
downloadai-pulse-obsidian-plugin-cab2ab3f848874bcceb9cadd6257056ba50cf8bb.tar.gz
ai-pulse-obsidian-plugin-cab2ab3f848874bcceb9cadd6257056ba50cf8bb.zip
add deletion tool. first tool with user approval prompt.
-rw-r--r--.rules/changelog/2026-03/24/10.md51
-rw-r--r--src/chat-view.ts56
-rw-r--r--src/ollama-client.ts127
-rw-r--r--src/settings-modal.ts123
-rw-r--r--src/settings.ts6
-rw-r--r--src/tools.ts70
-rw-r--r--styles.css133
7 files changed, 558 insertions, 8 deletions
diff --git a/.rules/changelog/2026-03/24/10.md b/.rules/changelog/2026-03/24/10.md
new file mode 100644
index 0000000..81d22b7
--- /dev/null
+++ b/.rules/changelog/2026-03/24/10.md
@@ -0,0 +1,51 @@
+# Changelog — 2026-03-24 #10
+
+## Mobile "Load failed" Fix (`src/ollama-client.ts`)
+
+- Imported `Platform` from Obsidian for runtime mobile detection.
+- Split `sendChatMessageStreaming` into a dispatcher + mobile/desktop implementations:
+ - Mobile: uses `requestUrl()` (non-streaming) to bypass WebView sandbox.
+ - Desktop: keeps native `fetch()` for real token-by-token streaming.
+- Enhanced error messages with mobile-specific hints.
+- Added `"load failed"` to caught network error patterns in `testConnection`.
+
+## UI: DaisyUI-inspired Collapse (`src/chat-view.ts`, `styles.css`)
+
+- Replaced native `<details>/<summary>` with a checkbox-driven CSS grid collapse.
+- Uses `grid-template-rows: 0fr → 1fr` transition with a rotating arrow indicator.
+- Tightened padding and margins around the collapse for compact layout.
+
+## UI: FAB / Speed Dial (`src/chat-view.ts`, `styles.css`)
+
+- Replaced inline settings/tools buttons with a FAB in the top-right of the messages area.
+- Main trigger: gear icon, rotates 90° on open.
+- Three actions fan downward with staggered animations:
+ - **AI Settings** (sliders icon) — opens the settings modal.
+ - **Tools** (wrench icon) — opens the tools modal.
+ - **Clear Chat** (trash icon) — clears message history and UI.
+- Removed old inline button styles and tools-active coloring.
+
+## UI: Text Selection (`styles.css`)
+
+- Enabled `user-select: text` on messages and tool call bubbles.
+
+## UI: Settings Modal Rename (`src/settings-modal.ts`)
+
+- Changed modal title to "AI Settings".
+
+## Generation Parameters (`src/settings.ts`, `src/settings-modal.ts`, `src/ollama-client.ts`, `src/chat-view.ts`)
+
+- Added `temperature` (default 0.7), `numCtx` (default 4096), `numPredict` (default -1) to settings.
+- Added `ModelOptions` interface and `options` field to `StreamingChatOptions`.
+- Options are now passed to Ollama in the request body for all chat paths.
+- Settings modal shows:
+ - **Temperature** — slider 0–2 with live value display.
+ - **Context Window** — number input with model max shown below.
+ - **Max Output Tokens** — number input (-1 = unlimited).
+- Added `showModel()` function querying `/api/show` to extract the model's context length.
+- Model max label turns red when context window exceeds model limit.
+- Clicking the model max label sets context window to the model's max.
+
+## Disabled GitHub Action (`.github/workflows/lint.yml`)
+
+- Commented out push/PR triggers; added `workflow_dispatch` for manual runs.
diff --git a/src/chat-view.ts b/src/chat-view.ts
index 524c731..3355689 100644
--- a/src/chat-view.ts
+++ b/src/chat-view.ts
@@ -1,6 +1,6 @@
import { ItemView, Notice, WorkspaceLeaf, setIcon } from "obsidian";
import type AIOrganizer from "./main";
-import type { ChatMessage, ToolCallEvent } from "./ollama-client";
+import type { ChatMessage, ToolCallEvent, ApprovalRequestEvent } from "./ollama-client";
import { sendChatMessageStreaming } from "./ollama-client";
import { SettingsModal } from "./settings-modal";
import { ToolModal } from "./tool-modal";
@@ -204,6 +204,10 @@ export class ChatView extends ItemView {
this.scrollToBottom();
};
+ const onApprovalRequest = (event: ApprovalRequestEvent): Promise<boolean> => {
+ return this.showApprovalRequest(event);
+ };
+
const onCreateBubble = (): void => {
// Finalize any previous bubble before creating a new one
if (currentBubble !== null) {
@@ -234,8 +238,14 @@ export class ChatView extends ItemView {
messages: this.messages,
tools: hasTools ? enabledTools : undefined,
app: hasTools ? this.plugin.app : undefined,
+ options: {
+ temperature: this.plugin.settings.temperature,
+ num_ctx: this.plugin.settings.numCtx,
+ num_predict: this.plugin.settings.numPredict,
+ },
onChunk,
onToolCall: hasTools ? onToolCall : undefined,
+ onApprovalRequest: hasTools ? onApprovalRequest : undefined,
onCreateBubble,
abortSignal: this.abortController.signal,
});
@@ -349,6 +359,50 @@ export class ChatView extends ItemView {
contentInner.createEl("pre", { text: resultPreview, cls: "ai-organizer-tool-call-result" });
}
+ private showApprovalRequest(event: ApprovalRequestEvent): Promise<boolean> {
+ return new Promise<boolean>((resolve) => {
+ if (this.messageContainer === null) {
+ resolve(false);
+ return;
+ }
+
+ const container = this.messageContainer.createDiv({ cls: "ai-organizer-approval" });
+
+ const header = container.createDiv({ cls: "ai-organizer-approval-header" });
+ setIcon(header.createSpan({ cls: "ai-organizer-approval-icon" }), "shield-alert");
+ header.createSpan({ text: event.friendlyName, cls: "ai-organizer-approval-name" });
+
+ container.createDiv({ text: event.message, cls: "ai-organizer-approval-message" });
+
+ const buttonRow = container.createDiv({ cls: "ai-organizer-approval-buttons" });
+
+ const approveBtn = buttonRow.createEl("button", {
+ text: "Approve",
+ cls: "ai-organizer-approval-approve",
+ });
+
+ const declineBtn = buttonRow.createEl("button", {
+ text: "Decline",
+ cls: "ai-organizer-approval-decline",
+ });
+
+ const finalize = (approved: boolean): void => {
+ approveBtn.disabled = true;
+ declineBtn.disabled = true;
+ container.addClass(approved ? "ai-organizer-approval-approved" : "ai-organizer-approval-declined");
+ const statusEl = container.createDiv({ cls: "ai-organizer-approval-status" });
+ statusEl.setText(approved ? "Approved" : "Declined");
+ this.scrollToBottom();
+ resolve(approved);
+ };
+
+ approveBtn.addEventListener("click", () => finalize(true));
+ declineBtn.addEventListener("click", () => finalize(false));
+
+ this.scrollToBottom();
+ });
+ }
+
private scrollToBottom(): void {
if (this.messageContainer !== null) {
this.messageContainer.scrollTop = this.messageContainer.scrollHeight;
diff --git a/src/ollama-client.ts b/src/ollama-client.ts
index 6ada18a..30e4d41 100644
--- a/src/ollama-client.ts
+++ b/src/ollama-client.ts
@@ -94,6 +94,54 @@ export async function listModels(ollamaUrl: string): Promise<string[]> {
}
/**
+ * Model info returned by /api/show.
+ */
+export interface ModelInfo {
+ contextLength: number;
+}
+
+/**
+ * Query Ollama for model details, extracting the context length.
+ * The context length is found in model_info under keys like
+ * "<family>.context_length" or "context_length".
+ */
+export async function showModel(ollamaUrl: string, model: string): Promise<ModelInfo> {
+ try {
+ const response = await requestUrl({
+ url: `${ollamaUrl}/api/show`,
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ model }),
+ });
+
+ const json = response.json as Record<string, unknown>;
+ let contextLength = 4096; // fallback default
+
+ const modelInfo = json.model_info as Record<string, unknown> | undefined;
+ if (modelInfo !== undefined && modelInfo !== null) {
+ // Look for context_length in model_info
+ // Keys are typically "<family>.context_length" e.g. "llama.context_length"
+ for (const key of Object.keys(modelInfo)) {
+ if (key.endsWith(".context_length") || key === "context_length") {
+ const val = modelInfo[key];
+ if (typeof val === "number" && val > 0) {
+ contextLength = val;
+ break;
+ }
+ }
+ }
+ }
+
+ return { contextLength };
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ throw new Error(`Failed to get model info: ${err.message}`);
+ }
+ throw new Error("Failed to get model info: unknown error.");
+ }
+}
+
+/**
* Send a chat message with optional tool-calling agent loop.
* When tools are provided, the function handles the multi-turn tool
* execution loop automatically and calls onToolCall for each invocation.
@@ -105,6 +153,7 @@ export async function sendChatMessage(
tools?: OllamaToolDefinition[],
app?: App,
onToolCall?: (event: ToolCallEvent) => void,
+ onApprovalRequest?: (event: ApprovalRequestEvent) => Promise<boolean>,
): Promise<string> {
const maxIterations = 10;
let iterations = 0;
@@ -180,6 +229,22 @@ export async function sendChatMessage(
let result: string;
if (toolEntry === undefined) {
result = `Error: Unknown tool "${fnName}".`;
+ } else if (toolEntry.requiresApproval) {
+ let approved = false;
+ if (onApprovalRequest !== undefined) {
+ const message = toolEntry.approvalMessage !== undefined
+ ? toolEntry.approvalMessage(fnArgs)
+ : `Allow ${toolEntry.friendlyName}?`;
+ approved = await onApprovalRequest({
+ toolName: fnName,
+ friendlyName: toolEntry.friendlyName,
+ message,
+ args: fnArgs,
+ });
+ }
+ result = approved
+ ? await toolEntry.execute(app, fnArgs)
+ : `Action declined by user: ${toolEntry.friendlyName} was not approved.`;
} else {
result = await toolEntry.execute(app, fnArgs);
}
@@ -211,6 +276,22 @@ export async function sendChatMessage(
}
/**
+ * Approval request event for tools that require user confirmation.
+ */
+export interface ApprovalRequestEvent {
+ toolName: string;
+ friendlyName: string;
+ message: string;
+ args: Record<string, unknown>;
+}
+
+export interface ModelOptions {
+ temperature?: number;
+ num_ctx?: number;
+ num_predict?: number;
+}
+
+/**
* Streaming chat options.
*/
export interface StreamingChatOptions {
@@ -219,8 +300,10 @@ export interface StreamingChatOptions {
messages: ChatMessage[];
tools?: OllamaToolDefinition[];
app?: App;
+ options?: ModelOptions;
onChunk: (text: string) => void;
onToolCall?: (event: ToolCallEvent) => void;
+ onApprovalRequest?: (event: ApprovalRequestEvent) => Promise<boolean>;
onCreateBubble: () => void;
abortSignal?: AbortSignal;
}
@@ -284,7 +367,7 @@ export async function sendChatMessageStreaming(
async function sendChatMessageStreamingMobile(
opts: StreamingChatOptions,
): Promise<string> {
- const { ollamaUrl, model, messages, tools, app, onChunk, onToolCall, onCreateBubble } = opts;
+ const { ollamaUrl, model, messages, tools, app, options, onChunk, onToolCall, onApprovalRequest, onCreateBubble } = opts;
const maxIterations = 10;
let iterations = 0;
@@ -317,6 +400,10 @@ async function sendChatMessageStreamingMobile(
body.tools = tools;
}
+ if (options !== undefined) {
+ body.options = options;
+ }
+
try {
const response = await requestUrl({
url: `${ollamaUrl}/api/chat`,
@@ -362,6 +449,22 @@ async function sendChatMessageStreamingMobile(
let result: string;
if (toolEntry === undefined) {
result = `Error: Unknown tool "${fnName}".`;
+ } else if (toolEntry.requiresApproval) {
+ let approved = false;
+ if (onApprovalRequest !== undefined) {
+ const message = toolEntry.approvalMessage !== undefined
+ ? toolEntry.approvalMessage(fnArgs)
+ : `Allow ${toolEntry.friendlyName}?`;
+ approved = await onApprovalRequest({
+ toolName: fnName,
+ friendlyName: toolEntry.friendlyName,
+ message,
+ args: fnArgs,
+ });
+ }
+ result = approved
+ ? await toolEntry.execute(app, fnArgs)
+ : `Action declined by user: ${toolEntry.friendlyName} was not approved.`;
} else {
result = await toolEntry.execute(app, fnArgs);
}
@@ -404,7 +507,7 @@ async function sendChatMessageStreamingMobile(
async function sendChatMessageStreamingDesktop(
opts: StreamingChatOptions,
): Promise<string> {
- const { ollamaUrl, model, messages, tools, app, onChunk, onToolCall, onCreateBubble, abortSignal } = opts;
+ const { ollamaUrl, model, messages, tools, app, options, onChunk, onToolCall, onApprovalRequest, onCreateBubble, abortSignal } = opts;
const maxIterations = 10;
let iterations = 0;
@@ -437,6 +540,10 @@ async function sendChatMessageStreamingDesktop(
body.tools = tools;
}
+ if (options !== undefined) {
+ body.options = options;
+ }
+
const response = await fetch(`${ollamaUrl}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -501,6 +608,22 @@ async function sendChatMessageStreamingDesktop(
let result: string;
if (toolEntry === undefined) {
result = `Error: Unknown tool "${fnName}".`;
+ } else if (toolEntry.requiresApproval) {
+ let approved = false;
+ if (onApprovalRequest !== undefined) {
+ const message = toolEntry.approvalMessage !== undefined
+ ? toolEntry.approvalMessage(fnArgs)
+ : `Allow ${toolEntry.friendlyName}?`;
+ approved = await onApprovalRequest({
+ toolName: fnName,
+ friendlyName: toolEntry.friendlyName,
+ message,
+ args: fnArgs,
+ });
+ }
+ result = approved
+ ? await toolEntry.execute(app, fnArgs)
+ : `Action declined by user: ${toolEntry.friendlyName} was not approved.`;
} else {
result = await toolEntry.execute(app, fnArgs);
}
diff --git a/src/settings-modal.ts b/src/settings-modal.ts
index 4fb089f..5a90649 100644
--- a/src/settings-modal.ts
+++ b/src/settings-modal.ts
@@ -1,8 +1,13 @@
import { Modal, Setting } from "obsidian";
import type AIOrganizer from "./main";
+import { showModel } from "./ollama-client";
+import type { ModelInfo } from "./ollama-client";
export class SettingsModal extends Modal {
private plugin: AIOrganizer;
+ private modelInfo: ModelInfo | null = null;
+ private ctxMaxEl: HTMLElement | null = null;
+ private ctxInputEl: HTMLInputElement | null = null;
constructor(plugin: AIOrganizer) {
super(plugin.app);
@@ -40,6 +45,7 @@ export class SettingsModal extends Modal {
dropdown.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
+ void this.fetchAndApplyModelInfo(value);
});
modelDropdownSelectEl = dropdown.selectEl;
});
@@ -66,12 +72,129 @@ export class SettingsModal extends Modal {
// Move connect above model in the DOM
contentEl.insertBefore(connectSetting.settingEl, modelSetting.settingEl);
+
+ // --- Generation Parameters ---
+
+ const paramHeader = contentEl.createEl("h4", { text: "Generation Parameters" });
+ paramHeader.style.marginTop = "16px";
+ paramHeader.style.marginBottom = "4px";
+
+ // Temperature
+ const tempSetting = new Setting(contentEl)
+ .setName("Temperature")
+ .setDesc("Controls randomness. Lower = more focused, higher = more creative.");
+
+ const tempValueEl = tempSetting.descEl.createSpan({
+ cls: "ai-organizer-param-value",
+ text: ` (${this.plugin.settings.temperature.toFixed(2)})`,
+ });
+
+ tempSetting.addSlider((slider) =>
+ slider
+ .setLimits(0, 2, 0.05)
+ .setValue(this.plugin.settings.temperature)
+ .setDynamicTooltip()
+ .onChange(async (value) => {
+ this.plugin.settings.temperature = value;
+ tempValueEl.setText(` (${value.toFixed(2)})`);
+ await this.plugin.saveSettings();
+ }),
+ );
+
+ // Context Window
+ const ctxSetting = new Setting(contentEl)
+ .setName("Context Window")
+ .setDesc("Max tokens the model sees (prompt + response).");
+
+ let ctxInputEl: HTMLInputElement | null = null;
+
+ ctxSetting.addText((text) => {
+ text.inputEl.type = "number";
+ text.inputEl.min = "256";
+ text.inputEl.max = "1048576";
+ text.inputEl.step = "256";
+ text.setValue(String(this.plugin.settings.numCtx));
+ text.onChange(async (value) => {
+ const num = parseInt(value, 10);
+ if (!isNaN(num) && num >= 256) {
+ this.plugin.settings.numCtx = num;
+ await this.plugin.saveSettings();
+ }
+ this.updateCtxMaxWarning();
+ });
+ text.inputEl.style.width = "80px";
+ ctxInputEl = text.inputEl;
+ });
+
+ // Model max label placed below the input
+ const ctxControlEl = ctxSetting.controlEl;
+ ctxControlEl.style.flexDirection = "column";
+ ctxControlEl.style.alignItems = "flex-end";
+ this.ctxMaxEl = ctxControlEl.createDiv({ cls: "ai-organizer-ctx-max" });
+ this.ctxMaxEl.style.cursor = "pointer";
+ this.ctxMaxEl.addEventListener("click", async () => {
+ if (this.modelInfo !== null && this.ctxInputEl !== null) {
+ this.plugin.settings.numCtx = this.modelInfo.contextLength;
+ this.ctxInputEl.value = String(this.modelInfo.contextLength);
+ await this.plugin.saveSettings();
+ this.updateCtxMaxWarning();
+ }
+ });
+ this.ctxInputEl = ctxInputEl;
+
+ // Max Output Tokens
+ const predictSetting = new Setting(contentEl)
+ .setName("Max Output Tokens")
+ .setDesc("Maximum tokens to generate. -1 = unlimited.");
+
+ predictSetting.addText((text) => {
+ text.inputEl.type = "number";
+ text.inputEl.min = "-1";
+ text.inputEl.max = "1048576";
+ text.inputEl.step = "256";
+ text.setValue(String(this.plugin.settings.numPredict));
+ text.onChange(async (value) => {
+ const num = parseInt(value, 10);
+ if (!isNaN(num) && num >= -1) {
+ this.plugin.settings.numPredict = num;
+ await this.plugin.saveSettings();
+ }
+ });
+ text.inputEl.style.width = "80px";
+ });
+
+ // Fetch model info if a model is already selected
+ if (this.plugin.settings.model !== "") {
+ void this.fetchAndApplyModelInfo(this.plugin.settings.model);
+ }
}
onClose(): void {
this.contentEl.empty();
}
+ private async fetchAndApplyModelInfo(model: string): Promise<void> {
+ if (model === "") return;
+ try {
+ this.modelInfo = await showModel(this.plugin.settings.ollamaUrl, model);
+ if (this.modelInfo !== null && this.ctxMaxEl !== null) {
+ this.ctxMaxEl.setText(`Model max: ${this.modelInfo.contextLength.toLocaleString()}`);
+ this.updateCtxMaxWarning();
+ }
+ } catch {
+ // Silently ignore — model info is optional enhancement
+ if (this.ctxMaxEl !== null) {
+ this.ctxMaxEl.setText("");
+ }
+ }
+ }
+
+ private updateCtxMaxWarning(): void {
+ if (this.ctxMaxEl === null || this.modelInfo === null) return;
+ const exceeds = this.plugin.settings.numCtx > this.modelInfo.contextLength;
+ this.ctxMaxEl.toggleClass("ai-organizer-ctx-max-warn", exceeds);
+ }
+
private populateModelDropdown(selectEl: HTMLSelectElement): void {
const models = this.plugin.availableModels;
diff --git a/src/settings.ts b/src/settings.ts
index 209ff98..ff61c89 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -4,10 +4,16 @@ export interface AIOrganizerSettings {
ollamaUrl: string;
model: string;
enabledTools: Record<string, boolean>;
+ temperature: number;
+ numCtx: number;
+ numPredict: number;
}
export const DEFAULT_SETTINGS: AIOrganizerSettings = {
ollamaUrl: "http://localhost:11434",
model: "",
enabledTools: getDefaultToolStates(),
+ temperature: 0.7,
+ numCtx: 4096,
+ numPredict: -1,
};
diff --git a/src/tools.ts b/src/tools.ts
index 7e31fb1..ca85091 100644
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -25,6 +25,8 @@ export interface ToolEntry {
label: string;
description: string;
friendlyName: string;
+ requiresApproval: boolean;
+ approvalMessage?: (args: Record<string, unknown>) => string;
summarize: (args: Record<string, unknown>) => string;
summarizeResult: (result: string) => string;
definition: OllamaToolDefinition;
@@ -89,6 +91,30 @@ async function executeReadFile(app: App, args: Record<string, unknown>): Promise
}
/**
+ * Execute the "delete_file" tool.
+ * Deletes a file by its vault path (moves to trash).
+ */
+async function executeDeleteFile(app: App, args: Record<string, unknown>): Promise<string> {
+ const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ if (filePath === "") {
+ return "Error: file_path parameter is required.";
+ }
+
+ const file = app.vault.getAbstractFileByPath(filePath);
+ if (file === null || !(file instanceof TFile)) {
+ return `Error: File not found at path "${filePath}".`;
+ }
+
+ try {
+ await app.vault.trash(file, true);
+ return `File "${filePath}" has been deleted (moved to system trash).`;
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : "Unknown error";
+ return `Error deleting file: ${msg}`;
+ }
+}
+
+/**
* All available tools for the plugin.
*/
export const TOOL_REGISTRY: ToolEntry[] = [
@@ -97,6 +123,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [
label: "Search File Names",
description: "Search for files in the vault by name or path.",
friendlyName: "Search Files",
+ requiresApproval: false,
summarize: (args) => {
const query = typeof args.query === "string" ? args.query : "";
return `"${query}"`;
@@ -135,6 +162,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [
label: "Read File Contents",
description: "Read the full text content of a file in the vault.",
friendlyName: "Read File",
+ requiresApproval: false,
summarize: (args) => {
const filePath = typeof args.file_path === "string" ? args.file_path : "";
return `"/${filePath}"`;
@@ -165,6 +193,48 @@ export const TOOL_REGISTRY: ToolEntry[] = [
},
execute: executeReadFile,
},
+ {
+ id: "delete_file",
+ label: "Delete File",
+ description: "Delete a file from the vault (requires approval).",
+ friendlyName: "Delete File",
+ requiresApproval: true,
+ approvalMessage: (args) => {
+ const filePath = typeof args.file_path === "string" ? args.file_path : "unknown";
+ return `Delete "${filePath}"?`;
+ },
+ summarize: (args) => {
+ const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ return `"/${filePath}"`;
+ },
+ summarizeResult: (result) => {
+ if (result.startsWith("Error")) {
+ return result;
+ }
+ if (result.includes("declined")) {
+ return "Declined by user";
+ }
+ return "File deleted";
+ },
+ definition: {
+ type: "function",
+ function: {
+ name: "delete_file",
+ description: "Delete a file from the Obsidian vault. The file is moved to the system trash. The file_path must be an exact path as returned by search_files. This action requires user approval.",
+ parameters: {
+ type: "object",
+ required: ["file_path"],
+ properties: {
+ file_path: {
+ type: "string",
+ description: "The vault-relative path to the file to delete (e.g. 'folder/note.md').",
+ },
+ },
+ },
+ },
+ },
+ execute: executeDeleteFile,
+ },
];
/**
diff --git a/styles.css b/styles.css
index 4aa810b..12a6305 100644
--- a/styles.css
+++ b/styles.css
@@ -17,7 +17,7 @@
flex: 1;
overflow-y: auto;
padding: 8px;
- padding-top: 52px;
+ padding-top: 56px;
display: flex;
flex-direction: column;
gap: 6px;
@@ -29,6 +29,8 @@
max-width: 85%;
word-wrap: break-word;
white-space: pre-wrap;
+ user-select: text;
+ -webkit-user-select: text;
}
.ai-organizer-message.user {
@@ -218,6 +220,8 @@
border-left: 3px solid var(--interactive-accent);
font-size: 0.85em;
margin: 2px 0;
+ user-select: text;
+ -webkit-user-select: text;
}
.ai-organizer-tool-call-header {
@@ -249,7 +253,7 @@
}
.ai-organizer-tool-call-result-summary {
- margin: 0 0 4px 0;
+ margin: 0 0 0 0;
color: var(--text-muted);
font-size: 0.9em;
}
@@ -265,7 +269,7 @@
grid-template-rows: max-content 0fr;
grid-template-columns: minmax(0, 1fr);
transition: grid-template-rows 0.2s ease-out;
- margin-top: 4px;
+ margin-top: 2px;
}
.ai-organizer-collapse-toggle {
@@ -281,7 +285,7 @@
grid-row-start: 1;
position: relative;
width: 100%;
- padding: 4px 28px 4px 0;
+ padding: 2px 28px 2px 0;
cursor: pointer;
color: var(--text-muted);
font-size: 0.9em;
@@ -332,7 +336,7 @@
}
.ai-organizer-collapse-content-inner {
- padding: 4px 0 6px 0;
+ padding: 2px 0 4px 0;
}
.ai-organizer-tool-call-args,
@@ -365,3 +369,122 @@
color: var(--text-on-accent) !important;
border-color: var(--text-error) !important;
}
+
+.ai-organizer-param-value {
+ font-weight: 600;
+ color: var(--text-normal);
+}
+
+.ai-organizer-ctx-max {
+ font-size: 0.8em;
+ color: var(--text-muted);
+ margin-top: 4px;
+ text-align: right;
+}
+
+.ai-organizer-ctx-max-warn {
+ color: var(--text-error);
+}
+
+/* ===== Tool Approval Prompt ===== */
+
+.ai-organizer-approval {
+ align-self: flex-start;
+ max-width: 85%;
+ padding: 8px 12px;
+ border-radius: 6px;
+ background-color: var(--background-secondary-alt);
+ border-left: 3px solid var(--text-warning);
+ margin: 2px 0;
+}
+
+.ai-organizer-approval-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 4px;
+ color: var(--text-warning);
+}
+
+.ai-organizer-approval-icon {
+ display: flex;
+ align-items: center;
+}
+
+.ai-organizer-approval-icon svg {
+ width: 16px;
+ height: 16px;
+}
+
+.ai-organizer-approval-name {
+ font-weight: 600;
+ font-size: 0.9em;
+}
+
+.ai-organizer-approval-message {
+ margin: 4px 0 8px 0;
+ color: var(--text-normal);
+ font-size: 0.9em;
+}
+
+.ai-organizer-approval-buttons {
+ display: flex;
+ gap: 8px;
+}
+
+.ai-organizer-approval-approve,
+.ai-organizer-approval-decline {
+ padding: 4px 16px;
+ border-radius: 4px;
+ border: none;
+ cursor: pointer;
+ font-size: 0.85em;
+ font-weight: 500;
+ transition: opacity 0.15s;
+}
+
+.ai-organizer-approval-approve {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+
+.ai-organizer-approval-approve:hover {
+ opacity: 0.85;
+}
+
+.ai-organizer-approval-decline {
+ background-color: var(--background-modifier-border);
+ color: var(--text-normal);
+}
+
+.ai-organizer-approval-decline:hover {
+ opacity: 0.85;
+}
+
+.ai-organizer-approval-approve:disabled,
+.ai-organizer-approval-decline:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
+.ai-organizer-approval-status {
+ margin-top: 6px;
+ font-size: 0.8em;
+ font-weight: 600;
+}
+
+.ai-organizer-approval-approved .ai-organizer-approval-status {
+ color: var(--interactive-accent);
+}
+
+.ai-organizer-approval-declined .ai-organizer-approval-status {
+ color: var(--text-error);
+}
+
+.ai-organizer-approval-approved {
+ border-left-color: var(--interactive-accent);
+}
+
+.ai-organizer-approval-declined {
+ border-left-color: var(--text-error);
+}