mirror of
https://github.com/meganjjzhang/Promptuary.git
synced 2026-07-22 07:49:22 +00:00
Improve agent terminal reuse and diff refresh
This commit is contained in:
parent
6aabbc1116
commit
1d8ea77fa4
8 changed files with 502 additions and 81 deletions
174
main.js
174
main.js
File diff suppressed because one or more lines are too long
|
|
@ -2,9 +2,11 @@ import { App, Notice } from "obsidian";
|
|||
import { isMobile } from "../utils/platform";
|
||||
import { copyToClipboard } from "../export/Exporters";
|
||||
import { t } from "../i18n/i18n";
|
||||
import { execSync, fs, os, nodePath } from "../utils/nodeApi";
|
||||
import { execFileSync, fs, os, nodePath, nodeCrypto } from "../utils/nodeApi";
|
||||
import { shellEscape } from "../utils/shellescape";
|
||||
|
||||
export type TerminalApp = "Terminal" | "iTerm2";
|
||||
const AGENT_TERMINAL_TITLE_PREFIX = "Promptuary Agent";
|
||||
|
||||
export interface LaunchOptions {
|
||||
command: string;
|
||||
|
|
@ -49,23 +51,21 @@ export function launchInTerminal(opts: LaunchOptions): void {
|
|||
|
||||
function launchMacOS(opts: LaunchOptions): void {
|
||||
// Write the command to a temp shell script to avoid all AppleScript quoting issues.
|
||||
// The script path is safe ASCII — no special chars to escape.
|
||||
// The script path is safe ASCII - no special chars to escape.
|
||||
const tmpScript = nodePath.join(os.tmpdir(), `prm-agent-${Date.now()}.sh`);
|
||||
fs.writeFileSync(tmpScript, `#!/bin/bash\n${opts.command}\n`, { mode: 0o755 });
|
||||
|
||||
// Schedule cleanup after 60 s (well after execution starts)
|
||||
window.setTimeout(() => { try { fs.unlinkSync(tmpScript); } catch { /* ignore */ } }, 60_000);
|
||||
|
||||
let appleScript: string;
|
||||
|
||||
if (opts.terminalApp === "iTerm2") {
|
||||
appleScript = `tell application "iTerm"\nactivate\ncreate window with default profile command "${tmpScript}"\nend tell`;
|
||||
} else {
|
||||
appleScript = `tell application "Terminal"\ndo script "${tmpScript}"\nactivate\nend tell`;
|
||||
}
|
||||
const terminalCommand = buildTerminalSessionCommand(tmpScript, opts.vaultPath);
|
||||
const appleScript = opts.terminalApp === "iTerm2"
|
||||
? buildITermAppleScript(terminalCommand, opts.vaultPath)
|
||||
: buildTerminalAppleScript(terminalCommand, opts.vaultPath);
|
||||
|
||||
try {
|
||||
execSync(`osascript -e '${appleScript}'`, {
|
||||
execFileSync("osascript", [], {
|
||||
input: appleScript,
|
||||
encoding: "utf-8",
|
||||
timeout: 10_000,
|
||||
stdio: "pipe",
|
||||
|
|
@ -79,15 +79,184 @@ function launchMacOS(opts: LaunchOptions): void {
|
|||
}
|
||||
}
|
||||
|
||||
function buildTerminalAppleScript(command: string, vaultPath: string): string {
|
||||
const commandText = escapeAppleScriptString(command);
|
||||
const sessionTitle = escapeAppleScriptString(buildAgentTerminalTitle(vaultPath));
|
||||
const targetCwd = escapeAppleScriptString(normalizeVaultPath(vaultPath));
|
||||
|
||||
return `
|
||||
${buildCwdLookupAppleScriptHandler()}
|
||||
|
||||
tell application "Terminal"
|
||||
activate
|
||||
set targetTab to missing value
|
||||
set targetCwd to "${targetCwd}"
|
||||
|
||||
repeat with terminalWindow in windows
|
||||
repeat with terminalTab in tabs of terminalWindow
|
||||
if (custom title of terminalTab is "${sessionTitle}") and (busy of terminalTab is false) then
|
||||
set targetTab to terminalTab
|
||||
set index of terminalWindow to 1
|
||||
set selected of terminalTab to true
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
if targetTab is not missing value then exit repeat
|
||||
end repeat
|
||||
|
||||
if targetTab is missing value then
|
||||
repeat with terminalWindow in windows
|
||||
repeat with terminalTab in tabs of terminalWindow
|
||||
if busy of terminalTab is false then
|
||||
set tabCwd to my promptuaryCwdForTty(tty of terminalTab)
|
||||
if tabCwd is targetCwd then
|
||||
set targetTab to terminalTab
|
||||
set index of terminalWindow to 1
|
||||
set selected of terminalTab to true
|
||||
exit repeat
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
if targetTab is not missing value then exit repeat
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if targetTab is missing value then
|
||||
set targetTab to do script "${commandText}"
|
||||
set custom title of targetTab to "${sessionTitle}"
|
||||
else
|
||||
do script "${commandText}" in targetTab
|
||||
set custom title of targetTab to "${sessionTitle}"
|
||||
end if
|
||||
end tell
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function buildITermAppleScript(command: string, vaultPath: string): string {
|
||||
const commandText = escapeAppleScriptString(command);
|
||||
const sessionTitle = escapeAppleScriptString(buildAgentTerminalTitle(vaultPath));
|
||||
const targetCwd = escapeAppleScriptString(normalizeVaultPath(vaultPath));
|
||||
|
||||
return `
|
||||
${buildCwdLookupAppleScriptHandler()}
|
||||
|
||||
tell application "iTerm"
|
||||
activate
|
||||
set targetSession to missing value
|
||||
set targetCwd to "${targetCwd}"
|
||||
|
||||
repeat with terminalWindow in windows
|
||||
repeat with terminalTab in tabs of terminalWindow
|
||||
repeat with terminalSession in sessions of terminalTab
|
||||
if (name of terminalSession is "${sessionTitle}") and (is processing of terminalSession is false) then
|
||||
set targetSession to terminalSession
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
if targetSession is not missing value then exit repeat
|
||||
end repeat
|
||||
if targetSession is not missing value then exit repeat
|
||||
end repeat
|
||||
|
||||
if targetSession is missing value then
|
||||
repeat with terminalWindow in windows
|
||||
repeat with terminalTab in tabs of terminalWindow
|
||||
repeat with terminalSession in sessions of terminalTab
|
||||
if (is processing of terminalSession is false) then
|
||||
set sessionCwd to my promptuaryCwdForTty(tty of terminalSession)
|
||||
if sessionCwd is targetCwd then
|
||||
set targetSession to terminalSession
|
||||
exit repeat
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
if targetSession is not missing value then exit repeat
|
||||
end repeat
|
||||
if targetSession is not missing value then exit repeat
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if targetSession is missing value then
|
||||
if (count of windows) is 0 then
|
||||
set targetWindow to (create window with default profile)
|
||||
else
|
||||
set targetWindow to current window
|
||||
tell targetWindow
|
||||
create tab with default profile
|
||||
end tell
|
||||
end if
|
||||
set targetSession to current session of targetWindow
|
||||
end if
|
||||
|
||||
tell targetSession
|
||||
set name to "${sessionTitle}"
|
||||
write text "${commandText}"
|
||||
end tell
|
||||
end tell
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function buildTerminalSessionCommand(tmpScript: string, vaultPath: string): string {
|
||||
return `cd ${shellEscape(normalizeVaultPath(vaultPath))} && ${shellEscape(tmpScript)}`;
|
||||
}
|
||||
|
||||
function escapeAppleScriptString(value: string): string {
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
||||
}
|
||||
|
||||
function buildCwdLookupAppleScriptHandler(): string {
|
||||
return `
|
||||
on promptuaryCwdForTty(tabTty)
|
||||
if tabTty is missing value then return ""
|
||||
set ttyText to tabTty as text
|
||||
if ttyText is "" then return ""
|
||||
if ttyText starts with "/dev/" then set ttyText to text 6 thru -1 of ttyText
|
||||
set lookupCmd to "tty=" & quoted form of ttyText & "; pid=$(/bin/ps -t \\"$tty\\" -o pid= 2>/dev/null | /usr/bin/head -n 1 | /usr/bin/tr -d ' '); if [ -n \\"$pid\\" ]; then /usr/sbin/lsof -a -p \\"$pid\\" -d cwd -Fn 2>/dev/null | /usr/bin/sed -n 's/^n//p' | /usr/bin/head -n 1; fi"
|
||||
try
|
||||
return do shell script lookupCmd
|
||||
on error
|
||||
return ""
|
||||
end try
|
||||
end promptuaryCwdForTty
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function buildAgentTerminalTitle(vaultPath: string): string {
|
||||
const normalizedPath = normalizeVaultPath(vaultPath);
|
||||
const vaultName = sanitizeTitlePart(nodePath.basename(normalizedPath) || "Vault");
|
||||
const pathHash = nodeCrypto.createHash("sha256")
|
||||
.update(normalizedPath)
|
||||
.digest("hex")
|
||||
.slice(0, 12);
|
||||
return `${AGENT_TERMINAL_TITLE_PREFIX} - ${vaultName} - ${pathHash}`;
|
||||
}
|
||||
|
||||
function normalizeVaultPath(vaultPath: string): string {
|
||||
let resolved = nodePath.resolve(vaultPath);
|
||||
try {
|
||||
resolved = fs.realpathSync.native(resolved);
|
||||
} catch {
|
||||
// Keep the resolved path when the directory cannot be realpathed.
|
||||
}
|
||||
const parsed = nodePath.parse(resolved);
|
||||
return resolved === parsed.root ? resolved : resolved.replace(/[\\/]+$/, "");
|
||||
}
|
||||
|
||||
function sanitizeTitlePart(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim() || "Vault";
|
||||
}
|
||||
|
||||
// ---------- file change monitor ----------
|
||||
|
||||
export type ChangeStatus = "idle" | "running" | "detected" | "timeout";
|
||||
|
||||
export class FileChangeMonitor {
|
||||
private status: ChangeStatus = "idle";
|
||||
private timer: number | null = null;
|
||||
private timeoutTimer: number | null = null;
|
||||
private settleTimer: number | null = null;
|
||||
private resolve: ((detected: boolean) => void) | null = null;
|
||||
private watchedAbsPath: string | null = null;
|
||||
private unwatch: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Start monitoring a file for changes made by an external process.
|
||||
|
|
@ -96,12 +265,13 @@ export class FileChangeMonitor {
|
|||
* because vault events are only fired for writes Obsidian itself initiates —
|
||||
* external CLI Agents write directly to disk and vault events are unreliable.
|
||||
*
|
||||
* Resolves `true` when mtime changes, `false` on timeout.
|
||||
* Resolves `true` after a detected change stays quiet briefly, `false` on timeout.
|
||||
*/
|
||||
startMonitor(
|
||||
app: App,
|
||||
filePath: string,
|
||||
timeoutMs = 5 * 60 * 1000,
|
||||
settleMs = 1500,
|
||||
): Promise<boolean> {
|
||||
this.status = "running";
|
||||
|
||||
|
|
@ -112,29 +282,35 @@ export class FileChangeMonitor {
|
|||
return new Promise<boolean>((resolve) => {
|
||||
this.resolve = resolve;
|
||||
|
||||
// Timeout guard
|
||||
this.timer = window.setTimeout(() => {
|
||||
const finish = (detected: boolean, status: ChangeStatus) => {
|
||||
if (this.status !== "running") return;
|
||||
this.cleanup();
|
||||
this.status = "timeout";
|
||||
resolve(false);
|
||||
this.status = status;
|
||||
this.resolve = null;
|
||||
resolve(detected);
|
||||
};
|
||||
|
||||
this.timeoutTimer = window.setTimeout(() => {
|
||||
finish(false, "timeout");
|
||||
}, timeoutMs);
|
||||
|
||||
// Poll every 1 s using fs.watchFile
|
||||
const listener = (curr: import("fs").Stats, prev: import("fs").Stats) => {
|
||||
if (curr.mtimeMs !== prev.mtimeMs && this.status === "running") {
|
||||
this.cleanup();
|
||||
this.status = "detected";
|
||||
resolve(true);
|
||||
}
|
||||
if (this.status !== "running") return;
|
||||
if (curr.mtimeMs === prev.mtimeMs && curr.size === prev.size) return;
|
||||
|
||||
if (this.settleTimer) window.clearTimeout(this.settleTimer);
|
||||
this.settleTimer = window.setTimeout(() => {
|
||||
finish(true, "detected");
|
||||
}, settleMs);
|
||||
};
|
||||
|
||||
fs.watchFile(absPath, { persistent: false, interval: 1000 }, listener);
|
||||
|
||||
// Override cleanup to also stop the watcher
|
||||
const origCleanup: () => void = this.cleanup;
|
||||
this.cleanup = () => {
|
||||
origCleanup();
|
||||
try { fs.unwatchFile(absPath, listener); } catch { /* ignore */ }
|
||||
this.unwatch = () => {
|
||||
try {
|
||||
fs.unwatchFile(absPath, listener);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -142,9 +318,11 @@ export class FileChangeMonitor {
|
|||
/** Cancel an in-progress monitor */
|
||||
cancel(): void {
|
||||
if (this.status === "running") {
|
||||
const resolve = this.resolve;
|
||||
this.cleanup();
|
||||
this.status = "idle";
|
||||
this.resolve?.(false);
|
||||
this.resolve = null;
|
||||
resolve?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,10 +331,17 @@ export class FileChangeMonitor {
|
|||
}
|
||||
|
||||
private cleanup = (): void => {
|
||||
if (this.timer) {
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
if (this.timeoutTimer) {
|
||||
window.clearTimeout(this.timeoutTimer);
|
||||
this.timeoutTimer = null;
|
||||
}
|
||||
if (this.settleTimer) {
|
||||
window.clearTimeout(this.settleTimer);
|
||||
this.settleTimer = null;
|
||||
}
|
||||
if (this.unwatch) {
|
||||
this.unwatch();
|
||||
this.unwatch = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { App, Modal, setIcon } from "obsidian";
|
||||
import { App, Modal, Notice, setIcon } from "obsidian";
|
||||
import {
|
||||
DiffBlock,
|
||||
computeDiff,
|
||||
|
|
@ -12,6 +12,10 @@ export interface DiffModalResult {
|
|||
mergedText?: string;
|
||||
}
|
||||
|
||||
interface DiffModalOptions {
|
||||
reloadModifiedText?: () => Promise<string>;
|
||||
}
|
||||
|
||||
interface Hunk {
|
||||
header: string;
|
||||
blocks: DiffBlock[];
|
||||
|
|
@ -57,8 +61,8 @@ function groupIntoHunks(blocks: DiffBlock[], ctxLines = 2): Hunk[] {
|
|||
}
|
||||
|
||||
export class DiffModal extends Modal {
|
||||
private blocks: DiffBlock[];
|
||||
private hunks: Hunk[];
|
||||
private blocks: DiffBlock[] = [];
|
||||
private hunks: Hunk[] = [];
|
||||
/** true = accepted, false = rejected, undefined = pending */
|
||||
private hunkDecisions: Map<number, boolean> = new Map();
|
||||
private acceptMap: Map<number, boolean> = new Map();
|
||||
|
|
@ -70,11 +74,18 @@ export class DiffModal extends Modal {
|
|||
private originalText: string,
|
||||
private modifiedText: string,
|
||||
private fileName: string,
|
||||
private options: DiffModalOptions = {},
|
||||
) {
|
||||
super(app);
|
||||
this.blocks = computeDiff(originalText, modifiedText);
|
||||
this.hunks = groupIntoHunks(this.blocks);
|
||||
// Default: all changes accepted
|
||||
this.resetDiff(modifiedText);
|
||||
}
|
||||
|
||||
private resetDiff(modifiedText: string): void {
|
||||
this.modifiedText = modifiedText;
|
||||
this.blocks = computeDiff(this.originalText, modifiedText);
|
||||
this.hunks = groupIntoHunks(this.blocks).filter((hunk) => hunk.changeIndices.length > 0);
|
||||
this.acceptMap.clear();
|
||||
this.hunkDecisions.clear();
|
||||
for (const b of this.blocks) {
|
||||
if (b.type !== "unchanged") this.acceptMap.set(b.index, true);
|
||||
}
|
||||
|
|
@ -110,7 +121,19 @@ export class DiffModal extends Modal {
|
|||
const hunkBadge = headerRight.createSpan({ cls: "prm-dm-hunk-badge" });
|
||||
hunkBadge.setText(`${this.hunks.length} ${t("diff.hunks")}`);
|
||||
|
||||
if (this.options.reloadModifiedText) {
|
||||
const refreshBtn = headerRight.createEl("button", { cls: "prm-dm-refresh" });
|
||||
refreshBtn.title = t("diff.btn.refresh");
|
||||
refreshBtn.setAttribute("aria-label", t("diff.btn.refresh"));
|
||||
setIcon(refreshBtn, "refresh-cw");
|
||||
refreshBtn.createSpan({ text: t("diff.btn.refresh") });
|
||||
refreshBtn.onclick = () => {
|
||||
void this.refreshDiff(refreshBtn);
|
||||
};
|
||||
}
|
||||
|
||||
const closeBtn = headerRight.createEl("button", { cls: "prm-dm-close" });
|
||||
closeBtn.title = t("common.close");
|
||||
setIcon(closeBtn, "x");
|
||||
closeBtn.onclick = () => {
|
||||
this.result = { action: "reject" };
|
||||
|
|
@ -150,15 +173,14 @@ export class DiffModal extends Modal {
|
|||
setIcon(applyBtn, "check");
|
||||
applyBtn.createSpan({ text: t("diff.btn.applySelected") });
|
||||
applyBtn.onclick = () => {
|
||||
this.result = this.buildResultFromAcceptMap();
|
||||
this.close();
|
||||
void this.applySelected();
|
||||
};
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
if (this.resolve) {
|
||||
if (!this.result) {
|
||||
this.result = this.buildResultFromAcceptMap();
|
||||
this.result = { action: "reject" };
|
||||
}
|
||||
this.resolve(this.result);
|
||||
this.resolve = null;
|
||||
|
|
@ -168,6 +190,9 @@ export class DiffModal extends Modal {
|
|||
private buildResultFromAcceptMap(): DiffModalResult {
|
||||
// Build the result from current hunk-level decisions.
|
||||
// Default map value is true, so untouched hunks are accepted unless rejected.
|
||||
if (this.acceptMap.size === 0) {
|
||||
return { action: "reject" };
|
||||
}
|
||||
const decisions = [...this.acceptMap.values()];
|
||||
const allAccepted = decisions.every(v => v);
|
||||
const allRejected = decisions.every(v => !v);
|
||||
|
|
@ -175,12 +200,52 @@ export class DiffModal extends Modal {
|
|||
return { action: "reject" };
|
||||
}
|
||||
if (allAccepted) {
|
||||
return { action: "accept-all" };
|
||||
return { action: "accept-all", mergedText: this.modifiedText };
|
||||
}
|
||||
const merged = applyPartialDiff(this.originalText, this.blocks, this.acceptMap);
|
||||
return { action: "accept-partial", mergedText: merged };
|
||||
}
|
||||
|
||||
private async refreshDiff(refreshBtn: HTMLButtonElement): Promise<void> {
|
||||
if (!this.options.reloadModifiedText) return;
|
||||
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.addClass("is-loading");
|
||||
|
||||
try {
|
||||
const modifiedText = await this.options.reloadModifiedText();
|
||||
this.result = null;
|
||||
this.resetDiff(modifiedText);
|
||||
this.onOpen();
|
||||
new Notice(t("diff.notice.refreshed"));
|
||||
} catch {
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.removeClass("is-loading");
|
||||
new Notice(t("diff.notice.refreshFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async applySelected(): Promise<void> {
|
||||
if (this.options.reloadModifiedText) {
|
||||
try {
|
||||
const latestModifiedText = await this.options.reloadModifiedText();
|
||||
if (latestModifiedText !== this.modifiedText) {
|
||||
this.result = null;
|
||||
this.resetDiff(latestModifiedText);
|
||||
this.onOpen();
|
||||
new Notice(t("diff.notice.changedBeforeApply"));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
new Notice(t("diff.notice.refreshFailed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.result = this.buildResultFromAcceptMap();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private renderHunk(parent: HTMLElement, hunk: Hunk, idx: number): void {
|
||||
const wrap = parent.createDiv({ cls: "prm-dm-hunk" });
|
||||
|
||||
|
|
|
|||
|
|
@ -305,6 +305,10 @@ const en: Record<string, string> = {
|
|||
"diff.btn.applySelected": "Apply selected",
|
||||
"diff.btn.reject": "Reject",
|
||||
"diff.btn.accept": "Accept",
|
||||
"diff.btn.refresh": "Refresh",
|
||||
"diff.notice.refreshed": "Diff refreshed",
|
||||
"diff.notice.refreshFailed": "Failed to refresh diff",
|
||||
"diff.notice.changedBeforeApply": "File changed again. Diff refreshed, please review again.",
|
||||
"diff.hunks": "changes",
|
||||
|
||||
// ==================== api ====================
|
||||
|
|
|
|||
|
|
@ -322,6 +322,10 @@ const zhCN: Record<string, string> = {
|
|||
"diff.btn.applySelected": "应用选中",
|
||||
"diff.btn.reject": "拒绝",
|
||||
"diff.btn.accept": "接受",
|
||||
"diff.btn.refresh": "刷新",
|
||||
"diff.notice.refreshed": "已刷新 Diff",
|
||||
"diff.notice.refreshFailed": "刷新 Diff 失败",
|
||||
"diff.notice.changedBeforeApply": "文件有新变化,已刷新 Diff,请重新确认",
|
||||
"diff.hunks": "个变更",
|
||||
|
||||
// ==================== api ====================
|
||||
|
|
|
|||
10
src/main.ts
10
src/main.ts
|
|
@ -396,8 +396,9 @@ export default class PromptuaryPlugin extends Plugin {
|
|||
|
||||
switch (diffResult.action) {
|
||||
case "accept-all": {
|
||||
await this.app.vault.modify(file, modifiedText);
|
||||
await this.reanchorAndConfirm(targetPath, originalText, modifiedText);
|
||||
const finalText = diffResult.mergedText ?? modifiedText;
|
||||
await this.app.vault.modify(file, finalText);
|
||||
await this.reanchorAndConfirm(targetPath, originalText, finalText);
|
||||
new Notice(t("main.notice.allAccepted"));
|
||||
break;
|
||||
}
|
||||
|
|
@ -665,11 +666,14 @@ export default class PromptuaryPlugin extends Plugin {
|
|||
original,
|
||||
modified,
|
||||
fileName,
|
||||
{
|
||||
reloadModifiedText: () => this.app.vault.read(file),
|
||||
},
|
||||
).openForResult();
|
||||
|
||||
switch (result.action) {
|
||||
case "accept-all": {
|
||||
const finalText = modified;
|
||||
const finalText = result.mergedText ?? modified;
|
||||
await this.reanchorAndConfirm(filePath, original, finalText);
|
||||
this.originalTextBeforeAgent = null;
|
||||
new Notice(t("main.notice.allAccepted"));
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports -- window.require is the only way to access Node.js builtins in Obsidian's renderer process
|
||||
const nodeRequire = (window as unknown as { require?: NodeRequire }).require ?? require;
|
||||
|
||||
export const { execSync, exec } = nodeRequire("child_process") as typeof import("child_process");
|
||||
export const { execSync, exec, execFileSync } = nodeRequire("child_process") as typeof import("child_process");
|
||||
export const fs = nodeRequire("fs") as typeof import("fs");
|
||||
export const os = nodeRequire("os") as typeof import("os");
|
||||
export const nodePath = nodeRequire("path") as typeof import("path");
|
||||
export const nodeCrypto = nodeRequire("crypto") as typeof import("crypto");
|
||||
|
|
|
|||
52
styles.css
52
styles.css
|
|
@ -1540,6 +1540,40 @@
|
|||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.prm-dm-refresh {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-monospace);
|
||||
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||
}
|
||||
.prm-dm-refresh:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.prm-dm-refresh:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.prm-dm-refresh svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.prm-dm-refresh.is-loading svg {
|
||||
animation: prm-dm-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes prm-dm-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.prm-dm-close {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 6px;
|
||||
|
|
@ -1572,6 +1606,7 @@
|
|||
border-radius: 8px; overflow: hidden;
|
||||
background: var(--background-primary);
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.prm-dm-hunk.prm-dm-hunk-decided {
|
||||
opacity: 0;
|
||||
|
|
@ -1580,9 +1615,11 @@
|
|||
}
|
||||
.prm-dm-hunk-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--background-secondary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.prm-dm-hunk-header-left { display: flex; align-items: center; gap: 8px; }
|
||||
.prm-dm-hunk-pos {
|
||||
|
|
@ -1642,7 +1679,8 @@
|
|||
.prm-dm-lines {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px; line-height: 1.7;
|
||||
max-height: min(52vh, 520px);
|
||||
max-height: min(46vh, 460px);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
|
@ -1654,6 +1692,7 @@
|
|||
.prm-dm-line {
|
||||
display: flex; align-items: baseline;
|
||||
padding: 0 12px;
|
||||
min-height: 19px;
|
||||
}
|
||||
.prm-dm-line-unchanged {
|
||||
color: var(--text-normal);
|
||||
|
|
@ -1694,6 +1733,17 @@
|
|||
}
|
||||
.prm-dm-text { white-space: pre-wrap; word-break: break-all; flex: 1; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.prm-dm-refresh span { display: none; }
|
||||
.prm-dm-hunk-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.prm-dm-hunk-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.prm-dm-footer {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
|
|
|
|||
Loading…
Reference in a new issue