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
@@ -12,8 +12,12 @@ import { ShareDialog } from "./ShareDialog";
|
||||
import { QueueList } from "./QueueList";
|
||||
import { BudgetChip } from "./Budget";
|
||||
import { PermissionCard, QuestionCard } from "./Interaction";
|
||||
import { Markdown } from "./Markdown";
|
||||
import { ImageRefs, Markdown } from "./Markdown";
|
||||
import { UsageOverlay } from "./UsageOverlay";
|
||||
import { RewindDialog } from "./RewindDialog";
|
||||
import { ShellsChip } from "./ShellsPanel";
|
||||
import { TodoList, ToolCard } from "./ToolCard";
|
||||
import { confirmDialog, promptDialog, toast } from "../dialogs";
|
||||
|
||||
type ToolResult = Extract<Part, { type: "tool_result" }>;
|
||||
|
||||
@@ -53,7 +57,7 @@ function Reasoning({ text, live }: { text: string; live: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AssistantParts({ parts, results, running, live }: { parts: Part[]; results: Map<string, ToolResult>; running: boolean; live: boolean }) {
|
||||
function AssistantParts({ parts, results, running, live, baseDir }: { parts: Part[]; results: Map<string, ToolResult>; running: boolean; live: boolean; baseDir: string }) {
|
||||
return (
|
||||
<>
|
||||
{parts.map((p, i) => {
|
||||
@@ -61,13 +65,14 @@ function AssistantParts({ parts, results, running, live }: { parts: Part[]; resu
|
||||
case "text":
|
||||
return (
|
||||
<div key={i} className={live && i === parts.length - 1 ? "streaming" : undefined}>
|
||||
<Markdown text={p.text} />
|
||||
<Markdown text={p.text} baseDir={baseDir} />
|
||||
{!live && <ImageRefs text={p.text} baseDir={baseDir} />}
|
||||
</div>
|
||||
);
|
||||
case "reasoning":
|
||||
return <Reasoning key={i} text={p.text} live={live && i === parts.length - 1} />;
|
||||
case "tool_call":
|
||||
return <ToolCard key={p.id} call={p} result={results.get(p.id)} running={running} />;
|
||||
return <ToolCard key={p.id} call={p} result={results.get(p.id)} running={running} baseDir={baseDir} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -102,46 +107,6 @@ function BackgroundResult({ text }: { text: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function StatsPanel({ stats }: { stats: SessionStats }) {
|
||||
const t = useT();
|
||||
const a = stats.total;
|
||||
return (
|
||||
<div className="panel" style={{ margin: "8px 0" }}>
|
||||
<table className="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("stats.model")}</th>
|
||||
<th className="num">{t("stats.requests")}</th>
|
||||
<th className="num">{t("stats.input")}</th>
|
||||
<th className="num">{t("stats.cacheRead")}</th>
|
||||
<th className="num">{t("stats.output")}</th>
|
||||
<th className="num">{t("stats.ttft")}</th>
|
||||
<th className="num">{t("stats.outputSpeed")}</th>
|
||||
<th className="num">{t("stats.cost")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...stats.by_model, { ...a, model: t("stats.total") }].map((r) => (
|
||||
<tr key={r.model}>
|
||||
<td>{r.model}</td>
|
||||
<td className="num">{fmtNumber(r.requests)}</td>
|
||||
<td className="num">{fmtCompact(r.input_tokens + r.cache_write_tokens)}</td>
|
||||
<td className="num">{fmtCompact(r.cache_read_tokens)}</td>
|
||||
<td className="num">{fmtCompact(r.output_tokens)}</td>
|
||||
<td className="num">{fmtNumber(r.avg_ttft_ms)} ms</td>
|
||||
<td className="num">{fmtNumber(r.output_tps, 1)} tok/s</td>
|
||||
<td className="num">{fmtCost(r.cost, stats.currency)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ fontSize: 12, color: "var(--muted)", marginTop: 6 }}>
|
||||
{t("stats.prefillSpeed")}: {fmtNumber(a.prefill_tps)} tok/s · {t("stats.cacheHit")}: {fmtPercent(a.cache_hit_rate)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: () => void }) {
|
||||
const t = useT();
|
||||
const session = useStore((s) => s.sessionById[sessionId]);
|
||||
@@ -195,7 +160,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
try {
|
||||
await patch(`/sessions/${sessionId}`, p);
|
||||
} catch (e: any) {
|
||||
alert(e.message);
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,20 +175,21 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
setText("");
|
||||
stick.current = true;
|
||||
} catch (e: any) {
|
||||
alert(e.message);
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const rewind = async (m: Message) => {
|
||||
if (!confirm(t("chat.rewindConfirm"))) return;
|
||||
const txt = m.parts.map((p) => (p.type === "text" ? p.text : "")).join("");
|
||||
await post(`/sessions/${sessionId}/rewind`, { message_id: m.id });
|
||||
const [rewinding, setRewinding] = useState<Message>();
|
||||
const rewind = (m: Message) => setRewinding(m);
|
||||
const rewindDone = async () => {
|
||||
const m = rewinding!;
|
||||
setRewinding(undefined);
|
||||
await loadSession(sessionId);
|
||||
setText(txt);
|
||||
setText(m.parts.map((p) => (p.type === "text" ? p.text : "")).join(""));
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
if (!confirm(t("chat.deleteConfirm"))) return;
|
||||
if (!(await confirmDialog({ message: t("chat.deleteConfirm"), danger: true, confirmLabel: t("common.delete") }))) return;
|
||||
await del(`/sessions/${sessionId}`);
|
||||
navigate(session.parent_id ? `/s/${session.parent_id}` : "/new");
|
||||
};
|
||||
@@ -237,6 +203,11 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
const todos = Array.isArray(session.todos) ? session.todos : [];
|
||||
const openTodos = todos.filter((x) => x.status !== "completed");
|
||||
const lastIsAssistant = messages.length > 0 && messages[messages.length - 1].role === "assistant";
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
const pendingCall = lastIsAssistant
|
||||
? (lastMsg.parts.filter((p) => p.type === "tool_call" && !results.has(p.id)).pop() as Extract<Part, { type: "tool_call" }> | undefined)
|
||||
: undefined;
|
||||
const pendingTool = busy && pendingCall ? { name: pendingCall.name, input: pendingCall.input, since: lastMsg.created_at } : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -279,6 +250,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
{fmtCost(stats.total.cost, stats.currency)}
|
||||
</button>
|
||||
)}
|
||||
<ShellsChip sessionId={sessionId} busy={busy} />
|
||||
{isAdmin && <BudgetChip />}
|
||||
{session.permission_mode === "bypass" && <span className="chip warn">{t("permissionMode.bypass")}</span>}
|
||||
{readOnly && <span className="chip">{t("chat.readOnly")}</span>}
|
||||
@@ -289,7 +261,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
⇄
|
||||
</button>
|
||||
)}
|
||||
<button className="small ghost" onClick={() => post(`/sessions/${sessionId}/compact`).catch((e) => alert(e.message))} disabled={busy} title={t("command.compact")}>
|
||||
<button className="small ghost" onClick={() => post(`/sessions/${sessionId}/compact`).catch((e) => toast(e.message))} disabled={busy} title={t("command.compact")}>
|
||||
⇊
|
||||
</button>
|
||||
<button className="small ghost danger" onClick={remove} title={t("chat.delete")}>
|
||||
@@ -308,13 +280,13 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
}}
|
||||
>
|
||||
<div className="chat">
|
||||
{showStats && stats && <StatsPanel stats={stats} />}
|
||||
{showStats && <UsageOverlay session={session} onClose={() => setShowStats(false)} />}
|
||||
{messages.length === 0 && !draft && <div className="empty">{t("chat.empty")}</div>}
|
||||
{messages.map((m) => {
|
||||
if (m.role === "assistant") {
|
||||
return (
|
||||
<div key={m.id} className="msg">
|
||||
<AssistantParts parts={m.parts} results={results} running={busy} live={false} />
|
||||
<AssistantParts parts={m.parts} results={results} running={busy} live={false} baseDir={session.cwd} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -367,11 +339,11 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
})}
|
||||
{draft && (
|
||||
<div className="msg">
|
||||
<AssistantParts parts={draft.parts} results={results} running={busy} live />
|
||||
<AssistantParts parts={draft.parts} results={results} running={busy} live baseDir={session.cwd} />
|
||||
</div>
|
||||
)}
|
||||
{(busy || waiting) && (
|
||||
<ActivityLine draft={draft} results={results} waiting={waiting} permissionPending={permissions.length > 0 || questions.length > 0} />
|
||||
<ActivityLine sessionId={sessionId} draft={draft} pendingTool={pendingTool} waiting={waiting} permissionPending={permissions.length > 0 || questions.length > 0} />
|
||||
)}
|
||||
{openTodos.length > 0 && (busy || permissions.length > 0 || questions.length > 0) && (
|
||||
<div className="todos">
|
||||
@@ -409,6 +381,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
</div>
|
||||
</div>
|
||||
{sharing && <ShareDialog sessionId={sessionId} onClose={() => setSharing(false)} />}
|
||||
{rewinding && <RewindDialog sessionId={sessionId} messageId={rewinding.id} onDone={rewindDone} onClose={() => setRewinding(undefined)} />}
|
||||
{!readOnly && <QueueList sessionId={sessionId} busy={busy} />}
|
||||
{readOnly ? (
|
||||
<div className="readonly-bar">{t("chat.readOnlyHint")}</div>
|
||||
|
||||
Reference in new issue
Block a user