From 3c9b5f9b7399ce740321de81573c6a912876a292 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 6 Dec 2025 16:38:01 +0000 Subject: [PATCH] feat: add mobile support for diff view with accept/reject buttons - Add mobile-specific controls with Accept/Reject buttons for diff review - Centralize user rejection/suggestion messages in AIFunctionResponse - Update VaultService to use centralized message constants - Style mobile buttons with proper touch targets and theme colors - Auto-focus main view after accepting/rejecting changes on mobile --- .../FunctionDefinitions/AIFunctionResponse.ts | 7 +++ Services/VaultService.ts | 5 +- Styles/diff2html_styles.css | 61 +++++++++++++++++++ Views/DiffView.ts | 60 +++++++++++++++++- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/AIClasses/FunctionDefinitions/AIFunctionResponse.ts b/AIClasses/FunctionDefinitions/AIFunctionResponse.ts index acc92bc..1a0fb94 100644 --- a/AIClasses/FunctionDefinitions/AIFunctionResponse.ts +++ b/AIClasses/FunctionDefinitions/AIFunctionResponse.ts @@ -5,6 +5,13 @@ export class AIFunctionResponse { public readonly response: object; public readonly toolId?: string; + public static readonly UserRejectionMessage: string = `The user has explicitly rejected this change. + They may have changed their mind about the requested change. + + **CRITICAL:** Immediately stop all further actions and consult with the user`; + + public static readonly UserSuggestionMessage: string = "The user has rejected the change with the following suggestion: "; + constructor(name: string, response: object, toolId?: string) { this.name = name; this.response = response; diff --git a/Services/VaultService.ts b/Services/VaultService.ts index ade7ffe..3ff06cd 100644 --- a/Services/VaultService.ts +++ b/Services/VaultService.ts @@ -15,6 +15,7 @@ import { DiffService } from "./DiffService"; import * as path from "path-browserify"; import { Event } from "Enums/Event"; import { AbortService } from "./AbortService"; +import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse"; interface IFileEventArgs { oldPath: string; @@ -457,9 +458,9 @@ export class VaultService { return await performChange(); } - let response = "User rejected this change. Stop all actions and consult with the user"; + let response = AIFunctionResponse.UserRejectionMessage; if (result.suggestion) { - response = `User has rejected the change with the following suggestion: ${result.suggestion}`; + response = AIFunctionResponse.UserSuggestionMessage + result.suggestion; } return Exception.new(response); } catch (error) { diff --git a/Styles/diff2html_styles.css b/Styles/diff2html_styles.css index 6adb8b0..52744c0 100644 --- a/Styles/diff2html_styles.css +++ b/Styles/diff2html_styles.css @@ -231,3 +231,64 @@ .theme-light .d2h-code-line.d2h-ins .hljs { color: #24292e !important; /* Dark text on light green background */ } + +/* ============================== */ +/* Mobile Controls */ +/* ============================== */ + +.diff-mobile-controls { + display: none; /* Hidden by default (desktop) */ +} + +.diff-view-mobile .diff-mobile-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--size-4-2); + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: var(--size-4-3); + background-color: var(--background-primary); + border-top: 1px solid var(--background-modifier-border); + z-index: 10; +} + +.diff-mobile-button { + min-height: 44px; + font-size: var(--font-ui-medium); + font-weight: var(--font-semibold); + border-radius: var(--button-radius); + cursor: pointer; + border: none; + color: var(--text-on-accent); +} + +.diff-mobile-accept { + background-color: color-mix( + in srgb, + var(--color-green) 75%, + var(--background-primary) 25% + ) !important; +} + +.diff-mobile-accept:active { + background-color: var(--color-green) !important; +} + +.diff-mobile-reject { + background-color: color-mix( + in srgb, + var(--color-red) 75%, + var(--background-primary) 25% + ) !important; +} + +.diff-mobile-reject:active { + background-color: var(--color-red) !important; +} + +/* Adjust diff height on mobile to account for buttons */ +.diff-view-mobile .d2h-file-diff { + max-height: calc(100cqh - 37px - 40px); +} diff --git a/Views/DiffView.ts b/Views/DiffView.ts index c4377d1..05b7349 100644 --- a/Views/DiffView.ts +++ b/Views/DiffView.ts @@ -1,9 +1,11 @@ import { Diff2HtmlUI, type Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui"; import { Event } from "Enums/Event"; -import { ItemView, WorkspaceLeaf, type ViewStateResult } from "obsidian"; +import { ItemView, WorkspaceLeaf, Platform, type ViewStateResult } from "obsidian"; import { Resolve } from "Services/DependencyService"; import type { EventService } from "Services/EventService"; +import type { DiffService } from "Services/DiffService"; import { Services } from "Services/Services"; +import { VIEW_TYPE_MAIN } from "./MainView"; export const VIEW_TYPE_DIFF = 'vaultkeeper-ai-diff-view'; @@ -15,16 +17,19 @@ interface DiffViewState { export class DiffView extends ItemView { private readonly eventService: EventService; + private readonly diffService: DiffService; private diffString: string = ""; private config: Diff2HtmlUIConfig = {}; private diffContainer: HTMLElement | null = null; + private mobileButtonsContainer: HTMLElement | null = null; constructor(leaf: WorkspaceLeaf) { super(leaf); this.eventService = Resolve(Services.EventService); + this.diffService = Resolve(Services.DiffService); this.registerEvent(this.eventService.on(Event.DiffClosed, () => { this.leaf.detach(); @@ -64,12 +69,20 @@ export class DiffView extends ItemView { private renderDiff() { const container = this.resetContainer(); + if (Platform.isMobile) { + container.addClass('diff-view-mobile'); + } + this.diffContainer = container.createDiv({ cls: 'd2h-wrapper' }); const diff2htmlUi = new Diff2HtmlUI(this.diffContainer, this.diffString, this.config); diff2htmlUi.draw(); diff2htmlUi.highlightCode(); + if (Platform.isMobile) { + this.mobileButtonsContainer = this.createMobileButtons(); + } + requestAnimationFrame(() => { const firstChange = this.diffContainer?.querySelector('.d2h-change, .d2h-ins, .d2h-del'); firstChange?.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -82,9 +95,52 @@ export class DiffView extends ItemView { if (this.diffContainer) { this.diffContainer.remove(); - this.diffContainer = null + this.diffContainer = null; } + + if (this.mobileButtonsContainer) { + this.mobileButtonsContainer.remove(); + this.mobileButtonsContainer = null; + } + return container; } + private createMobileButtons(): HTMLElement { + const container = this.contentEl.createDiv({ cls: 'diff-mobile-controls' }); + + const acceptButton = container.createEl('button', { + cls: 'diff-mobile-button diff-mobile-accept', + text: 'Accept' + }); + acceptButton.setAttribute('aria-label', 'Accept changes'); + + const rejectButton = container.createEl('button', { + cls: 'diff-mobile-button diff-mobile-reject', + text: 'Reject' + }); + rejectButton.setAttribute('aria-label', 'Reject changes'); + + this.registerDomEvent(acceptButton, 'click', async () => { + this.diffService.onAccept(); + await this.refocusMainView(); + }); + + this.registerDomEvent(rejectButton, 'click', async () => { + this.diffService.onReject(); + await this.refocusMainView(); + }); + + return container; + } + + private async refocusMainView(): Promise { + const { workspace } = this.app; + const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN); + + if (leaves.length > 0) { + await workspace.revealLeaf(leaves[0]); + } + } + } \ No newline at end of file