feat(FN-3463): refine ScriptsModal responsive token usage

Refines responsive CSS token usage in the ScriptsModal component by swapping hardcoded values for design token variables.

Fusion-Task-Id: FN-3463
This commit is contained in:
Fusion
2026-05-07 05:33:26 -07:00
committed by gsxdsm
parent 70105b2481
commit 957b8b4efe
15 changed files with 717 additions and 73 deletions

View File

@@ -2825,4 +2825,22 @@ describe("AgentStore", () => {
}
});
});
it("exports and applies agent and run snapshots", async () => {
const agent = await store.createAgent({ name: "Snapshot Agent", role: "executor" });
await store.setLastBlockedState(agent.id, { taskId: "FN-1", blockedBy: "dep", recordedAt: new Date().toISOString(), contextHash: "h" });
const agentSnapshot = await store.getAgentSnapshot();
const runSnapshot = store.getAgentRunSnapshot();
const applyAgent = await store.applyAgentSnapshot(agentSnapshot);
const applyRun = await store.applyAgentRunSnapshot(runSnapshot);
const agentSnapshot2 = await store.getAgentSnapshot();
const runSnapshot2 = store.getAgentRunSnapshot();
expect(applyAgent.appliedAgents).toBeGreaterThan(0);
expect(agentSnapshot2.payload).toEqual(agentSnapshot.payload);
expect(runSnapshot2.payload).toEqual(runSnapshot.payload);
expect(applyRun.applied + applyRun.skipped).toBeGreaterThanOrEqual(0);
});
});

View File

@@ -2758,4 +2758,23 @@ describe("CentralCore", () => {
rmSync(tempDir + "-v5-migrate", { recursive: true, force: true });
});
});
it("exports and applies settings/auth snapshots", async () => {
const syncCentral = new CentralCore(tempDir + "-snapshot");
await syncCentral.init();
try {
const legacy = await syncCentral.getSettingsForSync({});
const snapshot = await syncCentral.getProjectSettingsSnapshot({});
const result = await syncCentral.applyProjectSettingsSnapshot(snapshot);
const authSnapshot = syncCentral.getAuthMaterialSnapshot({ foo: { providerId: "foo", accountLabel: "acct" } as any });
expect(snapshot.payload.global).toEqual(legacy.global);
expect(snapshot.payload.projects).toEqual(legacy.projects);
expect(typeof result.success).toBe("boolean");
expect(syncCentral.applyAuthMaterialSnapshot(authSnapshot).foo.providerId).toBe("foo");
} finally {
await syncCentral.close();
rmSync(tempDir + "-snapshot", { recursive: true, force: true });
}
});
});

View File

@@ -3035,6 +3035,20 @@ describe("MissionStore", () => {
store.off("validator-run:completed", eventListener);
});
});
it("exports and applies mission hierarchy snapshots", () => {
const mission = store.createMission({ title: "Snapshot Mission" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
store.addFeature(slice.id, { title: "F" });
const snapshot = store.getMissionHierarchySnapshot();
const result = store.applyMissionHierarchySnapshot(snapshot);
const snapshot2 = store.getMissionHierarchySnapshot();
expect(result.applied).toBeGreaterThan(0);
expect(snapshot2.payload).toEqual(snapshot.payload);
});
});
// vi import for vitest mocking

View File

@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import {
SHARED_STATE_SNAPSHOT_VERSION,
computeSnapshotChecksum,
createActivityLogSnapshot,
createAgentRunSnapshot,
createAgentSnapshot,
createAuthMaterialSnapshot,
createMissionHierarchySnapshot,
createProjectSettingsSnapshot,
createRunAuditSnapshot,
createTaskMetadataSnapshot,
validateSnapshotEnvelope,
type AgentSnapshot,
type MissionHierarchySnapshot,
type TaskMetadataRecord,
} from "../shared-mesh-state.js";
describe("shared-mesh-state", () => {
const exportedAt = "2026-05-04T00:00:00.000Z";
it("computes stable checksums", () => {
const snapshot = createActivityLogSnapshot([{ id: "a1", timestamp: exportedAt, type: "task:created", details: "x" }], exportedAt);
const checksum = computeSnapshotChecksum({
version: snapshot.version,
exportedAt: snapshot.exportedAt,
payload: snapshot.payload,
});
expect(checksum).toBe(snapshot.checksum);
});
it("rejects version mismatch", () => {
const snapshot = createRunAuditSnapshot([], exportedAt);
expect(() => validateSnapshotEnvelope({ ...snapshot, version: 999 }, SHARED_STATE_SNAPSHOT_VERSION)).toThrow(
"Unsupported shared-state snapshot version",
);
});
it("rejects checksum mismatch", () => {
const snapshot = createRunAuditSnapshot([], exportedAt);
expect(() => validateSnapshotEnvelope({ ...snapshot, checksum: "bad" })).toThrow("checksum mismatch");
});
it("supports happy-path round trips for all payload kinds", () => {
const task = { id: "FN-1", description: "d", column: "todo", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: exportedAt, updatedAt: exportedAt, worktree: "/tmp/x", sessionFile: "/tmp/y" } as unknown as TaskMetadataRecord;
const taskSnapshot = createTaskMetadataSnapshot([task as any], exportedAt);
expect((taskSnapshot.payload.tasks[0] as any).worktree).toBeUndefined();
const missionSnapshot: MissionHierarchySnapshot = createMissionHierarchySnapshot(
{ missions: [], milestones: [], slices: [], features: [], missionEvents: [], assertions: [], featureAssertionLinks: [] },
exportedAt,
);
const agentSnapshot: AgentSnapshot = createAgentSnapshot({ agents: [], blockedStates: [] }, exportedAt);
const runSnapshot = createAgentRunSnapshot([], exportedAt);
const activitySnapshot = createActivityLogSnapshot([], exportedAt);
const auditSnapshot = createRunAuditSnapshot([], exportedAt);
const settingsSnapshot = createProjectSettingsSnapshot({ global: {} }, exportedAt);
const authSnapshot = createAuthMaterialSnapshot({}, exportedAt);
for (const snapshot of [
taskSnapshot,
missionSnapshot,
agentSnapshot,
runSnapshot,
activitySnapshot,
auditSnapshot,
settingsSnapshot,
authSnapshot,
]) {
validateSnapshotEnvelope(snapshot);
const roundTrip = JSON.parse(JSON.stringify(snapshot));
expect(roundTrip).toEqual(snapshot);
validateSnapshotEnvelope(roundTrip);
}
});
});

View File

@@ -11573,4 +11573,35 @@ describe("RunMutationContext", () => {
expect(reloaded?.email).toBe("persist@example.com");
});
});
describe("shared mesh snapshots", () => {
it("exports and reapplies task/activity/audit snapshots deterministically", async () => {
const task = await store.createTask({ description: "snapshot task" });
await store.updateTask(task.id, { worktree: "/tmp/fn-worktree", executionStartBranch: "fn/base" });
await store.recordActivity({ type: "task:created", taskId: task.id, details: "created" });
const taskSnapshot = await store.getTaskMetadataSnapshot();
const activitySnapshot = await store.getActivityLogSnapshot();
const auditSnapshot = store.getRunAuditSnapshot();
const taskResult = await store.applyTaskMetadataSnapshot(taskSnapshot);
const activityResult = store.applyActivityLogSnapshot(activitySnapshot);
const auditResult = store.applyRunAuditSnapshot(auditSnapshot);
const taskSnapshot2 = await store.getTaskMetadataSnapshot();
const activitySnapshot2 = await store.getActivityLogSnapshot();
const auditSnapshot2 = store.getRunAuditSnapshot();
expect(taskResult.applied + taskResult.skipped).toBeGreaterThan(0);
expect(taskSnapshot2.payload).toEqual(taskSnapshot.payload);
expect(activitySnapshot2.payload).toEqual(activitySnapshot.payload);
expect(auditSnapshot2.payload).toEqual(auditSnapshot.payload);
expect(activityResult.skipped).toBeGreaterThanOrEqual(1);
expect(auditResult.skipped).toBeGreaterThanOrEqual(0);
const persisted = await store.getTask(task.id);
expect(persisted?.worktree).toBe("/tmp/fn-worktree");
expect(persisted?.executionStartBranch).toBe("fn/base");
});
});
});

View File

@@ -55,6 +55,7 @@ import type { RunMutationContext } from "./types.js";
import type { TaskStore } from "./store.js";
import { computeAccessState } from "./agent-permissions.js";
import { Database } from "./db.js";
import { createAgentRunSnapshot, createAgentSnapshot, validateSnapshotEnvelope, type AgentRunSnapshot, type AgentSnapshot } from "./shared-mesh-state.js";
/** Database row shape returned by SELECT on agentRatings. */
interface AgentRatingRow {
@@ -2125,6 +2126,81 @@ export class AgentStore extends EventEmitter {
});
}
getAgentSnapshot(): Promise<AgentSnapshot> {
return (async () => {
const agents = await this.listAgents({ includeEphemeral: true });
const blockedRows = this.db.prepare("SELECT agentId, data FROM agentBlockedStates ORDER BY updatedAt ASC").all() as Array<{ agentId: string; data: string }>;
const blockedStates = blockedRows
.map((row) => ({ agentId: row.agentId, state: this.parseJson<BlockedStateSnapshot | null>(row.data, null) }))
.filter((row): row is { agentId: string; state: BlockedStateSnapshot } => row.state !== null);
return createAgentSnapshot({ agents, blockedStates });
})();
}
async applyAgentSnapshot(snapshot: AgentSnapshot): Promise<{ appliedAgents: number; appliedBlockedStates: number }> {
validateSnapshotEnvelope(snapshot);
let appliedAgents = 0;
let appliedBlockedStates = 0;
for (const agent of snapshot.payload.agents) {
this.db.prepare(`INSERT INTO agents (id, name, role, state, taskId, createdAt, updatedAt, lastHeartbeatAt, metadata, data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name=excluded.name, role=excluded.role, state=excluded.state, taskId=excluded.taskId, updatedAt=excluded.updatedAt,
lastHeartbeatAt=excluded.lastHeartbeatAt, metadata=excluded.metadata, data=excluded.data`)
.run(
agent.id,
agent.name,
agent.role,
agent.state,
agent.taskId ?? null,
agent.createdAt,
agent.updatedAt,
agent.lastHeartbeatAt ?? null,
JSON.stringify(agent.metadata ?? {}),
JSON.stringify(agent),
);
appliedAgents++;
}
for (const blocked of snapshot.payload.blockedStates) {
this.db.prepare(`INSERT INTO agentBlockedStates (agentId, data, updatedAt)
VALUES (?, ?, ?)
ON CONFLICT(agentId) DO UPDATE SET data=excluded.data, updatedAt=excluded.updatedAt`)
.run(blocked.agentId, JSON.stringify(blocked.state), blocked.state.recordedAt);
appliedBlockedStates++;
}
this.db.bumpLastModified();
return { appliedAgents, appliedBlockedStates };
}
getAgentRunSnapshot(): AgentRunSnapshot {
const runs = this.db.prepare("SELECT data FROM agentRuns ORDER BY startedAt ASC").all() as Array<{ data: string }>;
const parsed = runs
.map((row) => this.parseJson<AgentHeartbeatRun | null>(row.data, null))
.filter((run): run is AgentHeartbeatRun => run !== null);
return createAgentRunSnapshot(parsed);
}
async applyAgentRunSnapshot(snapshot: AgentRunSnapshot): Promise<{ applied: number; skipped: number }> {
validateSnapshotEnvelope(snapshot);
let applied = 0;
let skipped = 0;
for (const run of snapshot.payload.runs) {
const exists = this.db.prepare("SELECT 1 FROM agentRuns WHERE id = ?").get(run.id);
if (exists) {
skipped++;
continue;
}
await this.saveRun(run);
applied++;
}
return { applied, skipped };
}
// ─────────────────────────────────────────────────────────────────────────
// Private helpers
// ─────────────────────────────────────────────────────────────────────────

View File

@@ -74,7 +74,7 @@ import { NodeConnection } from "./node-connection.js";
import { NodeDiscovery } from "./node-discovery.js";
import { collectSystemMetrics } from "./system-metrics.js";
import type { ConnectionOptions, ConnectionResult } from "./node-connection.js";
import { createAuthMaterialSnapshot, createProjectSettingsSnapshot, validateSnapshotEnvelope, type AuthMaterialSnapshot, type ProjectSettingsSnapshot } from "./shared-mesh-state.js";
// ── Event Types ───────────────────────────────────────────────────────────
export interface CentralCoreEvents {
@@ -2850,6 +2850,41 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
};
}
getProjectSettingsSnapshot(globalSettings: GlobalSettings): Promise<ProjectSettingsSnapshot> {
return (async () => {
const payload = await this.getSettingsForSync(globalSettings);
return createProjectSettingsSnapshot({
global: payload.global ?? {},
projects: payload.projects,
}, payload.exportedAt);
})();
}
async applyProjectSettingsSnapshot(snapshot: ProjectSettingsSnapshot): Promise<SettingsSyncResult> {
validateSnapshotEnvelope(snapshot);
const payloadWithoutChecksum: Omit<SettingsSyncPayload, "checksum"> = {
version: 1,
exportedAt: snapshot.exportedAt,
global: snapshot.payload.global,
projects: snapshot.payload.projects,
providerAuth: undefined,
};
const checksum = createHash("sha256")
.update(JSON.stringify(payloadWithoutChecksum))
.digest("hex");
return this.applyRemoteSettings({ ...payloadWithoutChecksum, checksum });
}
getAuthMaterialSnapshot(providerAuth?: Record<string, ProviderAuthEntry>): AuthMaterialSnapshot {
return createAuthMaterialSnapshot(providerAuth);
}
applyAuthMaterialSnapshot(snapshot: AuthMaterialSnapshot): Record<string, ProviderAuthEntry> {
validateSnapshotEnvelope(snapshot);
return { ...(snapshot.payload.providerAuth ?? {}) };
}
// ── Settings Sync API ─────────────────────────────────────────────────
/**

View File

@@ -1,8 +1,9 @@
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey, PROJECT_AUTH_ROLES } from "./types.js";
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey, PROJECT_AUTH_ROLES, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, ProjectAuthRole, ProjectAuthUser, ProjectAuthMembership, ProjectAuthProvider, ProjectAuthSession, ProjectAuthUserCreateInput, ProjectAuthMembershipCreateInput, ProjectAuthProviderCreateInput, ProjectAuthSessionCreateInput, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js";
export * from "./mesh-replication-protocol.js";
export * from "./mesh-task-replication.js";
export * from "./shared-mesh-state.js";
export {
BUILTIN_AGENT_PROMPTS,
resolveAgentPrompt,

View File

@@ -48,7 +48,11 @@ import type {
ValidatorRunStatus,
FeatureLoopState,
} from "./mission-types.js";
import {
createMissionHierarchySnapshot,
validateSnapshotEnvelope,
type MissionHierarchySnapshot,
} from "./shared-mesh-state.js";
// ── Constants ────────────────────────────────────────────────────────
/**
@@ -3263,6 +3267,106 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
private idSequence = 0;
getMissionHierarchySnapshot(): MissionHierarchySnapshot {
const missions = this.listMissions();
const milestones = missions.flatMap((mission) => this.listMilestones(mission.id));
const slices = milestones.flatMap((milestone) => this.listSlices(milestone.id));
const features = slices.flatMap((slice) => this.listFeatures(slice.id));
const missionEvents = missions.flatMap((mission) => this.getMissionEvents(mission.id, { limit: 10_000 }).events);
const assertions = milestones.flatMap((milestone) => this.listContractAssertions(milestone.id));
const featureAssertionLinks = this.db.prepare("SELECT featureId, assertionId, createdAt FROM mission_feature_assertions ORDER BY createdAt ASC").all() as Array<{ featureId: string; assertionId: string; createdAt: string }>;
return createMissionHierarchySnapshot({
missions,
milestones,
slices,
features,
missionEvents,
assertions,
featureAssertionLinks,
});
}
applyMissionHierarchySnapshot(snapshot: MissionHierarchySnapshot): { applied: number } {
validateSnapshotEnvelope(snapshot);
let applied = 0;
for (const mission of snapshot.payload.missions) {
this.db.prepare(`INSERT INTO missions (id, title, description, status, interviewState, autoAdvance, autopilotEnabled, autopilotState, lastAutopilotActivityAt, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
title=excluded.title, description=excluded.description, status=excluded.status, interviewState=excluded.interviewState,
autoAdvance=excluded.autoAdvance, autopilotEnabled=excluded.autopilotEnabled, autopilotState=excluded.autopilotState,
lastAutopilotActivityAt=excluded.lastAutopilotActivityAt, updatedAt=excluded.updatedAt`).run(
mission.id, mission.title, mission.description ?? null, mission.status, mission.interviewState, mission.autoAdvance ? 1 : 0,
mission.autopilotEnabled ? 1 : 0, mission.autopilotState, mission.lastAutopilotActivityAt ?? null, mission.createdAt, mission.updatedAt,
);
applied++;
}
for (const milestone of snapshot.payload.milestones) {
this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, description=excluded.description, status=excluded.status, orderIndex=excluded.orderIndex,
interviewState=excluded.interviewState, dependencies=excluded.dependencies, planningNotes=excluded.planningNotes, verification=excluded.verification,
validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
milestone.id, milestone.missionId, milestone.title, milestone.description ?? null, milestone.status, milestone.orderIndex,
milestone.interviewState, toJsonNullable(milestone.dependencies), milestone.planningNotes ?? null, milestone.verification ?? null,
milestone.validationState ?? null, milestone.createdAt, milestone.updatedAt,
);
applied++;
}
for (const slice of snapshot.payload.slices) {
this.db.prepare(`INSERT INTO slices (id, milestoneId, title, description, status, orderIndex, activatedAt, planState, planningNotes, verification, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, description=excluded.description, status=excluded.status, orderIndex=excluded.orderIndex,
activatedAt=excluded.activatedAt, planState=excluded.planState, planningNotes=excluded.planningNotes, verification=excluded.verification, updatedAt=excluded.updatedAt`).run(
slice.id, slice.milestoneId, slice.title, slice.description ?? null, slice.status, slice.orderIndex, slice.activatedAt ?? null,
slice.planState ?? null, slice.planningNotes ?? null, slice.verification ?? null, slice.createdAt, slice.updatedAt,
);
applied++;
}
for (const feature of snapshot.payload.features) {
this.db.prepare(`INSERT INTO mission_features (id, sliceId, taskId, title, description, acceptanceCriteria, status, createdAt, updatedAt, loopState, implementationAttemptCount, validatorAttemptCount, lastValidatorRunId, lastValidatorStatus, generatedFromFeatureId, generatedFromRunId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET taskId=excluded.taskId, title=excluded.title, description=excluded.description, acceptanceCriteria=excluded.acceptanceCriteria,
status=excluded.status, updatedAt=excluded.updatedAt, loopState=excluded.loopState, implementationAttemptCount=excluded.implementationAttemptCount,
validatorAttemptCount=excluded.validatorAttemptCount, lastValidatorRunId=excluded.lastValidatorRunId, lastValidatorStatus=excluded.lastValidatorStatus,
generatedFromFeatureId=excluded.generatedFromFeatureId, generatedFromRunId=excluded.generatedFromRunId`).run(
feature.id, feature.sliceId, feature.taskId ?? null, feature.title, feature.description ?? null, feature.acceptanceCriteria ?? null,
feature.status, feature.createdAt, feature.updatedAt, feature.loopState ?? null, feature.implementationAttemptCount ?? null,
feature.validatorAttemptCount ?? null, feature.lastValidatorRunId ?? null, feature.lastValidatorStatus ?? null,
feature.generatedFromFeatureId ?? null, feature.generatedFromRunId ?? null,
);
applied++;
}
for (const event of snapshot.payload.missionEvents) {
if (!event.id || !event.missionId) continue;
this.db.prepare(`INSERT OR IGNORE INTO mission_events (id, missionId, eventType, description, metadata, timestamp, seq)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run(event.id, event.missionId, event.eventType, event.description, toJsonNullable(event.metadata), event.timestamp, event.seq ?? null);
}
for (const assertion of snapshot.payload.assertions) {
if (!assertion.id || !assertion.milestoneId) continue;
this.db.prepare(`INSERT INTO mission_contract_assertions (id, milestoneId, title, assertion, status, orderIndex, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, assertion=excluded.assertion, status=excluded.status, orderIndex=excluded.orderIndex, updatedAt=excluded.updatedAt`)
.run(assertion.id, assertion.milestoneId, assertion.title, assertion.assertion, assertion.status, assertion.orderIndex, assertion.createdAt, assertion.updatedAt);
}
for (const link of snapshot.payload.featureAssertionLinks) {
if (!link.featureId || !link.assertionId) continue;
this.db.prepare(`INSERT OR IGNORE INTO mission_feature_assertions (featureId, assertionId, createdAt) VALUES (?, ?, ?)`)
.run(link.featureId, link.assertionId, link.createdAt);
}
return { applied };
}
private generateId(prefix: string): string {
const timestamp = Date.now().toString(36).toUpperCase();
this.idSequence += 1;

View File

@@ -0,0 +1,143 @@
import { createHash } from "node:crypto";
export { SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
import { SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
import type {
ActivityLogEntry,
Agent,
AgentHeartbeatRun,
BlockedStateSnapshot,
GlobalSettings,
ProjectSettings,
ProviderAuthEntry,
RunAuditEvent,
Task,
} from "./types.js";
import type {
FeatureAssertionLink,
Milestone,
Mission,
MissionContractAssertion,
MissionEvent,
MissionFeature,
Slice,
} from "./mission-types.js";
export interface SharedSnapshotEnvelope<TPayload> {
version: number;
exportedAt: string;
checksum: string;
payload: TPayload;
}
/** Excludes file/blob/runtime state: no PROMPT.md body, task documents, attachment bytes, or worktree/runtime handles. */
export type TaskMetadataRecord = Omit<Task, "worktree" | "executionStartBranch" | "sessionFile">;
export type TaskMetadataSnapshot = SharedSnapshotEnvelope<{ tasks: TaskMetadataRecord[] }>;
/** Excludes instruction-bundle file contents and other node-local runtime handles. */
export interface AgentBlockedStateRecord {
agentId: string;
state: BlockedStateSnapshot;
}
export type AgentSnapshot = SharedSnapshotEnvelope<{ agents: Agent[]; blockedStates: AgentBlockedStateRecord[] }>;
/** Excludes agent.log/run-log JSONL content; structured run rows only. */
export type AgentRunSnapshot = SharedSnapshotEnvelope<{ runs: AgentHeartbeatRun[] }>;
export type ActivityLogSnapshot = SharedSnapshotEnvelope<{ entries: ActivityLogEntry[] }>;
export type RunAuditSnapshot = SharedSnapshotEnvelope<{ entries: RunAuditEvent[] }>;
export type MissionHierarchySnapshot = SharedSnapshotEnvelope<{
missions: Mission[];
milestones: Milestone[];
slices: Slice[];
features: MissionFeature[];
missionEvents: MissionEvent[];
assertions: MissionContractAssertion[];
featureAssertionLinks: FeatureAssertionLink[];
}>;
export type ProjectSettingsSnapshot = SharedSnapshotEnvelope<{
global: GlobalSettings;
projects?: Record<string, ProjectSettings>;
}>;
export type AuthMaterialSnapshot = SharedSnapshotEnvelope<{
providerAuth?: Record<string, ProviderAuthEntry>;
}>;
export type SharedMeshStateSnapshot =
| TaskMetadataSnapshot
| MissionHierarchySnapshot
| AgentSnapshot
| AgentRunSnapshot
| ActivityLogSnapshot
| RunAuditSnapshot
| ProjectSettingsSnapshot
| AuthMaterialSnapshot;
function withChecksum<TPayload>(payload: TPayload, exportedAt?: string): SharedSnapshotEnvelope<TPayload> {
const withoutChecksum = {
version: SHARED_STATE_SNAPSHOT_VERSION,
exportedAt: exportedAt ?? new Date().toISOString(),
payload,
};
return {
...withoutChecksum,
checksum: createHash("sha256").update(JSON.stringify(withoutChecksum)).digest("hex"),
};
}
export function computeSnapshotChecksum(snapshotWithoutChecksum: Omit<SharedSnapshotEnvelope<unknown>, "checksum">): string {
return createHash("sha256").update(JSON.stringify(snapshotWithoutChecksum)).digest("hex");
}
export function validateSnapshotEnvelope(snapshot: SharedSnapshotEnvelope<unknown>, expectedVersion = SHARED_STATE_SNAPSHOT_VERSION): void {
if (snapshot.version !== expectedVersion) {
throw new Error(`Unsupported shared-state snapshot version: ${snapshot.version}`);
}
const expected = computeSnapshotChecksum({
version: snapshot.version,
exportedAt: snapshot.exportedAt,
payload: snapshot.payload,
});
if (snapshot.checksum !== expected) {
throw new Error("Shared-state snapshot checksum mismatch");
}
}
export function toTaskMetadataRecord(task: Task): TaskMetadataRecord {
const { worktree: _worktree, executionStartBranch: _executionStartBranch, sessionFile: _sessionFile, ...rest } = task;
return rest;
}
export function createTaskMetadataSnapshot(tasks: Task[], exportedAt?: string): TaskMetadataSnapshot {
return withChecksum({ tasks: tasks.map((task) => toTaskMetadataRecord(task)) }, exportedAt);
}
export function createMissionHierarchySnapshot(payload: MissionHierarchySnapshot["payload"], exportedAt?: string): MissionHierarchySnapshot {
return withChecksum(payload, exportedAt);
}
export function createAgentSnapshot(payload: AgentSnapshot["payload"], exportedAt?: string): AgentSnapshot {
return withChecksum(payload, exportedAt);
}
export function createAgentRunSnapshot(runs: AgentHeartbeatRun[], exportedAt?: string): AgentRunSnapshot {
return withChecksum({ runs }, exportedAt);
}
export function createActivityLogSnapshot(entries: ActivityLogEntry[], exportedAt?: string): ActivityLogSnapshot {
return withChecksum({ entries }, exportedAt);
}
export function createRunAuditSnapshot(entries: RunAuditEvent[], exportedAt?: string): RunAuditSnapshot {
return withChecksum({ entries }, exportedAt);
}
export function createProjectSettingsSnapshot(payload: ProjectSettingsSnapshot["payload"], exportedAt?: string): ProjectSettingsSnapshot {
return withChecksum(payload, exportedAt);
}
export function createAuthMaterialSnapshot(providerAuth: Record<string, ProviderAuthEntry> | undefined, exportedAt?: string): AuthMaterialSnapshot {
return withChecksum({ providerAuth }, exportedAt);
}

View File

@@ -4,6 +4,7 @@ import { mkdir, readdir, readFile, writeFile, rename, unlink } from "node:fs/pro
import { join } from "node:path";
import { existsSync, watch, type FSWatcher } from "node:fs";
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType, WorkflowStepTemplate } from "./types.js";
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalSettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
import { normalizeTaskPriority } from "./task-priority.js";
import { GlobalSettingsStore } from "./global-settings.js";
@@ -6906,6 +6907,108 @@ ${notificationsSection}`;
.run(treeSha, normalizedTest, normalizedBuild, recordedAt, taskId);
}
// ── Shared mesh state export/apply helpers ───────────────────────────────
async getTaskMetadataSnapshot(): Promise<TaskMetadataSnapshot> {
const tasks = await this.listTasks({ slim: false, includeArchived: true });
return createTaskMetadataSnapshot(tasks as unknown as TaskMetadataSnapshot["payload"]["tasks"]);
}
async applyTaskMetadataSnapshot(snapshot: TaskMetadataSnapshot): Promise<{ applied: number; skipped: number }> {
validateSnapshotEnvelope(snapshot);
const existingTasks = new Map((await this.listTasks({ slim: false, includeArchived: true })).map((task) => [task.id, task]));
let applied = 0;
let skipped = 0;
for (const incoming of snapshot.payload.tasks) {
const current = existingTasks.get(incoming.id);
const currentMetadata = current ? toTaskMetadataRecord(current) : undefined;
if (currentMetadata && JSON.stringify(currentMetadata) === JSON.stringify(incoming)) {
skipped++;
continue;
}
const toUpsert: Task = {
...(incoming as unknown as Task),
worktree: current?.worktree,
executionStartBranch: current?.executionStartBranch,
sessionFile: current?.sessionFile,
};
this.upsertTaskWithFtsRecovery(toUpsert);
applied++;
}
return { applied, skipped };
}
async getActivityLogSnapshot(limit = 10_000): Promise<ActivityLogSnapshot> {
const entries = await this.getActivityLog({ limit });
return createActivityLogSnapshot([...entries].reverse());
}
applyActivityLogSnapshot(snapshot: ActivityLogSnapshot): { applied: number; skipped: number } {
validateSnapshotEnvelope(snapshot);
let applied = 0;
let skipped = 0;
for (const entry of snapshot.payload.entries) {
const exists = this.db.prepare("SELECT 1 FROM activityLog WHERE id = ?").get(entry.id);
if (exists) {
skipped++;
continue;
}
this.db.prepare(
`INSERT INTO activityLog (id, timestamp, type, taskId, taskTitle, details, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run(
entry.id,
entry.timestamp,
entry.type,
entry.taskId ?? null,
entry.taskTitle ?? null,
entry.details,
entry.metadata ? JSON.stringify(entry.metadata) : null,
);
applied++;
}
return { applied, skipped };
}
getRunAuditSnapshot(filter: RunAuditEventFilter = {}): RunAuditSnapshot {
return createRunAuditSnapshot(this.getRunAuditEvents({ ...filter, limit: filter.limit ?? 10_000 }).reverse());
}
applyRunAuditSnapshot(snapshot: RunAuditSnapshot): { applied: number; skipped: number } {
validateSnapshotEnvelope(snapshot);
let applied = 0;
let skipped = 0;
for (const entry of snapshot.payload.entries) {
const exists = this.db.prepare("SELECT 1 FROM runAuditEvents WHERE id = ?").get(entry.id);
if (exists) {
skipped++;
continue;
}
this.db.prepare(`
INSERT INTO runAuditEvents (id, timestamp, taskId, agentId, runId, domain, mutationType, target, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
entry.id,
entry.timestamp,
entry.taskId ?? null,
entry.agentId,
entry.runId,
entry.domain,
entry.mutationType,
entry.target,
entry.metadata ? JSON.stringify(entry.metadata) : null,
);
applied++;
}
return { applied, skipped };
}
// ── Backward Compatibility (Multi-Project Support) ────────────────────────
}

View File

@@ -1170,6 +1170,9 @@ export interface MeshReplicatedTaskApplyResult {
applied: boolean;
}
/** Canonical version for shared-state snapshots exchanged across mesh nodes. */
export const SHARED_STATE_SNAPSHOT_VERSION = 1 as const;
export interface TodoList {
id: string;
projectId: string;

View File

@@ -438,7 +438,7 @@
padding: 0 4px;
border-radius: 8px;
background: var(--triage);
color: #fff;
color: var(--cta-text);
font-size: 10px;
font-weight: 700;
line-height: 16px;

View File

@@ -5,7 +5,7 @@
}
.scripts-modal-body {
padding: 16px;
padding: var(--space-lg);
max-height: 70vh;
overflow-y: auto;
}
@@ -94,10 +94,10 @@
.modal.automation-modal {
width: min(95vw, 720px);
max-width: 95vw;
min-width: 480px;
min-width: 0;
height: 80vh;
min-height: 480px;
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - 16px);
min-height: calc(var(--space-2xl) * 15);
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - var(--space-lg));
overflow: hidden;
resize: both;
}
@@ -154,19 +154,19 @@
.schedule-scope-toggle {
display: flex;
align-items: center;
gap: 2px;
gap: calc(var(--space-xs) / 2);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 2px;
padding: calc(var(--space-xs) / 2);
width: fit-content;
}
.schedule-scope-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-md);
font-size: 12px;
font-weight: 500;
border: none;
@@ -174,7 +174,7 @@
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: background-color 0.15s, color 0.15s;
transition: background-color var(--transition-fast), color var(--transition-fast);
}
.schedule-scope-btn:hover:not(:disabled) {
@@ -184,7 +184,7 @@
.schedule-scope-btn.active {
background: var(--todo);
color: white;
color: var(--cta-text);
}
.schedule-scope-btn:disabled {
@@ -201,19 +201,19 @@
.routine-scope-toggle {
display: flex;
align-items: center;
gap: 2px;
gap: calc(var(--space-xs) / 2);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 2px;
padding: calc(var(--space-xs) / 2);
width: fit-content;
}
.routine-scope-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-md);
font-size: 12px;
font-weight: 500;
border: none;
@@ -221,7 +221,7 @@
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: background-color 0.15s, color 0.15s;
transition: background-color var(--transition-fast), color var(--transition-fast);
}
.routine-scope-btn:hover:not(:disabled) {
@@ -231,7 +231,7 @@
.routine-scope-btn.active {
background: var(--todo);
color: white;
color: var(--cta-text);
}
.routine-scope-btn:disabled {
@@ -250,7 +250,7 @@
align-items: center;
justify-content: center;
gap: var(--space-md);
padding: 48px 24px;
padding: calc(var(--space-2xl) * 1.5) var(--space-xl);
color: var(--text-muted);
text-align: center;
}
@@ -269,7 +269,7 @@
.schedule-empty-state .btn {
display: inline-flex;
align-items: center;
gap: 6px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
margin-top: var(--space-sm);
}
@@ -282,9 +282,9 @@
.schedule-card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 14px 16px;
padding: calc(var(--space-md) + var(--space-xs) / 2) var(--space-lg);
background: var(--card-bg);
transition: border-color 0.15s;
transition: border-color var(--transition-fast);
}
.schedule-card:hover {
@@ -373,7 +373,7 @@
flex-wrap: wrap;
align-items: center;
gap: var(--space-md);
margin-top: 10px;
margin-top: var(--space-md);
font-size: 12px;
color: var(--text-muted);
}
@@ -421,7 +421,7 @@
/* Run History */
.schedule-card-history {
margin-top: 10px;
margin-top: var(--space-md);
border-top: 1px solid var(--border);
padding-top: 8px;
}
@@ -429,7 +429,7 @@
.schedule-history-toggle {
display: flex;
align-items: center;
gap: 6px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
font-size: 12px;
color: var(--text-muted);
background: none;
@@ -578,7 +578,7 @@
.schedule-step-result {
display: flex;
align-items: center;
gap: 6px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
font-size: 11px;
}
@@ -659,7 +659,7 @@
.steps-list {
display: flex;
flex-direction: column;
gap: 6px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
margin-bottom: var(--space-sm);
}
@@ -673,7 +673,7 @@
display: flex;
align-items: center;
gap: var(--space-sm);
padding: 8px 10px;
padding: var(--space-sm) var(--space-md);
}
.step-card-drag {
@@ -707,7 +707,7 @@
.step-card-actions {
display: flex;
gap: 2px;
gap: calc(var(--space-xs) / 2);
align-items: center;
}
@@ -757,7 +757,7 @@
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
transition: background 0.15s, color 0.15s;
transition: background var(--transition-fast), color var(--transition-fast);
}
.schedule-mode-btn:hover {
@@ -766,7 +766,7 @@
.schedule-mode-btn.active {
background: var(--ws-pre-merge);
color: white;
color: var(--cta-text);
}
.form-group-row {
@@ -784,7 +784,7 @@
}
.schedule-form .modal-actions {
padding: 16px 0 0;
padding: var(--space-lg) 0 0;
border-top: 1px solid var(--border);
margin-top: var(--space-lg);
position: sticky;
@@ -857,7 +857,7 @@
align-items: center;
justify-content: center;
gap: var(--space-md);
padding: 48px 24px;
padding: calc(var(--space-2xl) * 1.5) var(--space-xl);
color: var(--text-muted);
text-align: center;
}
@@ -876,7 +876,7 @@
.routine-empty-state .btn {
display: inline-flex;
align-items: center;
gap: 6px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
margin-top: var(--space-sm);
}
@@ -889,7 +889,7 @@
.routine-card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 14px 16px;
padding: calc(var(--space-md) + var(--space-xs) / 2) var(--space-lg);
background: var(--card);
transition: border-color var(--transition-fast);
}
@@ -940,7 +940,7 @@
.routine-trigger-badge {
display: inline-flex;
align-items: center;
gap: 4px;
gap: var(--space-xs);
font-size: 11px;
font-weight: 500;
padding: 1px 7px;
@@ -993,7 +993,7 @@
flex-wrap: wrap;
align-items: center;
gap: var(--space-md);
margin-top: 10px;
margin-top: var(--space-md);
font-size: 12px;
color: var(--text-muted);
}
@@ -1058,7 +1058,7 @@
}
.routine-card-history {
margin-top: 10px;
margin-top: var(--space-md);
border-top: 1px solid var(--border);
padding-top: 8px;
}
@@ -1078,7 +1078,7 @@
}
.routine-form .modal-actions {
padding: 16px 0 0;
padding: var(--space-lg) 0 0;
border-top: 1px solid var(--border);
margin-top: var(--space-lg);
position: sticky;
@@ -1095,7 +1095,7 @@
.routine-trigger-btn {
display: inline-flex;
align-items: center;
gap: 6px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
@@ -1105,9 +1105,9 @@
font-weight: 500;
cursor: pointer;
transition:
border-color 0.15s,
background 0.15s,
color 0.15s;
border-color var(--transition-fast),
background var(--transition-fast),
color var(--transition-fast);
}
.routine-trigger-btn:hover {
@@ -1193,7 +1193,7 @@
.activity-log-title {
display: flex;
align-items: center;
gap: 10px;
gap: var(--space-md);
font-size: 16px;
font-weight: 600;
color: var(--text);
@@ -1208,8 +1208,8 @@
.activity-log-filter {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
gap: calc(var(--space-sm) - var(--space-xs) / 2);
padding: calc(var(--space-sm) - var(--space-xs) / 2) var(--space-md);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
@@ -1249,7 +1249,7 @@
.activity-log-content {
flex: 1;
overflow-y: auto;
padding: var(--space-lg) 20px;
padding: var(--space-lg) var(--space-xl);
min-height: 300px;
}
@@ -1258,7 +1258,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
padding: calc(var(--space-2xl) * 2 - var(--space-xs)) var(--space-xl);
color: var(--text-muted);
text-align: center;
}
@@ -1449,7 +1449,7 @@
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
padding: var(--space-xl);
}
.activity-log-loading .spin {
@@ -1470,7 +1470,7 @@
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) 20px;
padding: var(--space-sm) var(--space-xl);
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
flex-wrap: wrap;
@@ -1485,7 +1485,7 @@
.activity-log-filter-badge {
display: inline-flex;
align-items: center;
gap: 4px;
gap: var(--space-xs);
padding: 2px 8px;
background: var(--card);
border: 1px solid var(--border);
@@ -1498,7 +1498,7 @@
.activity-log-clear-filters {
display: inline-flex;
align-items: center;
gap: 4px;
gap: var(--space-xs);
padding: 2px 8px;
margin-left: auto;
background: none;
@@ -1546,14 +1546,14 @@
}
.activity-log-confirm-dialog h3 {
margin: 0 0 8px;
margin: 0 0 var(--space-sm);
font-size: 16px;
font-weight: 600;
color: var(--text);
}
.activity-log-confirm-dialog p {
margin: 0 0 20px;
margin: 0 0 var(--space-xl);
font-size: 13px;
color: var(--text-muted);
}
@@ -1566,7 +1566,7 @@
.activity-log-confirm-cancel,
.activity-log-confirm-clear {
padding: 10px 20px;
padding: var(--space-md) var(--space-xl);
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
@@ -1644,8 +1644,8 @@
/* Action buttons stay inline but shrink */
.activity-log-refresh,
.activity-log-clear {
width: 28px;
height: 28px;
width: 44px;
height: 44px;
flex-shrink: 0;
}
@@ -1723,10 +1723,10 @@
.modal.gm-modal {
width: min(95vw, 1400px);
max-width: 95vw;
min-width: 480px;
min-width: 0;
height: 92vh;
min-height: 480px;
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - 16px);
min-height: calc(var(--space-2xl) * 15);
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - var(--space-lg));
display: flex;
flex-direction: column;
overflow: hidden;
@@ -1931,7 +1931,7 @@
.gm-ahead {
display: inline-flex;
align-items: center;
gap: 2px;
gap: calc(var(--space-xs) / 2);
color: var(--color-success);
font-size: 13px;
font-weight: 500;
@@ -1940,7 +1940,7 @@
.gm-behind {
display: inline-flex;
align-items: center;
gap: 2px;
gap: calc(var(--space-xs) / 2);
color: var(--color-error);
font-size: 13px;
font-weight: 500;
@@ -2514,7 +2514,7 @@
.gm-commit-header {
display: flex;
flex-direction: column;
gap: 2px;
gap: calc(var(--space-xs) / 2);
width: 100%;
background: none;
border: none;
@@ -2610,7 +2610,7 @@
.gm-create-form select {
flex: 1;
min-width: 0;
padding: 6px 10px;
padding: calc(var(--space-sm) - var(--space-xs) / 2) var(--space-md);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
@@ -2755,7 +2755,7 @@
.gm-branch-commit-row {
display: flex;
flex-direction: column;
gap: 2px;
gap: calc(var(--space-xs) / 2);
width: 100%;
padding: var(--space-xs) var(--space-sm);
background: none;
@@ -2894,7 +2894,7 @@
.gm-stash-info {
display: flex;
flex-direction: column;
gap: 2px;
gap: calc(var(--space-xs) / 2);
min-width: 0;
flex: 1;
}
@@ -2925,7 +2925,7 @@
.gm-stash-branch {
display: inline-flex;
align-items: center;
gap: 2px;
gap: calc(var(--space-xs) / 2);
}
.gm-stash-actions {
@@ -3298,7 +3298,7 @@
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
gap: calc(var(--space-xs) / 2);
}
.gm-commit-compact-info .gm-commit-message {
@@ -3527,7 +3527,7 @@
.gm-nav-item {
flex-direction: column;
gap: 2px;
gap: calc(var(--space-xs) / 2);
padding: var(--space-xs) var(--space-sm);
border-left: none;
border-bottom: 2px solid transparent;
@@ -3786,7 +3786,7 @@
display: flex;
align-items: center;
gap: var(--space-sm);
padding: 10px 12px;
padding: var(--space-md) var(--space-lg);
background: none;
border: none;
width: 100%;
@@ -3794,7 +3794,7 @@
cursor: pointer;
color: var(--text);
font-size: 13px;
transition: background 0.15s;
transition: background var(--transition-fast);
}
.changes-file-header:hover {