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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
77
packages/core/src/__tests__/shared-mesh-state.test.ts
Normal file
77
packages/core/src/__tests__/shared-mesh-state.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
143
packages/core/src/shared-mesh-state.ts
Normal file
143
packages/core/src/shared-mesh-state.ts
Normal 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);
|
||||
}
|
||||
@@ -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) ────────────────────────
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user