Add ability for users to provide suggestions during diff review through new DiffControls component. Integrate suggestion workflow into chat input, allowing users to approve/reject changes or provide feedback without interrupting the review process.

Improve component lifecycle management by converting DiffService and StatusBarService to extend Component class, ensuring proper cleanup and event handling. Add automatic service cleanup in DependencyService during deregistration.

Refine error handling in AI function responses to return error messages instead of Error objects for better serialization. Update user rejection messaging to provide clearer guidance to AI about stopping actions.
This commit is contained in:
Andrew Beal 2025-11-27 12:39:08 +00:00
parent 2d5a1b52bf
commit c69351404b
15 changed files with 365 additions and 4057 deletions

View file

@ -12,7 +12,7 @@ export class AIFunctionCall {
this.toolId = toolId;
}
public toConversationString(): string {
public toConversationString() {
return JSON.stringify({
functionCall: {
name: this.name,

View file

@ -1,15 +1,19 @@
<script lang="ts">
import { tick } from "svelte";
import { Platform, setIcon } from "obsidian";
import { onDestroy, tick } from "svelte";
import { Platform, setIcon, type EventRef } from "obsidian";
import type { UserInputService } from "Services/UserInputService";
import type { ISearchState, SearchStateStore } from "Stores/SearchStateStore";
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import { SearchTrigger, isSearchTrigger, isSearchTriggerElement, fromInput, toNode, triggerToText } from "Enums/SearchTrigger";
import { isSearchTrigger, isSearchTriggerElement, fromInput, toNode, triggerToText } from "Enums/SearchTrigger";
import ChatSearchResults from "./ChatSearchResults.svelte";
import type { Writable } from "svelte/store";
import type { InputService } from "Services/InputService";
import UserInstruction from "./UserInstruction.svelte";
import DiffControls from "./DiffControls.svelte";
import type { EventService } from "Services/EventService";
import { Event } from "Enums/Event";
import type { DiffService } from "Services/DiffService";
export let hasNoApiKey: boolean;
export let isSubmitting: boolean;
@ -21,6 +25,8 @@
const inputService: InputService = Resolve<InputService>(Services.InputService);
const userInputService: UserInputService = Resolve<UserInputService>(Services.UserInputService);
const searchStateStore: SearchStateStore = Resolve<SearchStateStore>(Services.SearchStateStore);
const diffService: DiffService = Resolve<DiffService>(Services.DiffService);
const eventService: EventService = Resolve<EventService>(Services.EventService);
const searchState: Writable<ISearchState> = searchStateStore.searchState;
@ -32,6 +38,15 @@
let userInstructionActive = false;
let userRequest = "";
let diffOpen: boolean = false;
const diffOpenedRef: EventRef = eventService.on(Event.DiffOpened, () => { diffOpen = true; focusInput(); });
const diffClosedRef: EventRef = eventService.on(Event.DiffClosed, () => { diffOpen = false; focusInput(); });
onDestroy(() => {
eventService.offref(diffOpenedRef);
eventService.offref(diffClosedRef);
});
export function focusInput() {
tick().then(() => {
textareaElement?.focus();
@ -43,7 +58,7 @@
}
$: if (submitButton) {
setIcon(submitButton, isSubmitting ? "square" : "send-horizontal");
setIcon(submitButton, diffOpen || !isSubmitting ? "send-horizontal" : "square");
}
$: if (editModeButton) {
@ -58,7 +73,19 @@
if (userRequest.trim() === "" || isSubmitting) {
return;
}
const result = requestFromInput();
onsubmit(result.request, result.formattedRequest);
}
function handleSuggestion() {
if (userRequest.trim() === "" || !diffOpen) {
return;
}
const suggestion = requestFromInput().formattedRequest;
diffService.onSuggest(suggestion);
}
function requestFromInput() {
const request = textareaElement.innerHTML;
const formattedRequest = triggerToText(request);
@ -71,7 +98,7 @@
focusInput();
}
onsubmit(request, formattedRequest);
return { request: request, formattedRequest: formattedRequest };
}
function toggleEditMode() {
@ -110,7 +137,7 @@
return;
}
e.preventDefault();
handleSubmit();
diffOpen ? handleSuggestion() : handleSubmit();
}
}
@ -268,6 +295,10 @@
</script>
<div id="input-container" class:edit-mode={editModeActive}>
<div id="diff-controls-container" style:padding-top={diffOpen ? "var(--size-4-2)" : 0} style:display={diffOpen ? "inline" : "none"}>
<DiffControls/>
</div>
<div id="input-search-results-container" style:padding-top={$searchState.results.length > 0 ? "var(--size-4-2)" : 0}>
<ChatSearchResults searchState={$searchState} onResultAccept={handleSearchResultAcceptance}/>
</div>
@ -299,7 +330,7 @@
on:click={handleCursorPositionChange}
on:keyup={handleCursorPositionChange}
on:focusout={handleFocusOut}
data-placeholder="Type a message..."
data-placeholder={diffOpen ? "Make a suggestion..." : "Type a message..."}
role="textbox"
aria-multiline="true"
tabindex="0">
@ -318,7 +349,7 @@
id="submit-button"
class:edit-mode={editModeActive}
bind:this={submitButton}
on:click={() => { isSubmitting ? handleStop() : handleSubmit() }}
on:click={() => { diffOpen ? handleSuggestion() : isSubmitting ? handleStop() : handleSubmit() }}
disabled={!isSubmitting && userRequest.trim() === ""}
aria-label={isSubmitting ? "Cancel" : "Send Message"}>
</button>
@ -329,7 +360,7 @@
grid-row: 2;
grid-column: 1;
display: grid;
grid-template-rows: auto var(--size-4-3) 1fr var(--size-4-3);
grid-template-rows: auto auto var(--size-4-3) 1fr var(--size-4-3);
grid-template-columns: var(--size-4-3) auto var(--size-4-2) 1fr var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
border-radius: var(--modal-radius);
background-color: var(--background-primary);
@ -340,18 +371,23 @@
transition: border-color 0.5s ease-out;
}
#input-search-results-container {
#diff-controls-container {
grid-row: 1;
grid-column: 2 / 9;
}
#input-search-results-container {
grid-row: 2;
grid-column: 2 / 9;
}
#user-instruction-container {
grid-row: 1;
grid-row: 2;
grid-column: 2 / 9;
}
#user-instruction-button {
grid-row: 3;
grid-row: 4;
grid-column: 2;
border-radius: var(--button-radius);
align-self: end;
@ -367,7 +403,7 @@
}
#input-field {
grid-row: 3;
grid-row: 4;
grid-column: 4;
height: 100%;
max-height: 30vh;
@ -428,7 +464,7 @@
}
#edit-mode-button {
grid-row: 3;
grid-row: 4;
grid-column: 6;
border-radius: var(--button-radius);
align-self: end;
@ -440,7 +476,7 @@
}
#submit-button {
grid-row: 3;
grid-row: 4;
grid-column: 8;
border-radius: var(--button-radius);
padding-left: var(--size-4-5);

View file

@ -0,0 +1,71 @@
<script lang="ts">
import { Resolve } from "Services/DependencyService";
import type { DiffService } from "Services/DiffService";
import { Services } from "Services/Services";
const diffService: DiffService = Resolve<DiffService>(Services.DiffService);
</script>
<div id="diff-controls">
<button
id="diff-accept"
class="diff-button"
aria-label="Accept"
on:click={() => diffService.onAccept()}>
Accept
</button>
<button
id="diff-reject"
class="diff-button"
aria-label="Reject"
on:click={() => diffService.onReject()}>
Reject
</button>
</div>
<style>
#diff-controls {
display: grid;
grid-template-columns: 1fr var(--size-4-2) 1fr;
grid-template-rows: auto;
}
#diff-accept {
grid-column: 1;
background-color: color-mix(
in srgb,
var(--color-green) 75%,
var(--background-primary) 25%
);
}
#diff-accept:hover {
background-color: var(--color-green);
}
#diff-accept:focus {
background-color: var(--color-green);
}
#diff-reject {
grid-column: 3;
background-color: color-mix(
in srgb,
var(--color-red) 75%,
var(--background-primary) 25%
);
}
#diff-reject:hover {
background-color: var(--color-red);
}
#diff-reject:focus {
background-color: var(--color-red);
}
.diff-button {
border-radius: var(--button-radius);
transition-duration: 0.5s;
}
</style>

View file

@ -26,6 +26,7 @@
};
function startNewConversation() {
conversationFileSystemService.resetCurrentConversation();
conversationStore.reset();
onNewConversation?.();

View file

@ -134,7 +134,7 @@ export class AIFunctionService {
filePaths.map(async (filePath) => {
const result = await this.fileSystemService.readFile(filePath);
if (result instanceof Error) {
return { path: filePath, error: result }
return { path: filePath, error: result.message }
}
return { path: filePath, contents: result }
})
@ -145,7 +145,7 @@ export class AIFunctionService {
private async writeVaultFile(filePath: string, content: string): Promise<object> {
const result = await this.fileSystemService.writeFile(normalizePath(filePath), content);
if (result instanceof Error) {
return { success: false, error: result };
return { success: false, error: result.message };
}
return { success: true };
}
@ -158,7 +158,7 @@ export class AIFunctionService {
const results = await Promise.all(filePaths.map(async filePath => {
const result = await this.fileSystemService.deleteFile(filePath);
if (result instanceof Error) {
return { path: filePath, success: false, error: result }
return { path: filePath, success: false, error: result.message }
}
return { path: filePath, success: true };
}));
@ -175,7 +175,7 @@ export class AIFunctionService {
const destinationPath = destinationPaths[index];
const result = await this.fileSystemService.moveFile(sourcePath, destinationPath);
if (result instanceof Error) {
return { path: destinationPath, success: false, error: result }
return { path: destinationPath, success: false, error: result.message }
}
return { path: destinationPath, success: true };
}));

View file

@ -13,6 +13,8 @@ import { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
import { Notice } from "obsidian";
import type { EventService } from "./EventService";
import { Event } from "Enums/Event";
export interface IChatServiceCallbacks {
onSubmit: () => void;
@ -29,6 +31,7 @@ export class ChatService {
private tokenService: ITokenService | undefined;
private prompt: IPrompt;
private statusBarService: StatusBarService;
private eventService: EventService;
private semaphore: Semaphore;
private semaphoreHeld: boolean = false;
@ -40,6 +43,7 @@ export class ChatService {
this.namingService = Resolve<ConversationNamingService>(Services.ConversationNamingService);
this.prompt = Resolve<IPrompt>(Services.IPrompt);
this.statusBarService = Resolve<StatusBarService>(Services.StatusBarService);
this.eventService = Resolve<EventService>(Services.EventService);
this.semaphore = new Semaphore(1, false);
}
@ -96,6 +100,7 @@ export class ChatService {
response = await this.streamRequestResponse(conversation, allowDestructiveActions, callbacks);
}
} finally {
this.eventService.trigger(Event.DiffClosed);
await this.saveConversation(conversation);
this.abortController = null;
if (this.semaphoreHeld) {

View file

@ -1,4 +1,5 @@
import { Exception } from "Helpers/Exception";
import type { Component } from "obsidian";
const services = new Map<symbol, unknown>();
@ -16,15 +17,20 @@ export function Resolve<T>(type: symbol): T {
Exception.throw(`Service not found for type: ${type.description}`);
}
if (typeof service === 'function') {
// It's a transient factory, return a new instance
if (typeof service === "function") {
// It"s a transient factory, return a new instance
return (service as () => T)();
}
// It's a singleton, return the existing instance
// It"s a singleton, return the existing instance
return service as T;
}
export function DeregisterAllServices() {
services.forEach((service) => {
if (service && typeof service === "object" && "unload" in service) {
(service as Component).unload();
}
});
services.clear();
}

View file

@ -6,23 +6,32 @@ import type { EventService } from './EventService';
import { Event } from 'Enums/Event';
import type { Diff2HtmlUIConfig } from 'diff2html/lib/ui/js/diff2html-ui';
import { ColorSchemeType, OutputFormatType } from 'diff2html/lib/types';
import { Platform } from 'obsidian';
import { Component, Platform } from 'obsidian';
interface DiffResult {
accepted: boolean;
suggestion?: string;
}
export class DiffService {
export class DiffService extends Component {
private readonly plugin: VaultkeeperAIPlugin;
private readonly eventService: EventService;
private diffResolve?: (result: DiffResult) => void;
private ongoingDiff: boolean = false;
public constructor() {
super();
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
this.eventService = Resolve<EventService>(Services.EventService);
this.registerEvent(this.eventService.on(Event.DiffClosed, () => {
if (this.ongoingDiff) {
this.onReject();
}
}));
}
public async requestDiff(oldFileName: string, newFileName: string, oldContent: string, newContent: string): Promise<DiffResult> {
@ -41,6 +50,8 @@ export class DiffService {
colorScheme: ColorSchemeType.AUTO
};
this.ongoingDiff = true;
return new Promise((resolve) => {
this.diffResolve = resolve;
@ -75,8 +86,9 @@ export class DiffService {
}
private finishDiff() {
this.ongoingDiff = false;
this.diffResolve = undefined;
this.eventService.trigger(Event.DiffClosed);
this.eventService.trigger(Event.DiffClosed);
}
}

View file

@ -1,8 +1,9 @@
import type VaultkeeperAIPlugin from "main";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import { Component } from "obsidian";
export class StatusBarService {
export class StatusBarService extends Component {
private readonly plugin: VaultkeeperAIPlugin;
private statusBarItem: HTMLElement | null;
@ -11,9 +12,14 @@ export class StatusBarService {
private animationFrame: number | null = null;
public constructor() {
super();
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
}
override onunload(): void {
this.removeStatusBarMessage();
}
public setStatusBarMessage(message: string) {
if (this.statusBarItem == null) {
this.createStatusBarMessage();

View file

@ -426,7 +426,7 @@ export class VaultService {
return await performChange();
}
let response = "User rejected this change"; // maybe need an event to abort an ongoing function call exchange?
let response = "User rejected this change. Stop all actions and consult with the user";
if (result.suggestion) {
response = `User has rejected the input with the following suggestion: ${result.suggestion}`;
}

View file

@ -1,400 +1,214 @@
/**
* Diff2html Obsidian Theme Override
*
* This stylesheet overrides diff2html's default styling to use Obsidian's CSS variables
*
*/
/* Override diff2html blue tones with Obsidian's CSS variables */
/* ========================================
ROOT VARIABLE OVERRIDES - LIGHT THEME
======================================== */
:root {
/* Background colors */
--d2h-bg-color: var(--background-primary);
--d2h-file-header-bg-color: var(--background-secondary);
--d2h-file-diff-wrapper-bg-color: var(--background-primary);
--d2h-file-list-wrapper-bg-color: var(--background-secondary);
--d2h-file-list-header-bg-color: var(--background-secondary-alt);
/* Border colors */
--d2h-border-color: var(--background-modifier-border);
--d2h-file-border-color: var(--background-modifier-border);
--d2h-line-border-color: var(--background-modifier-border);
/* Text colors */
--d2h-text-color: var(--text-normal);
--d2h-muted-text-color: var(--text-muted);
--d2h-file-name-color: var(--text-normal);
--d2h-file-stats-color: var(--text-muted);
--d2h-line-number-color: var(--text-muted);
/* Insertion colors (additions) */
--d2h-ins-bg-color: color-mix(in srgb, var(--color-green) 15%, var(--background-primary));
--d2h-ins-label-color: var(--color-green);
--d2h-ins-highlight-bg-color: color-mix(in srgb, var(--color-green) 30%, var(--background-primary));
--d2h-ins-border-color: color-mix(in srgb, var(--color-green) 40%, var(--background-modifier-border));
/* Deletion colors (removals) */
--d2h-del-bg-color: color-mix(in srgb, var(--color-red) 15%, var(--background-primary));
--d2h-del-label-color: var(--color-red);
--d2h-del-highlight-bg-color: color-mix(in srgb, var(--color-red) 30%, var(--background-primary));
--d2h-del-border-color: color-mix(in srgb, var(--color-red) 40%, var(--background-modifier-border));
/* Change/modification colors */
--d2h-change-bg-color: color-mix(in srgb, var(--color-yellow) 15%, var(--background-primary));
--d2h-change-label-color: var(--color-yellow);
/* Info colors */
--d2h-info-bg-color: color-mix(in srgb, var(--color-cyan) 10%, var(--background-secondary));
--d2h-info-text-color: var(--text-muted);
/* Link colors */
--d2h-link-color: var(--interactive-accent);
--d2h-link-hover-color: var(--interactive-accent-hover);
/* Code colors */
--d2h-code-bg-color: var(--background-primary-alt);
--d2h-code-text-color: var(--text-normal);
/* Selection and hover */
--d2h-hover-bg-color: var(--background-modifier-hover);
--d2h-selected-bg-color: var(--background-modifier-active-hover);
}
/* ========================================
DARK THEME OVERRIDES
======================================== */
/* Dark theme with explicit class */
.d2h-dark-color-scheme,
.theme-dark {
/* Background colors */
--d2h-dark-bg-color: var(--background-primary);
--d2h-dark-file-header-bg-color: var(--background-secondary);
--d2h-dark-file-diff-wrapper-bg-color: var(--background-primary);
--d2h-dark-file-list-wrapper-bg-color: var(--background-secondary);
--d2h-dark-file-list-header-bg-color: var(--background-secondary-alt);
/* Border colors */
--d2h-dark-border-color: var(--background-modifier-border);
--d2h-dark-file-border-color: var(--background-modifier-border);
--d2h-dark-line-border-color: var(--background-modifier-border);
/* Text colors */
--d2h-dark-text-color: var(--text-normal);
--d2h-dark-muted-text-color: var(--text-muted);
--d2h-dark-file-name-color: var(--text-normal);
--d2h-dark-file-stats-color: var(--text-muted);
--d2h-dark-line-number-color: var(--text-muted);
/* Insertion colors (additions) */
--d2h-dark-ins-bg-color: color-mix(in srgb, var(--color-green) 15%, var(--background-primary));
--d2h-dark-ins-label-color: color-mix(in srgb, var(--color-green) 120%, white);
--d2h-dark-ins-highlight-bg-color: color-mix(in srgb, var(--color-green) 30%, var(--background-primary));
--d2h-dark-ins-border-color: color-mix(in srgb, var(--color-green) 50%, var(--background-modifier-border));
/* Deletion colors (removals) */
--d2h-dark-del-bg-color: color-mix(in srgb, var(--color-red) 20%, var(--background-primary));
--d2h-dark-del-label-color: color-mix(in srgb, var(--color-red) 120%, white);
--d2h-dark-del-highlight-bg-color: color-mix(in srgb, var(--color-red) 35%, var(--background-primary));
--d2h-dark-del-border-color: color-mix(in srgb, var(--color-red) 50%, var(--background-modifier-border));
/* Change/modification colors */
--d2h-dark-change-bg-color: color-mix(in srgb, var(--color-yellow) 20%, var(--background-primary));
--d2h-dark-change-label-color: color-mix(in srgb, var(--color-yellow) 120%, white);
/* Info colors */
--d2h-dark-info-bg-color: color-mix(in srgb, var(--color-cyan) 15%, var(--background-secondary));
--d2h-dark-info-text-color: var(--text-muted);
/* Link colors */
--d2h-dark-link-color: var(--interactive-accent);
--d2h-dark-link-hover-color: var(--interactive-accent-hover);
/* Code colors */
--d2h-dark-code-bg-color: var(--background-primary-alt);
--d2h-dark-code-text-color: var(--text-normal);
/* Selection and hover */
--d2h-dark-hover-bg-color: var(--background-modifier-hover);
--d2h-dark-selected-bg-color: var(--background-modifier-active-hover);
}
/* Auto theme - follows system preference */
@media (prefers-color-scheme: dark) {
.d2h-auto-color-scheme {
/* Apply dark theme variables */
--d2h-bg-color: var(--d2h-dark-bg-color);
--d2h-file-header-bg-color: var(--d2h-dark-file-header-bg-color);
--d2h-border-color: var(--d2h-dark-border-color);
--d2h-text-color: var(--d2h-dark-text-color);
--d2h-ins-bg-color: var(--d2h-dark-ins-bg-color);
--d2h-ins-label-color: var(--d2h-dark-ins-label-color);
--d2h-ins-highlight-bg-color: var(--d2h-dark-ins-highlight-bg-color);
--d2h-del-bg-color: var(--d2h-dark-del-bg-color);
--d2h-del-label-color: var(--d2h-dark-del-label-color);
--d2h-del-highlight-bg-color: var(--d2h-dark-del-highlight-bg-color);
}
}
/* ========================================
COMPONENT-SPECIFIC OVERRIDES
======================================== */
/* File wrapper */
/* Main diff container background */
.d2h-wrapper {
background-color: var(--background-primary);
color: var(--text-normal);
font-family: var(--font-interface);
font-size: var(--font-ui-small);
background-color: var(--background-primary) !important;
box-shadow: 0px 0px 4px 1px var(--color-accent) !important;
}
/* File header */
/* File header background */
.d2h-file-header {
background-color: var(--background-secondary);
border-color: var(--background-modifier-border);
color: var(--text-normal);
background-color: var(--background-primary) !important;
border-color: var(--background-modifier-border) !important;
}
.d2h-file-name {
color: var(--text-normal);
font-family: var(--font-monospace);
/* File name container */
.d2h-file-name-wrapper {
background-color: var(--background-primary-alt) !important;
}
.d2h-file-stats {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
/* Line number backgrounds - only override context lines */
.d2h-code-linenumber.d2h-cntx {
background-color: var(--background-primary-alt) !important;
border-color: var(--background-modifier-border) !important;
}
/* Code lines */
.d2h-code-line {
color: var(--text-normal);
font-family: var(--font-monospace);
font-size: var(--font-ui-small);
/* Code line backgrounds - only override context lines */
.d2h-code-line.d2h-cntx {
background-color: var(--background-primary) !important;
}
.d2h-code-line-ctn {
color: var(--text-normal);
}
.d2h-code-line-prefix {
color: var(--text-muted);
}
/* Line numbers */
.d2h-code-linenumber {
background-color: var(--background-secondary-alt);
color: var(--text-muted);
border-color: var(--background-modifier-border);
}
/* Insertion (added) lines */
.d2h-ins {
background-color: var(--d2h-ins-bg-color);
}
.d2h-ins .d2h-code-line-prefix {
color: var(--d2h-ins-label-color);
}
.d2h-ins .d2h-code-line-ctn {
background-color: transparent;
}
.d2h-ins.d2h-change {
background-color: var(--d2h-change-bg-color);
}
/* Deletion (removed) lines */
.d2h-del {
background-color: var(--d2h-del-bg-color);
}
.d2h-del .d2h-code-line-prefix {
color: var(--d2h-del-label-color);
}
.d2h-del .d2h-code-line-ctn {
background-color: transparent;
}
.d2h-del.d2h-change {
background-color: var(--d2h-change-bg-color);
}
/* Word-level highlighting */
.d2h-ins .d2h-change-highlight {
background-color: var(--d2h-ins-highlight-bg-color);
border-radius: 2px;
padding: 0 2px;
}
.d2h-del .d2h-change-highlight {
background-color: var(--d2h-del-highlight-bg-color);
border-radius: 2px;
padding: 0 2px;
}
/* Context (unchanged) lines */
.d2h-cxt {
background-color: var(--background-primary);
color: var(--text-normal);
}
/* Info lines */
/* Info section background */
.d2h-info {
background-color: var(--d2h-info-bg-color);
color: var(--d2h-info-text-color);
border-color: var(--background-modifier-border);
background-color: var(--background-secondary) !important;
border-color: var(--background-modifier-border) !important;
}
/* File list */
.d2h-file-list-wrapper {
background-color: var(--background-secondary);
border-color: var(--background-modifier-border);
}
.d2h-file-list-header {
background-color: var(--background-secondary-alt);
border-color: var(--background-modifier-border);
color: var(--text-normal);
}
.d2h-file-list-line {
border-color: var(--background-modifier-border);
}
.d2h-file-list-line:hover {
background-color: var(--background-modifier-hover);
}
/* Links */
.d2h-file-name-link,
.d2h-file-link {
color: var(--interactive-accent);
text-decoration: none;
}
.d2h-file-name-link:hover,
.d2h-file-link:hover {
color: var(--interactive-accent-hover);
text-decoration: underline;
}
/* Tag/label styling */
.d2h-tag {
background-color: var(--background-modifier-border);
color: var(--text-normal);
border-radius: var(--radius-s);
padding: 2px 6px;
font-size: var(--font-ui-smaller);
}
/* Diff table */
.d2h-diff-table {
border-color: var(--background-modifier-border);
background-color: var(--background-primary);
/* File diff background */
.d2h-file-diff {
background-color: var(--background-primary) !important;
border-color: var(--background-modifier-border) !important;
}
/* Empty placeholder */
.d2h-empty-placeholder {
color: var(--text-faint);
background-color: var(--background-secondary);
.d2h-emptyplaceholder {
background-color: var(--background-secondary-alt) !important;
}
/* ========================================
OBSIDIAN-SPECIFIC ENHANCEMENTS
======================================== */
/* Integrate with Obsidian's scrollbar styling */
.d2h-wrapper::-webkit-scrollbar {
width: var(--scrollbar-width);
/* Code side backgrounds for side-by-side view - only override context lines */
.d2h-code-side-linenumber.d2h-cntx,
.d2h-code-side-line.d2h-cntx {
background-color: var(--background-primary) !important;
}
.d2h-wrapper::-webkit-scrollbar-track {
background: var(--background-primary);
/* Fix transparent backgrounds on insertion/deletion line numbers */
/* Dark mode styles - side-by-side view */
.theme-dark .d2h-code-side-linenumber.d2h-ins {
background-color: #1e3a1e !important; /* Dark green - solid color */
}
.d2h-wrapper::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-bg);
border-radius: var(--radius-s);
.theme-dark .d2h-code-side-linenumber.d2h-del {
background-color: #3a1e1e !important; /* Dark red - solid color */
}
.d2h-wrapper::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-active-thumb-bg);
/* Dark mode styles - inline view */
.theme-dark .d2h-code-linenumber.d2h-ins {
background-color: #1e3a1e !important; /* Dark green - solid color */
}
/* Match Obsidian's border radius */
.d2h-file-wrapper {
border-radius: var(--radius-m);
.theme-dark .d2h-code-linenumber.d2h-del {
background-color: #3a1e1e !important; /* Dark red - solid color */
}
/* Light mode styles - side-by-side view */
.theme-light .d2h-code-side-linenumber.d2h-ins {
background-color: #c8e6c9 !important; /* Light green with better contrast */
}
.theme-light .d2h-code-side-linenumber.d2h-del {
background-color: #ffcdd2 !important; /* Light red with better contrast */
}
/* Light mode styles - inline view */
.theme-light .d2h-code-linenumber.d2h-ins {
background-color: #c8e6c9 !important; /* Light green with better contrast */
}
.theme-light .d2h-code-linenumber.d2h-del {
background-color: #ffcdd2 !important; /* Light red with better contrast */
}
/* Tags and labels */
.d2h-tag {
background-color: var(--background-secondary) !important;
border-color: var(--background-modifier-border) !important;
}
/* File list wrapper */
.d2h-file-list-wrapper {
background-color: var(--background-primary) !important;
}
/* File list header */
.d2h-file-list-header {
background-color: var(--background-primary-alt) !important;
border-color: var(--background-modifier-border) !important;
}
/* File list line */
.d2h-file-list-line {
background-color: var(--background-primary) !important;
border-color: var(--background-modifier-border) !important;
}
/* File switch container */
.d2h-file-switch {
background-color: var(--background-primary-alt) !important;
}
/* Toolbar */
.d2h-file-diff-toolbar {
background-color: var(--background-secondary) !important;
}
.d2h-file-header {
border-top-left-radius: var(--radius-m);
border-top-right-radius: var(--radius-m);
padding: 0 !important;
}
/* Ensure proper contrast in Obsidian's theme */
.theme-light .d2h-code-line,
.theme-light .d2h-code-linenumber {
border-color: var(--background-modifier-border);
.d2h-file-name-wrapper {
padding-left: 8px !important;
}
.theme-dark .d2h-code-line,
.theme-dark .d2h-code-linenumber {
border-color: var(--background-modifier-border);
/* Text colors */
.d2h-file-header,
.d2h-file-name,
.d2h-file-stats,
.d2h-code-linenumber,
.d2h-info {
color: var(--text-normal) !important;
}
/* Responsive adjustments for mobile */
.is-mobile .d2h-wrapper {
font-size: var(--font-ui-smaller);
/* Light mode syntax highlighting overrides for better visibility */
.theme-light .d2h-code-side-line {
color: var(--text-normal) !important;
}
.is-mobile .d2h-code-line {
font-size: var(--font-ui-smaller);
padding: var(--size-2-1) var(--size-2-2);
/* Force base text color for all code content in light mode */
.theme-light .d2h-code-line-ctn,
.theme-light .d2h-code-line-ctn * {
color: var(--text-normal) !important;
}
/* ========================================
ACCESSIBILITY IMPROVEMENTS
======================================== */
/* Ensure focus states are visible */
.d2h-file-list-line:focus,
.d2h-file-name-link:focus {
outline: 2px solid var(--interactive-accent);
outline-offset: 2px;
/* Override Monokai theme colors in light mode for better contrast */
.theme-light .hljs-comment,
.theme-light .hljs-quote {
color: #5c6370 !important; /* Medium gray for comments */
}
/* Improve contrast for line numbers */
.d2h-code-linenumber {
opacity: 0.7;
.theme-light .hljs-keyword,
.theme-light .hljs-selector-tag,
.theme-light .hljs-literal,
.theme-light .hljs-type {
color: #a626a4 !important; /* Purple for keywords */
}
.d2h-code-line:hover .d2h-code-linenumber {
opacity: 1;
.theme-light .hljs-string,
.theme-light .hljs-doctag {
color: #50a14f !important; /* Green for strings */
}
/* ========================================
PRINT STYLES
======================================== */
.theme-light .hljs-title,
.theme-light .hljs-section,
.theme-light .hljs-selector-id {
color: #4078f2 !important; /* Blue for titles/functions */
}
@media print {
.d2h-wrapper {
background: white;
color: black;
}
.d2h-ins {
background-color: #d4ffd4 !important;
}
.d2h-del {
background-color: #ffd4d4 !important;
}
.d2h-code-linenumber {
background-color: #f5f5f5 !important;
}
}
.theme-light .hljs-subst,
.theme-light .hljs-tag,
.theme-light .hljs-name,
.theme-light .hljs-attr,
.theme-light .hljs-attribute {
color: #e45649 !important; /* Red for tags/attributes */
}
.theme-light .hljs-variable,
.theme-light .hljs-template-variable {
color: #986801 !important; /* Orange for variables */
}
.theme-light .hljs-number,
.theme-light .hljs-built_in,
.theme-light .hljs-builtin-name,
.theme-light .hljs-class .hljs-title {
color: #c18401 !important; /* Gold for numbers/built-ins */
}
.theme-light .hljs-meta,
.theme-light .hljs-meta-keyword {
color: #a626a4 !important; /* Purple for meta */
}
.theme-light .hljs-symbol,
.theme-light .hljs-bullet,
.theme-light .hljs-link {
color: #0184bc !important; /* Cyan for symbols */
}
/* Ensure deletion and insertion text is visible */
.theme-light .d2h-code-line.d2h-del .hljs {
color: #24292e !important; /* Dark text on light red background */
}
.theme-light .d2h-code-line.d2h-ins .hljs {
color: #24292e !important; /* Dark text on light green background */
}

View file

@ -1,5 +1,9 @@
import { Diff2HtmlUI, type Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui";
import { Event } from "Enums/Event";
import { ItemView, WorkspaceLeaf, type ViewStateResult } from "obsidian";
import { Resolve } from "Services/DependencyService";
import type { EventService } from "Services/EventService";
import { Services } from "Services/Services";
export const VIEW_TYPE_DIFF = 'vaultkeeper-ai-diff-view';
@ -9,7 +13,9 @@ interface DiffViewState {
}
export class DiffView extends ItemView {
private readonly eventService: EventService;
private diffString: string = "";
private config: Diff2HtmlUIConfig = {};
@ -17,6 +23,18 @@ export class DiffView extends ItemView {
constructor(leaf: WorkspaceLeaf) {
super(leaf);
this.eventService = Resolve<EventService>(Services.EventService);
this.registerEvent(this.eventService.on(Event.DiffClosed, () => {
this.leaf.detach();
}));
}
protected override onClose(): Promise<void> {
// trigger DiffClosed event in case the user closed the tab
this.eventService.trigger(Event.DiffClosed);
return Promise.resolve();
}
public getViewType(): string {
@ -44,6 +62,15 @@ export class DiffView extends ItemView {
}
private renderDiff() {
const container = this.resetContainer();
this.diffContainer = container.createDiv({ cls: 'd2h-wrapper' });
const diff2htmlUi = new Diff2HtmlUI(this.diffContainer, this.diffString, this.config);
diff2htmlUi.draw();
}
private resetContainer(): HTMLElement {
const container = this.contentEl;
container.empty();
@ -51,11 +78,7 @@ export class DiffView extends ItemView {
this.diffContainer.remove();
this.diffContainer = null
}
this.diffContainer = container.createDiv({ cls: 'd2h-wrapper' });
const diff2htmlUi = new Diff2HtmlUI(this.diffContainer, this.diffString, this.config);
diff2htmlUi.draw();
return container;
}
}

View file

@ -36,8 +36,7 @@ export class MainView extends ItemView {
return 'sparkles';
}
// eslint-disable-next-line @typescript-eslint/require-await -- mount operations are valid but synchronous
public async onOpen() {
protected override onOpen(): Promise<void> {
const container = this.contentEl;
container.empty();
@ -58,9 +57,11 @@ export class MainView extends ItemView {
target: container,
props: {}
}) as ChatWindowComponent;
return Promise.resolve();
}
public async onClose() {
public override async onClose(): Promise<void> {
if (this.topBar) {
await unmount(this.topBar);
}

View file

@ -4,7 +4,6 @@ import { RegisterDependencies, RegisterPlugin } from "Services/ServiceRegistrati
import { VaultkeeperAISettingTab } from "VaultkeeperAISettingTab";
import { DiffView, VIEW_TYPE_DIFF } from "Views/DiffView";
import { Services } from "Services/Services";
import type { StatusBarService } from "Services/StatusBarService";
import { DeregisterAllServices, Resolve } from "Services/DependencyService";
import type { VaultService } from "Services/VaultService";
import { Path } from "Enums/Path";
@ -13,7 +12,7 @@ import type { SettingsService } from "Services/SettingsService";
import type { Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui";
import "katex/dist/katex.min.css";
import 'highlight.js/styles/github.min.css';
import 'highlight.js/styles/monokai.min.css';
import 'diff2html/bundles/css/diff2html.min.css';
import 'diff2html/bundles/js/diff2html-ui.min.js';
@ -52,7 +51,6 @@ export default class VaultkeeperAIPlugin extends Plugin {
}
public onunload() {
Resolve<StatusBarService>(Services.StatusBarService).removeStatusBarMessage();
DeregisterAllServices();
}

3665
styles.css

File diff suppressed because one or more lines are too long