-
+ number>
};
const activeOnly = ref(false);
+/** When false (default), hide Settings test-pilot smokes. When true, only those. */
+const showPilot = ref(false);
const liveSessionCount = computed(
() => sessions.sessions.filter((s) => isSessionLive(s.status)).length,
);
+const pilotSessionCount = computed(
+ () => sessions.sessions.filter((s) => isPilotSession(s)).length,
+);
const filteredSessions = computed((): SessionRef[] => {
+ const q = sessionFilter.value.toLowerCase();
+ return sessions.sessions.filter(
+ (s) =>
+ (!activeOnly.value || isSessionLive(s.status)) &&
+ (showPilot.value ? isPilotSession(s) : !isPilotSession(s)) &&
+ (providerFilter.value === "all" || s.provider === providerFilter.value) &&
+ (!q ||
+ s.title?.toLowerCase().includes(q) ||
+ s.projectDir.toLowerCase().includes(q) ||
+ s.agent?.toLowerCase().includes(q)),
+ );
+});
+
+/** Year chart: same filters as the list, but always includes both actual + test. */
+const heatBaseSessions = computed((): SessionRef[] => {
const q = sessionFilter.value.toLowerCase();
return sessions.sessions.filter(
(s) =>
@@ -767,13 +823,20 @@ const sessionHeat = computed(() => {
const year = heatYear.value;
const yearPrefix = `${year}-`;
const today = startOfLocalDay(Date.now());
- const sessCounts = new Map();
+ const testMode = showPilot.value;
+ const actualCounts = new Map();
+ const testCounts = new Map();
const weights = new Map();
let sessInYear = 0;
- for (const s of filteredSessions.value) {
+ for (const s of heatBaseSessions.value) {
if (!s.updatedAt) continue;
- const w = Math.max(1, Math.min(20, Math.ceil((s.messageCount ?? 0) / 50) || 1));
+ const isTest = isPilotSession(s);
+ // Heat / year totals follow the list filter: actual by default, tests when ✦ test is on.
+ const contributesHeat = testMode ? isTest : !isTest;
+ const w = contributesHeat
+ ? Math.max(1, Math.min(20, Math.ceil((s.messageCount ?? 0) / 50) || 1))
+ : 0;
let touchesYear = false;
const days: string[] = [];
forEachSessionDay(s, (k) => {
@@ -781,13 +844,14 @@ const sessionHeat = computed(() => {
if (k.startsWith(yearPrefix)) touchesYear = true;
});
if (!touchesYear) continue;
- sessInYear += 1;
+ if (contributesHeat) sessInYear += 1;
const dayN = days.length || 1;
- const perDay = Math.max(1, Math.round(w / dayN));
+ const perDay = w > 0 ? Math.max(1, Math.round(w / dayN)) : 0;
for (const k of days) {
if (!k.startsWith(yearPrefix)) continue;
- sessCounts.set(k, (sessCounts.get(k) ?? 0) + 1);
- weights.set(k, (weights.get(k) ?? 0) + perDay);
+ if (isTest) testCounts.set(k, (testCounts.get(k) ?? 0) + 1);
+ else actualCounts.set(k, (actualCounts.get(k) ?? 0) + 1);
+ if (perDay > 0) weights.set(k, (weights.get(k) ?? 0) + perDay);
}
}
@@ -806,6 +870,7 @@ const sessionHeat = computed(() => {
key: string;
weight: number;
sessions: number;
+ tests: number;
ts: number;
inRange: boolean;
inYear: boolean;
@@ -818,9 +883,18 @@ const sessionHeat = computed(() => {
const inYear = cur.getFullYear() === year;
const inRange = inYear && key <= todayKey;
const weight = inYear ? (weights.get(key) ?? 0) : 0;
- const sessionsN = inYear ? (sessCounts.get(key) ?? 0) : 0;
+ const sessionsN = inYear ? (actualCounts.get(key) ?? 0) : 0;
+ const testsN = inYear ? (testCounts.get(key) ?? 0) : 0;
if (inRange && weight > max) max = weight;
- raw.push({ key, weight, sessions: sessionsN, ts, inRange, inYear });
+ raw.push({
+ key,
+ weight,
+ sessions: sessionsN,
+ tests: testsN,
+ ts,
+ inRange,
+ inYear,
+ });
cur.setDate(cur.getDate() + 1);
}
@@ -835,6 +909,23 @@ const sessionHeat = computed(() => {
return 4;
};
+ function heatTitle(actual: number, tests: number, date: string): string {
+ if (testMode) {
+ const testPart =
+ tests === 0
+ ? "No test sessions"
+ : `${tests} test session${tests === 1 ? "" : "s"}`;
+ if (actual <= 0) return `${testPart} · ${date}`;
+ return `${testPart} (${actual} session${actual === 1 ? "" : "s"}) · ${date}`;
+ }
+ const actualPart =
+ actual === 0
+ ? "No sessions"
+ : `${actual} session${actual === 1 ? "" : "s"}`;
+ if (tests <= 0) return `${actualPart} · ${date}`;
+ return `${actualPart} (${tests} test session${tests === 1 ? "" : "s"}) · ${date}`;
+ }
+
for (let w = 0; w < weekCount; w++) {
const week: HeatCell[] = [];
for (let d = 0; d < 7; d++) {
@@ -846,11 +937,9 @@ const sessionHeat = computed(() => {
day: "numeric",
year: "numeric",
});
- const n = cell.sessions;
let title = "";
if (cell.inRange) {
- title =
- n === 0 ? `No sessions · ${date}` : `${n} session${n === 1 ? "" : "s"} · ${date}`;
+ title = heatTitle(cell.sessions, cell.tests, date);
} else if (cell.inYear) {
title = date;
}
@@ -859,7 +948,7 @@ const sessionHeat = computed(() => {
level,
inRange: cell.inRange,
inYear: cell.inYear,
- count: n,
+ count: testMode ? cell.tests : cell.sessions,
title,
});
}
@@ -876,15 +965,18 @@ const sessionHeat = computed(() => {
}
const inYearCells = raw.filter((c) => c.inYear && c.inRange);
- const activeDays = inYearCells.filter((c) => c.sessions > 0).length;
+ const activeDays = inYearCells.filter((c) =>
+ testMode ? c.tests > 0 : c.sessions > 0,
+ ).length;
- // Year totals for the header — stable on day click (list filters separately)
+ // Year totals for the header — match the active list filter (actual vs test).
let msgs = 0;
let tokensIn = 0;
let out = 0;
const yearStart = `${year}-01-01`;
const yearEnd = `${year}-12-31`;
- for (const s of filteredSessions.value) {
+ for (const s of heatBaseSessions.value) {
+ if (testMode ? !isPilotSession(s) : isPilotSession(s)) continue;
const r = sessionDayRange(s);
if (!r || r.end < yearStart || r.start > yearEnd) continue;
msgs += s.messageCount ?? 0;
@@ -1233,6 +1325,16 @@ async function focusSession(s: SessionRef): Promise {
.sess-time {
width: 64px;
}
+.sess-pilot-mark {
+ display: inline-block;
+ margin-right: 6px;
+ color: var(--text-faint);
+ font-size: var(--fs-2xs);
+}
+.sess-row:hover .sess-pilot-mark,
+.sess-row.picked .sess-pilot-mark {
+ color: var(--text-dim);
+}
.compare-hint {
margin: 6px 0 0;
font-size: var(--fs-2xs);
diff --git a/packages/web/src/views/dashboard/SettingsView.vue b/packages/web/src/views/dashboard/SettingsView.vue
index ce83133..ee5e027 100644
--- a/packages/web/src/views/dashboard/SettingsView.vue
+++ b/packages/web/src/views/dashboard/SettingsView.vue
@@ -207,6 +207,44 @@
+
providers
+
+
+
harness extras
+
+ post-answer extras
+
+
+ off
+
+
+ on
+
+
+
+
+ Provider post-answer work that holds the CLI open: Muse reminder subagents,
+ Claude slash skills, Grok subagents, Antigravity slash skills.
+ off (default) skips them for faster workflow nodes. Flipable on the
+ canvas HUD while a run is live, or per agent node. Stored as
+ harnessExtras in settings.json.
+
+
+
+
+
costs
@@ -341,6 +379,7 @@ import { vColResize } from "@/lib/colResize";
import { useSettingsStore } from "@/stores/settings";
import SettingsCustomNodes from "./settings/SettingsCustomNodes.vue";
import SettingsMcp from "./settings/SettingsMcp.vue";
+import SettingsPilot from "./settings/SettingsPilot.vue";
import "./chrome.css";
interface InternalItem {
@@ -424,6 +463,10 @@ async function setShowExamples(on: boolean): Promise {
await saveSettings();
}
+async function setHarnessExtras(on: boolean): Promise {
+ await settings.setHarnessExtras(on);
+}
+
async function setNotifyEnabled(on: boolean): Promise {
if (settings.notifications.enabled === on) return;
settings.notifications.enabled = on;
diff --git a/packages/web/src/views/dashboard/settings/SettingsPilot.vue b/packages/web/src/views/dashboard/settings/SettingsPilot.vue
new file mode 100644
index 0000000..f061c3a
--- /dev/null
+++ b/packages/web/src/views/dashboard/settings/SettingsPilot.vue
@@ -0,0 +1,1118 @@
+
+
+
test pilot
+
+ Cheap smoke prompts against available providers (harness extras off by
+ default). ignore local md runs in an empty workspace so AGENTS.md /
+ rules do not inflate tokens. Turn on extra tests for permission /
+ sandbox / mode variants and extras:off · extras:on pairs. Click a row for
+ diagram · metrics · logs. Prompt:
+ {{ plan?.prompt ?? "…" }}.
+ Tok / baseline when usage is available (Claude, Cursor, Antigravity,
+ Muse, plus Codex / Copilot / Grok / OpenCode when the CLI or session
+ store reports it). Grok / OpenCode ignore-md is bare cwd only (no skip
+ flag).
+