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
This commit is contained in:
Andrew Beal 2025-12-06 16:38:01 +00:00
parent 33440d17bf
commit 3c9b5f9b73
4 changed files with 129 additions and 4 deletions

View file

@ -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;

View file

@ -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) {

View file

@ -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);
}

View file

@ -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<EventService>(Services.EventService);
this.diffService = Resolve<DiffService>(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<void> {
const { workspace } = this.app;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN);
if (leaves.length > 0) {
await workspace.revealLeaf(leaves[0]);
}
}
}