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
@@ -1,9 +1,8 @@
|
||||
import { useEffect } from "react";
|
||||
import { useT } from "../i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { fmtCompact, useT } from "../i18n";
|
||||
import { useStore, type Draft } from "../store";
|
||||
import type { Part } from "../types";
|
||||
|
||||
type ToolResult = Extract<Part, { type: "tool_result" }>;
|
||||
|
||||
/** Animated three-dot pulse. */
|
||||
export function Dots() {
|
||||
@@ -20,43 +19,113 @@ export function Spinner({ small }: { small?: boolean }) {
|
||||
return <span className={`spinner ${small ? "small" : ""}`} aria-hidden="true" />;
|
||||
}
|
||||
|
||||
/** What the agent is doing right now, derived from the streaming message. */
|
||||
const STARS = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"];
|
||||
|
||||
/** Rotating star like a terminal agent's spinner. */
|
||||
function Star() {
|
||||
const [i, setI] = useState(0);
|
||||
useEffect(() => {
|
||||
const h = setInterval(() => setI((x) => (x + 1) % STARS.length), 120);
|
||||
return () => clearInterval(h);
|
||||
}, []);
|
||||
return <span className="star" aria-hidden="true">{STARS[i]}</span>;
|
||||
}
|
||||
|
||||
function useNow(active: boolean) {
|
||||
const [now, setNow] = useState(Date.now());
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const h = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(h);
|
||||
}, [active]);
|
||||
return now;
|
||||
}
|
||||
|
||||
function fmtElapsed(ms: number) {
|
||||
const s = Math.max(0, Math.floor(ms / 1000));
|
||||
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s`;
|
||||
}
|
||||
|
||||
/** Generated tokens so far, estimated from the streamed characters. */
|
||||
function generatedTokens(draft?: Draft) {
|
||||
if (!draft) return 0;
|
||||
let chars = 0;
|
||||
for (const p of draft.parts) {
|
||||
if (p.type === "text" || p.type === "reasoning") chars += p.text.length;
|
||||
else if (p.type === "tool_call") chars += JSON.stringify(p.input ?? {}).length;
|
||||
}
|
||||
return Math.round(chars / 4);
|
||||
}
|
||||
|
||||
/** What the agent is doing right now – playful text, elapsed time and tokens. */
|
||||
export function ActivityLine({
|
||||
sessionId,
|
||||
draft,
|
||||
results,
|
||||
pendingTool,
|
||||
waiting,
|
||||
permissionPending,
|
||||
}: {
|
||||
sessionId: string;
|
||||
draft?: Draft;
|
||||
results: Map<string, ToolResult>;
|
||||
/** A tool call of the last (finished) model step that has no result yet. */
|
||||
pendingTool?: { name: string; input: any; since: number };
|
||||
waiting?: string;
|
||||
permissionPending: boolean;
|
||||
}) {
|
||||
const t = useT();
|
||||
let label: string;
|
||||
const step = useStore((s) => s.steps[sessionId]);
|
||||
const now = useNow((!!step || !!pendingTool) && !permissionPending);
|
||||
|
||||
if (permissionPending) {
|
||||
label = t("activity.waitingForYou");
|
||||
} else if (waiting) {
|
||||
return (
|
||||
<div className="activity paused" role="status">
|
||||
<span className="pause-icon" aria-hidden="true" />
|
||||
<span>{t("activity.waitingForYou")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (waiting) {
|
||||
const [kind, ...rest] = waiting.split(":");
|
||||
const what = rest.join(":");
|
||||
label = kind === "model" ? t("chat.waitingModel", { model: what }) : t("chat.waitingSubagent", { agent: what === "*" ? t("chat.anySubagent") : what });
|
||||
} else {
|
||||
const last = draft?.parts[draft.parts.length - 1];
|
||||
const runningTool = draft?.parts.filter((p) => p.type === "tool_call" && !results.has(p.id)).pop() as
|
||||
| Extract<Part, { type: "tool_call" }>
|
||||
| undefined;
|
||||
if (runningTool) {
|
||||
label = runningTool.name === "task" ? t("activity.subagent", { name: runningTool.input?.description ?? "" }) : t("activity.tool", { tool: runningTool.name });
|
||||
} else if (last?.type === "text") {
|
||||
label = t("activity.writing");
|
||||
} else {
|
||||
label = t("activity.thinking");
|
||||
}
|
||||
return (
|
||||
<div className="activity" role="status">
|
||||
<Dots />
|
||||
<span>{kind === "model" ? t("chat.waitingModel", { model: what }) : t("chat.waitingSubagent", { agent: what === "*" ? t("chat.anySubagent") : what })}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// A tool is running between two model steps.
|
||||
if (!draft && pendingTool) {
|
||||
const label = pendingTool.name === "task" ? t("activity.subagent", { name: pendingTool.input?.description ?? "" }) : t("activity.tool", { tool: pendingTool.name });
|
||||
return (
|
||||
<div className="activity" role="status" aria-live="polite">
|
||||
<Star />
|
||||
<span className="activity-text">{label}</span>
|
||||
<span className="activity-meta">({fmtElapsed(now - pendingTool.since)})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const live = draft && step && draft.id === step.messageId ? draft : undefined;
|
||||
const generated = generatedTokens(live);
|
||||
const phrases = t("activity.phrases").split("|");
|
||||
// A stable starting phrase per step, changing every 4 seconds.
|
||||
const seed = step ? [...step.messageId].reduce((a, c) => a + c.charCodeAt(0), 0) : 0;
|
||||
const elapsed = step ? now - step.startedAt : 0;
|
||||
const label = `${phrases[(seed + Math.floor(elapsed / 4000)) % phrases.length]}…`;
|
||||
const prefill = step && generated === 0;
|
||||
|
||||
return (
|
||||
<div className={`activity ${permissionPending ? "paused" : ""}`} role="status" aria-live="polite">
|
||||
{permissionPending ? <span className="pause-icon" aria-hidden="true" /> : <Dots />}
|
||||
<span>{label}</span>
|
||||
<div className="activity" role="status" aria-live="polite">
|
||||
<Star />
|
||||
<span className="activity-text">{label}</span>
|
||||
{step && (
|
||||
<span className="activity-meta">
|
||||
({fmtElapsed(elapsed)} · ↑ {step.estimated ? "~" : ""}
|
||||
{fmtCompact(step.promptTokens)} {t("activity.prefill")} · ↓ {prefill ? t("activity.prefilling") : `${fmtCompact(generated)} ${t("activity.generated")}`})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user