Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes
- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas, login rate limiting, audit log and backups - Ghidra headless decompilation (ARM64 natives built from source), binary_info tool, clean room / deep workflow per session - Recreate skill and commands for analysing and re-building old software - File checkpoints with rewind/restore, background shells (bash_output, kill_shell) - web_search tool (DuckDuckGo, SearXNG, Brave, Tavily) - In-app dialogs and toasts instead of browser dialogs - 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One, Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fab9add5d5
commit
5b569b6835
108 files changed
+6785
-279
No files matched your search
@@ -0,0 +1,142 @@
|
||||
// In-app dialogs and toasts instead of the browser's alert/confirm/prompt.
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
import { t } from "./i18n";
|
||||
|
||||
interface ConfirmOpts {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
/** Destructive action: red button. */
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
interface PromptOpts {
|
||||
title?: string;
|
||||
message: string;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
password?: boolean;
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
type Dialog =
|
||||
| ({ kind: "confirm"; id: number; resolve: (v: boolean) => void } & ConfirmOpts)
|
||||
| ({ kind: "prompt"; id: number; resolve: (v: string | null) => void } & PromptOpts);
|
||||
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
kind: "error" | "info" | "success";
|
||||
}
|
||||
|
||||
let dialogs: Dialog[] = [];
|
||||
let toasts: Toast[] = [];
|
||||
let seq = 0;
|
||||
const listeners = new Set<() => void>();
|
||||
const emit = () => listeners.forEach((l) => l());
|
||||
const subscribe = (l: () => void) => (listeners.add(l), () => listeners.delete(l));
|
||||
|
||||
export function confirmDialog(opts: ConfirmOpts | string): Promise<boolean> {
|
||||
const o = typeof opts === "string" ? { message: opts } : opts;
|
||||
return new Promise((resolve) => {
|
||||
dialogs = [...dialogs, { kind: "confirm", id: ++seq, resolve, ...o }];
|
||||
emit();
|
||||
});
|
||||
}
|
||||
|
||||
export function promptDialog(opts: PromptOpts | string): Promise<string | null> {
|
||||
const o = typeof opts === "string" ? { message: opts } : opts;
|
||||
return new Promise((resolve) => {
|
||||
dialogs = [...dialogs, { kind: "prompt", id: ++seq, resolve, ...o }];
|
||||
emit();
|
||||
});
|
||||
}
|
||||
|
||||
export function toast(message: unknown, kind: Toast["kind"] = "error") {
|
||||
const text = message instanceof Error ? message.message : typeof message === "string" ? message : String((message as any)?.message ?? message);
|
||||
const id = ++seq;
|
||||
toasts = [...toasts, { id, message: text, kind }];
|
||||
emit();
|
||||
setTimeout(() => {
|
||||
toasts = toasts.filter((x) => x.id !== id);
|
||||
emit();
|
||||
}, kind === "error" ? 7000 : 3500);
|
||||
}
|
||||
|
||||
function close(d: Dialog, value: any) {
|
||||
dialogs = dialogs.filter((x) => x.id !== d.id);
|
||||
emit();
|
||||
(d.resolve as (v: any) => void)(value);
|
||||
}
|
||||
|
||||
function DialogView({ d }: { d: Dialog }) {
|
||||
const [value, setValue] = useState(d.kind === "prompt" ? d.defaultValue ?? "" : "");
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const ok = useRef<HTMLButtonElement>(null);
|
||||
const cancelValue = d.kind === "confirm" ? false : null;
|
||||
useEffect(() => {
|
||||
if (d.kind === "prompt") {
|
||||
input.current?.focus();
|
||||
input.current?.select();
|
||||
} else ok.current?.focus();
|
||||
const h = (e: KeyboardEvent) => e.key === "Escape" && close(d, cancelValue);
|
||||
window.addEventListener("keydown", h);
|
||||
return () => window.removeEventListener("keydown", h);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const submit = () => close(d, d.kind === "confirm" ? true : value);
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={() => close(d, cancelValue)}>
|
||||
<form
|
||||
className="modal dialog"
|
||||
role={d.kind === "confirm" ? "alertdialog" : "dialog"}
|
||||
aria-label={d.title ?? d.message}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onSubmit={(e) => (e.preventDefault(), submit())}
|
||||
>
|
||||
{d.title && <h3>{d.title}</h3>}
|
||||
<p className="dialog-message">{d.message}</p>
|
||||
{d.kind === "prompt" && (
|
||||
<input
|
||||
ref={input}
|
||||
type={d.password ? "password" : "text"}
|
||||
value={value}
|
||||
placeholder={d.placeholder}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
autoComplete={d.password ? "new-password" : "off"}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
)}
|
||||
<div className="row dialog-actions">
|
||||
<button type="button" onClick={() => close(d, cancelValue)}>
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button ref={ok} type="submit" className={d.kind === "confirm" && d.danger ? "danger-solid" : "primary"}>
|
||||
{d.confirmLabel ?? t("common.ok")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders open dialogs and toasts; mounted once in App. */
|
||||
export function DialogHost() {
|
||||
const state = useSyncExternalStore(subscribe, () => dialogs);
|
||||
const ts = useSyncExternalStore(subscribe, () => toasts);
|
||||
return (
|
||||
<>
|
||||
{state.map((d) => (
|
||||
<DialogView key={d.id} d={d} />
|
||||
))}
|
||||
<div className="toasts" role="status" aria-live="polite">
|
||||
{ts.map((x) => (
|
||||
<div key={x.id} className={`toast ${x.kind}`} onClick={() => ((toasts = toasts.filter((y) => y.id !== x.id)), emit())}>
|
||||
{x.kind === "error" ? "⚠ " : x.kind === "success" ? "✓ " : ""}
|
||||
{x.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user