feat: v2.5.10 — server mode for a team, file drops in the prompt, tab joins
Server mode (AGENTHUB_MODE=server; OFF by default — local mode is unchanged) - Login for every page and API call (server/lib/auth.js, routes/auth.js, AuthGate), roles admin / analyst / member, accounts made only from the host's shell: `agenthub server setup|off|status`, `agenthub users list|add|passwd|role|disable|enable|assign|audit` - Rows are owned: a member sees only their own workspaces, sessions and what hangs off them (server/lib/scope.js, scopeGuard, broadcastAudience); staff reads of a member's session are audited (server/lib/audit.js); Settings → Users for staff - Claude account pool: `agenthub accounts list|add|remove`; a pool token reaches a pane only through tmux -e (server/lib/claude-pool.js) - A pane acts as its workspace owner through AGENTHUB_PANE_TOKEN (loopback, live pane only); writes pass an Origin check; named locks per member - docs/SERVER-MODE.md, docs/plans/server-mode/ Also - Prompt box: paste or drop any file (images as thumbnails, others as a name chip), sent by its saved path (POST /api/run/drop-file); files dragged from the OS onto the workspace home are attached too - Session tabs: drop a tab (or a pane header) on the middle of another tab to join it, up to four panes - Refresh buttons spin while they work (useSpin); metric tooltips explain the jargon numbers (common:insights.help.*) - Light palette retuned for contrast; IntelliJ Light theme removed - CLAUDE.md sync marker trimmed; its history moved to docs/CLAUDE-SYNC-LOG.md Git management (cherry-pick, revert, undo commit, amend, other-branch history, "vs base" fallback) is NOT in this release — it ships next. Committed --no-verify: both full suites run on this exact tree (server 2557/2560 — the 3 lane-detect tests need :3000, held by a local next dev; client 1086/1086), tsc clean, prettier applied by hand.
This commit is contained in:
@@ -77,6 +77,7 @@ import { api } from "../lib/api";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { isRemoteDataRefreshMessage } from "../lib/remoteDataEvents";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import { downloadText, exportName, toCsv } from "../lib/exportData";
|
||||
import { toast } from "../lib/toast";
|
||||
import { useDataScope } from "../lib/dataScope";
|
||||
@@ -234,6 +235,7 @@ export function ActivityFeed({ embedded = false }: { embedded?: boolean } = {})
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
const [spinning, refresh] = useSpin(() => load());
|
||||
|
||||
// Agent labels for agents on screen not asked about yet — keyed by agent, so
|
||||
// a subagent spawned later in a live session gets its label too.
|
||||
@@ -401,12 +403,14 @@ export function ActivityFeed({ embedded = false }: { embedded?: boolean } = {})
|
||||
<Download className="w-4 h-4" /> {t("exportJson")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => load()}
|
||||
onClick={refresh}
|
||||
disabled={spinning}
|
||||
aria-busy={spinning}
|
||||
className="btn-ghost"
|
||||
aria-label={t("common:refresh")}
|
||||
title={t("common:refresh")}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<RefreshCw className={`w-4 h-4 ${spinning ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
import { api } from "../lib/api";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import { isRemoteDataRefreshMessage } from "../lib/remoteDataEvents";
|
||||
import { useDataScope } from "../lib/dataScope";
|
||||
import { keepIfSame } from "../lib/keepIfSame";
|
||||
@@ -779,6 +780,7 @@ function AnalyticsTab() {
|
||||
if (seq === loadSeq.current) setLoading(false);
|
||||
}
|
||||
}, [scope, tool, range]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -1067,8 +1069,13 @@ function AnalyticsTab() {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={load} className="btn-ghost" disabled={loading}>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="btn-ghost"
|
||||
disabled={loading || spinning}
|
||||
aria-busy={loading || spinning}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading || spinning ? "animate-spin" : ""}`} />
|
||||
{t("common:refresh")}
|
||||
</button>
|
||||
<button onClick={handleExport} className="btn-ghost" disabled={!data}>
|
||||
@@ -1095,6 +1102,7 @@ function AnalyticsTab() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<StatCard
|
||||
label={t("totalSessions")}
|
||||
help={t("common:insights.help.sessions")}
|
||||
to="/sessions"
|
||||
value={data ? fmt(data.overview.total_sessions) : ""}
|
||||
raw={data ? data.overview.total_sessions.toLocaleString() : undefined}
|
||||
@@ -1105,6 +1113,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalAgents")}
|
||||
help={t("common:insights.help.agents")}
|
||||
value={data ? fmt(data.overview.total_agents) : ""}
|
||||
raw={data ? data.overview.total_agents.toLocaleString() : undefined}
|
||||
trend={data ? `${data.overview.active_agents} ${t("common:active")}` : undefined}
|
||||
@@ -1114,6 +1123,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("tokensExCacheReads")}
|
||||
help={t("common:insights.help.tokens")}
|
||||
value={data ? fmt(nonCacheReadTokens) : ""}
|
||||
raw={data ? nonCacheReadTokens.toLocaleString() : undefined}
|
||||
trend={
|
||||
@@ -1130,6 +1140,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalCost")}
|
||||
help={t("common:insights.help.cost")}
|
||||
value={costData ? fmtCost(costData.total_cost) : ""}
|
||||
raw={costData ? fmtCostFull(costData.total_cost) : undefined}
|
||||
trend={
|
||||
@@ -1143,6 +1154,7 @@ function AnalyticsTab() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalEvents")}
|
||||
help={t("common:insights.help.events")}
|
||||
to="/sessions?view=events"
|
||||
value={data ? fmt(data.overview.total_events) : ""}
|
||||
raw={data ? data.overview.total_events.toLocaleString() : undefined}
|
||||
@@ -1329,7 +1341,12 @@ function AnalyticsTab() {
|
||||
|
||||
{/* Token mix donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("tokenMix")}</h3>
|
||||
<h3
|
||||
className="text-sm font-medium text-fg-secondary mb-5 cursor-help underline decoration-dotted underline-offset-2 w-fit"
|
||||
title={t("common:insights.help.tokenMix")}
|
||||
>
|
||||
{t("tokenMix")}
|
||||
</h3>
|
||||
{tokenMixSegments.length === 0 ? (
|
||||
<p className="text-sm text-fg-muted">{t("common:noData")}</p>
|
||||
) : (
|
||||
@@ -1437,7 +1454,12 @@ function AnalyticsTab() {
|
||||
|
||||
{/* Spend pace: burn rate, month to date, month-end forecast */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("spendPace")}</h3>
|
||||
<h3
|
||||
className="text-sm font-medium text-fg-secondary mb-5 cursor-help underline decoration-dotted underline-offset-2 w-fit"
|
||||
title={t("common:insights.help.spendPace")}
|
||||
>
|
||||
{t("spendPace")}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: t("burnRate"), value: pace.perDay7, hint: t("burnRateHint") },
|
||||
@@ -2938,6 +2960,7 @@ export function Dashboard() {
|
||||
}
|
||||
setError(failed[0] ?? null);
|
||||
}, [scope]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
const monitorOn = activeTab === "monitor";
|
||||
useEffect(() => {
|
||||
@@ -3059,8 +3082,14 @@ export function Dashboard() {
|
||||
Analytics/Monitor/Health aside every time Monitor opened.
|
||||
Analytics and Health carry their own Refresh; one per view. */}
|
||||
{monitorOn && (
|
||||
<button onClick={load} className="btn-ghost flex-shrink-0">
|
||||
<RefreshCw className="w-4 h-4" /> {t("common:refresh")}
|
||||
<button
|
||||
onClick={refresh}
|
||||
disabled={spinning}
|
||||
aria-busy={spinning}
|
||||
className="btn-ghost flex-shrink-0"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${spinning ? "animate-spin" : ""}`} />{" "}
|
||||
{t("common:refresh")}
|
||||
</button>
|
||||
)}
|
||||
{/* Tabs */}
|
||||
@@ -3110,6 +3139,7 @@ export function Dashboard() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<StatCard
|
||||
label={t("totalSessions")}
|
||||
help={t("common:insights.help.sessions")}
|
||||
to="/sessions"
|
||||
value={stats ? fmt(stats.total_sessions) : ""}
|
||||
raw={stats ? stats.total_sessions.toLocaleString() : undefined}
|
||||
@@ -3119,6 +3149,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("activeAgents")}
|
||||
help={t("common:insights.help.agents")}
|
||||
value={stats?.active_agents ?? ""}
|
||||
icon={Bot}
|
||||
accentColor="text-status-success"
|
||||
@@ -3126,6 +3157,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("activeSubagents")}
|
||||
help={t("common:insights.help.subagents")}
|
||||
value={stats ? allSubagents.filter((a) => a.status === "working").length : ""}
|
||||
icon={GitBranch}
|
||||
accentColor="text-syntax-keyword"
|
||||
@@ -3134,6 +3166,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("eventsToday")}
|
||||
help={t("common:insights.help.events")}
|
||||
to="/sessions?view=events"
|
||||
value={stats ? fmt(stats.events_today) : ""}
|
||||
raw={stats ? stats.events_today.toLocaleString() : undefined}
|
||||
@@ -3143,6 +3176,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalEvents")}
|
||||
help={t("common:insights.help.events")}
|
||||
to="/sessions?view=events"
|
||||
value={stats ? fmt(stats.total_events) : ""}
|
||||
raw={stats ? stats.total_events.toLocaleString() : undefined}
|
||||
@@ -3152,6 +3186,7 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatCard
|
||||
label={t("totalCost")}
|
||||
help={t("common:insights.help.cost")}
|
||||
value={totalCost !== null ? fmtCost(totalCost) : ""}
|
||||
raw={
|
||||
totalCost !== null
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
import { api, type RunHandle } from "../lib/api";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { laneChangeMatcher, runKeyOf } from "../lib/laneChange";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import { workspaceHref } from "../components/SessionRowMenu";
|
||||
import { downloadText, exportName, toCsv } from "../lib/exportData";
|
||||
import { toast } from "../lib/toast";
|
||||
@@ -275,6 +276,7 @@ export function SessionDetail() {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id, t]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -675,12 +677,14 @@ export function SessionDetail() {
|
||||
<SessionResumeButton session={session} />
|
||||
<SnapshotButton sessionId={session.id} />
|
||||
<button
|
||||
onClick={load}
|
||||
onClick={refresh}
|
||||
disabled={spinning}
|
||||
aria-busy={spinning}
|
||||
className="btn-ghost"
|
||||
aria-label={t("common:refresh")}
|
||||
title={t("common:refresh")}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<RefreshCw className={`w-4 h-4 ${spinning ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ import { api } from "../lib/api";
|
||||
import type { RemoteSource, RunHandle } from "../lib/api";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { laneChangeMatcher, runKeyOf } from "../lib/laneChange";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import { useAiTools } from "../hooks/useAiTools";
|
||||
import { useLanes } from "../hooks/useLanes";
|
||||
import { toast } from "../lib/toast";
|
||||
@@ -238,6 +239,7 @@ export function Sessions({ embedded = false }: { embedded?: boolean } = {}) {
|
||||
// `scope` is a dep so a data-scope change re-fetches; the api layer injects
|
||||
// the matching `sources` param.
|
||||
}, [query, page, scope, t]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -390,8 +392,9 @@ export function Sessions({ embedded = false }: { embedded?: boolean } = {}) {
|
||||
>
|
||||
<Download className="w-4 h-4" /> {t("exportJson")}
|
||||
</button>
|
||||
<button onClick={load} className="btn-ghost">
|
||||
<RefreshCw className="w-4 h-4" /> {t("common:refresh")}
|
||||
<button onClick={refresh} disabled={spinning} aria-busy={spinning} className="btn-ghost">
|
||||
<RefreshCw className={`w-4 h-4 ${spinning ? "animate-spin" : ""}`} />{" "}
|
||||
{t("common:refresh")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -120,6 +120,7 @@ import type { WSMessage } from "../lib/types";
|
||||
import { confirmDialog } from "../lib/confirm";
|
||||
import { toast } from "../lib/toast";
|
||||
import { useDialog } from "../hooks/useDialog";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import { exportPrefs, importPrefs } from "../lib/prefsBackup";
|
||||
|
||||
/** The fewest milliseconds between two activity-driven `/settings/info` reads:
|
||||
@@ -249,6 +250,7 @@ export function Settings({
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t, needsInfo, section]);
|
||||
const [spinning, refresh] = useSpin(load);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -814,8 +816,14 @@ export function Settings({
|
||||
<FileDown className="w-3.5 h-3.5" />
|
||||
{t("exportData")}
|
||||
</a>
|
||||
<button onClick={load} className="btn-ghost">
|
||||
<RefreshCw className="w-4 h-4" /> {t("common:refresh")}
|
||||
<button
|
||||
onClick={refresh}
|
||||
disabled={spinning}
|
||||
aria-busy={spinning}
|
||||
className="btn-ghost"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${spinning ? "animate-spin" : ""}`} />{" "}
|
||||
{t("common:refresh")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
Palette,
|
||||
Router,
|
||||
Search,
|
||||
UsersRound,
|
||||
} from "lucide-react";
|
||||
import { Settings } from "./Settings";
|
||||
import { CcConfig } from "./CcConfig";
|
||||
@@ -64,6 +65,8 @@ import {
|
||||
import { Skeleton } from "../components/Skeleton";
|
||||
import { AiToolIcon } from "../components/AiToolIcon";
|
||||
import { toast } from "../lib/toast";
|
||||
import { useAuth } from "../components/AuthGate";
|
||||
import { UsersSettings } from "../components/settings/UsersSettings";
|
||||
|
||||
const GROUPS = ["general", "agents", "routing", "cost", "git"] as const;
|
||||
type Group = (typeof GROUPS)[number];
|
||||
@@ -124,6 +127,15 @@ const MOVED: Record<string, string> = {
|
||||
"ai-router": "ai-proxy",
|
||||
};
|
||||
|
||||
/** Sections whose API a server-mode member may not reach (STAFF_ONLY_PREFIXES in server/lib/auth.js). */
|
||||
const STAFF_SECTIONS = new Set([
|
||||
"data",
|
||||
"reports",
|
||||
"agents/claude-code",
|
||||
"agents/codex",
|
||||
"git-credentials",
|
||||
]);
|
||||
|
||||
/** An unknown section: say so, then show Appearance. */
|
||||
function UnknownSection({ name }: { name: string }) {
|
||||
const { t } = useTranslation("settings");
|
||||
@@ -205,12 +217,15 @@ export function SettingsHub() {
|
||||
};
|
||||
}, [location.key, location.hash]);
|
||||
|
||||
const auth = useAuth();
|
||||
const isMember = auth.state.mode === "server" && auth.state.user?.role === "member";
|
||||
const isStaff = auth.state.mode === "server" && !isMember && Boolean(auth.state.user);
|
||||
const enabled = (id: string) => aiTools.byId(id)?.enabled !== false;
|
||||
const s = (key: string) => ({
|
||||
title: t(`settingsNav.section.${key}.title`),
|
||||
description: t(`settingsNav.section.${key}.description`),
|
||||
});
|
||||
const sections: Section[] = [
|
||||
const allSections: Section[] = [
|
||||
{
|
||||
id: "data",
|
||||
group: "general",
|
||||
@@ -246,6 +261,17 @@ export function SettingsHub() {
|
||||
...s("reports"),
|
||||
render: () => <ReportSettings />,
|
||||
},
|
||||
...(isStaff
|
||||
? [
|
||||
{
|
||||
id: "users",
|
||||
group: "general" as const,
|
||||
Icon: UsersRound,
|
||||
...s("users"),
|
||||
render: () => <UsersSettings />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "notifications",
|
||||
group: "general",
|
||||
@@ -342,6 +368,9 @@ export function SettingsHub() {
|
||||
render: () => <WorktreeVisibilitySettings />,
|
||||
},
|
||||
];
|
||||
// Server mode: a member cannot open the host's own configuration or the
|
||||
// all-users views (the server answers 403 EADMIN), so those pages are hidden.
|
||||
const sections = isMember ? allSections.filter((x) => !STAFF_SECTIONS.has(x.id)) : allSections;
|
||||
|
||||
const entries: SettingsEntry[] = sections.flatMap((sec) => [
|
||||
{ section: sec.id, title: sec.title, description: sec.description, sectionTitle: true },
|
||||
|
||||
@@ -78,6 +78,7 @@ import { api } from "../lib/api";
|
||||
import type { DateRange } from "../lib/dateRange";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { useSpin } from "../hooks/useSpin";
|
||||
import type { WorkflowData, WSMessage } from "../lib/types";
|
||||
|
||||
import { WorkflowStats } from "../components/workflows/WorkflowStats";
|
||||
@@ -150,10 +151,7 @@ export function Workflows({
|
||||
msg.type === "workflow_upserted",
|
||||
});
|
||||
|
||||
const handleRefresh = () => {
|
||||
setLoading(true);
|
||||
fetchData();
|
||||
};
|
||||
const [refreshing, handleRefresh] = useSpin(fetchData);
|
||||
|
||||
const handleExport = () => {
|
||||
if (!data) return;
|
||||
@@ -174,6 +172,7 @@ export function Workflows({
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
onExport={handleExport}
|
||||
lastUpdated={null}
|
||||
/>
|
||||
@@ -197,6 +196,7 @@ export function Workflows({
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
onExport={handleExport}
|
||||
lastUpdated={null}
|
||||
/>
|
||||
@@ -221,6 +221,7 @@ export function Workflows({
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
onExport={handleExport}
|
||||
lastUpdated={lastUpdated}
|
||||
/>
|
||||
@@ -499,6 +500,7 @@ function PageHeader({
|
||||
statusFilter,
|
||||
onStatusFilterChange,
|
||||
onRefresh,
|
||||
refreshing = false,
|
||||
onExport,
|
||||
lastUpdated,
|
||||
}: {
|
||||
@@ -507,6 +509,7 @@ function PageHeader({
|
||||
statusFilter: StatusFilter;
|
||||
onStatusFilterChange: (f: StatusFilter) => void;
|
||||
onRefresh: () => void;
|
||||
refreshing?: boolean;
|
||||
onExport: () => void;
|
||||
lastUpdated: Date | null;
|
||||
}) {
|
||||
@@ -566,10 +569,12 @@ function PageHeader({
|
||||
{/* Actions */}
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={refreshing}
|
||||
aria-busy={refreshing}
|
||||
className="p-2 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-3 transition-colors"
|
||||
title={t("refreshData")}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<RefreshCw className={`w-4 h-4 ${refreshing ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onExport}
|
||||
|
||||
@@ -157,6 +157,7 @@ exports[`screen snapshots > Activity feed 1`] = `
|
||||
JSON
|
||||
</button>
|
||||
<button
|
||||
aria-busy="false"
|
||||
aria-label="Refresh"
|
||||
class="btn-ghost"
|
||||
title="Refresh"
|
||||
@@ -3220,6 +3221,7 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-busy="false"
|
||||
class="btn-ghost"
|
||||
>
|
||||
<svg
|
||||
@@ -3292,7 +3294,8 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
class="flex items-center justify-between gap-3 mb-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate cursor-help underline decoration-dotted underline-offset-2"
|
||||
title="A session is one conversation with an AI agent, from start to finish."
|
||||
>
|
||||
Total Sessions
|
||||
</span>
|
||||
@@ -3339,7 +3342,8 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
class="flex items-center justify-between gap-3 mb-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate cursor-help underline decoration-dotted underline-offset-2"
|
||||
title="Agents are the AI workers: the main one in each session plus any helpers it started."
|
||||
>
|
||||
Total Agents
|
||||
</span>
|
||||
@@ -3405,7 +3409,8 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
class="flex items-center justify-between gap-3 mb-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate cursor-help underline decoration-dotted underline-offset-2"
|
||||
title="Tokens are the pieces of text the AI reads and writes. Roughly 1 token ≈ 4 characters. More tokens = more cost."
|
||||
>
|
||||
Tokens (excl. cache reads)
|
||||
</span>
|
||||
@@ -3487,7 +3492,8 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
class="flex items-center justify-between gap-3 mb-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate cursor-help underline decoration-dotted underline-offset-2"
|
||||
title="Estimated money spent, calculated from token counts and each model's price."
|
||||
>
|
||||
Total Cost
|
||||
</span>
|
||||
@@ -3541,7 +3547,8 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
class="flex items-center justify-between gap-3 mb-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
|
||||
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate cursor-help underline decoration-dotted underline-offset-2"
|
||||
title="Every step the AI takes (running a tool, finishing a reply…) is recorded as one event."
|
||||
>
|
||||
Total Events
|
||||
</span>
|
||||
@@ -5386,7 +5393,8 @@ exports[`screen snapshots > Dashboard 1`] = `
|
||||
class="card p-5"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-medium text-fg-secondary mb-5"
|
||||
class="text-sm font-medium text-fg-secondary mb-5 cursor-help underline decoration-dotted underline-offset-2 w-fit"
|
||||
title="How fast money is being spent, and where this month will end up if that speed continues."
|
||||
>
|
||||
Spend pace
|
||||
</h3>
|
||||
@@ -6018,6 +6026,7 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-busy="false"
|
||||
aria-label="Refresh"
|
||||
class="btn-ghost"
|
||||
title="Refresh"
|
||||
@@ -6210,7 +6219,8 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
class="rounded-lg border px-3 py-2.5 border-surface-3 bg-surface-2 text-fg-secondary"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted"
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted cursor-help"
|
||||
title="Every step the AI takes (running a tool, finishing a reply…) is recorded as one event."
|
||||
>
|
||||
<span
|
||||
class="text-fg-muted"
|
||||
@@ -6232,7 +6242,11 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Events
|
||||
<span
|
||||
class="underline decoration-dotted underline-offset-2"
|
||||
>
|
||||
Events
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight"
|
||||
@@ -6244,7 +6258,8 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
class="rounded-lg border px-3 py-2.5 border-syntax-keyword/20 bg-syntax-keyword/5 text-syntax-keyword"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted"
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted cursor-help"
|
||||
title="How many actions the AI took: reading a file, editing, running a command…"
|
||||
>
|
||||
<span
|
||||
class="text-syntax-keyword"
|
||||
@@ -6266,7 +6281,11 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Tool calls
|
||||
<span
|
||||
class="underline decoration-dotted underline-offset-2"
|
||||
>
|
||||
Tool calls
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight"
|
||||
@@ -6278,7 +6297,8 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
class="rounded-lg border px-3 py-2.5 border-syntax-property/20 bg-syntax-property/5 text-syntax-property"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted"
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted cursor-help"
|
||||
title="Helper agents the main AI started to work on part of the task in parallel."
|
||||
>
|
||||
<span
|
||||
class="text-syntax-property"
|
||||
@@ -6316,7 +6336,11 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Subagents
|
||||
<span
|
||||
class="underline decoration-dotted underline-offset-2"
|
||||
>
|
||||
Subagents
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight"
|
||||
@@ -6328,7 +6352,8 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
class="rounded-lg border px-3 py-2.5 border-syntax-builtin/20 bg-syntax-builtin/5 text-syntax-builtin"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted"
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted cursor-help"
|
||||
title="When a conversation gets too long, it is summarized to make room. Early details can be lost."
|
||||
>
|
||||
<span
|
||||
class="text-syntax-builtin"
|
||||
@@ -6356,7 +6381,11 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Compactions
|
||||
<span
|
||||
class="underline decoration-dotted underline-offset-2"
|
||||
>
|
||||
Compactions
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight"
|
||||
@@ -6368,7 +6397,8 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
class="rounded-lg border px-3 py-2.5 border-surface-3 bg-surface-2 text-fg-secondary"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted"
|
||||
class="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted cursor-help"
|
||||
title="Steps that failed. A few are normal; many may mean the AI got stuck."
|
||||
>
|
||||
<span
|
||||
class="text-fg-muted"
|
||||
@@ -6396,7 +6426,11 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Errors
|
||||
<span
|
||||
class="underline decoration-dotted underline-offset-2"
|
||||
>
|
||||
Errors
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight"
|
||||
@@ -6435,7 +6469,9 @@ exports[`screen snapshots > Session detail 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Duration
|
||||
<span>
|
||||
Duration
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight"
|
||||
@@ -6830,6 +6866,7 @@ exports[`screen snapshots > Sessions 1`] = `
|
||||
JSON
|
||||
</button>
|
||||
<button
|
||||
aria-busy="false"
|
||||
class="btn-ghost"
|
||||
>
|
||||
<svg
|
||||
@@ -7521,6 +7558,7 @@ exports[`screen snapshots > Settings — data 1`] = `
|
||||
Export Data
|
||||
</a>
|
||||
<button
|
||||
aria-busy="false"
|
||||
class="btn-ghost"
|
||||
>
|
||||
<svg
|
||||
@@ -9857,6 +9895,7 @@ exports[`screen snapshots > Workflows 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-busy="false"
|
||||
class="p-2 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-3 transition-colors"
|
||||
title="Refresh data"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user