mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
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:
parent
33440d17bf
commit
3c9b5f9b73
4 changed files with 129 additions and 4 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue