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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 19:12:30 +01:00
1 parent fab9add5d5
commit 5b569b6835
108 files changed
+6785 -279

No files matched your search

+142
View File
@@ -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>
</>
);
}