Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a2762da
fix(auth): bind GitHub linking to the initiating browser
kerdofficial Oct 9, 2026
9e5b02e
fix(server): keep service state and credentials private
kerdofficial Oct 9, 2026
8f4a676
fix(site): verify font downloads and isolate deployment
kerdofficial Oct 9, 2026
70bf908
fix(github): enforce approval for private fork workflows
kerdofficial Oct 9, 2026
e803fdf
fix(runner): isolate guest networks and SSH identities
kerdofficial Oct 9, 2026
5aaa095
fix(ci): keep PR metadata jobs off Vectis runners
kerdofficial Oct 9, 2026
98b7e2b
fix(ci): use an available Xcode runner for native checks
kerdofficial Oct 9, 2026
cce0d00
test(ci): validate Vectis Ubuntu and macOS environments
kerdofficial Oct 10, 2026
18af643
test(ci): validate Vectis Windows ARM64 environment
kerdofficial Oct 10, 2026
e1d844d
fix(windows): install MinGit outside the standard Git path
kerdofficial Oct 10, 2026
440c9a3
fix(windows): install MinGit outside the standard Git path
kerdofficial Oct 10, 2026
13b8120
fix(runner): preserve exit evidence when VM cleanup fails
kerdofficial Oct 11, 2026
4e384d9
fix(windows): preserve bootstrap failure diagnostics
kerdofficial Oct 11, 2026
0ac3d4c
fix(runner): reconcile requests without cloud leases
kerdofficial Oct 11, 2026
5af0c49
chore(deps): bump @modelcontextprotocol/sdk from 1.30.0 to 1.32.1
dependabot[bot] Oct 14, 2026
7dfc506
feat(desktop): show runner lifecycle stages next to jobs
kerdofficial Oct 14, 2026
c91d8fc
feat(runner): release idle runners whose job no longer needs capacity
kerdofficial Oct 14, 2026
77010ac
docs(runner): explain idle runner drain
kerdofficial Oct 14, 2026
cebfb8b
fix(runner): decode runner busy state only for status reads
kerdofficial Oct 14, 2026
35ff99e
fix(cloud): expire runner requests that machines never claimed
kerdofficial Oct 14, 2026
4549860
docs(runner): explain unclaimed request expiry
kerdofficial Oct 14, 2026
331545c
fix(security): redact credentials from logs and operation details
kerdofficial Oct 14, 2026
491a3fc
perf(server): redact operations once when they are written
kerdofficial Oct 14, 2026
46ee583
test(ci): validate Ubuntu container jobs and services
kerdofficial Oct 14, 2026
da7f537
fix(design): pin extracted fonts instead of the repackaged archive
kerdofficial Oct 14, 2026
17ada9b
feat(repositories): enforce binding access policies
kerdofficial Oct 14, 2026
6d0792d
feat(runner): verify assignments with a job-started hook
kerdofficial Oct 14, 2026
95a401f
fix(runner): stop the runner when a job assignment is rejected
kerdofficial Oct 14, 2026
17ef64c
feat(desktop): choose who can start runners and approve jobs
kerdofficial Oct 14, 2026
c459bd6
fix(desktop): approve jobs on the environment they asked for
kerdofficial Oct 14, 2026
bfa0c31
fix(runner): keep the assignment hook within its own runner process tree
kerdofficial Oct 14, 2026
66210f8
feat(runner): add opt-in Rosetta for Linux guests
kerdofficial Oct 14, 2026
5f09fa4
test(runner): allow real process trees time on loaded runners
kerdofficial Oct 14, 2026
b6853e5
docs(linux): explain optional x86_64 tools
kerdofficial Oct 14, 2026
b4349b8
feat(environment): configure guest network egress
kerdofficial Oct 14, 2026
461bd96
docs(environment): explain egress support by guest OS
kerdofficial Oct 14, 2026
65594e9
feat(desktop): configure x86_64 tools and guest network access
kerdofficial Oct 14, 2026
9a7a60c
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/modelcon…
kerdofficial Oct 14, 2026
761120d
Merge remote-tracking branch 'origin/fix/overnight-windows-setup-2026…
kerdofficial Oct 14, 2026
8ad4808
Merge remote-tracking branch 'origin/feat/shared-redaction' into HEAD
kerdofficial Oct 14, 2026
f170e48
Merge commit '8ad4808ee8754ef9bce46e9990764ea1fbdf9283' into HEAD
kerdofficial Oct 14, 2026
5e5bdca
Merge commit 'f170e485ed1f85b740c23f5f82158c795350d629' into HEAD
kerdofficial Oct 14, 2026
289a54e
Merge commit '5e5bdcac906e8e467865b8ff7028f4f4e33325b3' into HEAD
kerdofficial Oct 14, 2026
b3eb20b
Merge commit '289a54e30d9f30be1ec2d9e8e688bf345ce3dcd4' into HEAD
kerdofficial Oct 14, 2026
0cbb179
Merge integration into feat/repository-access-policy
kerdofficial Oct 14, 2026
1474878
Merge integration into feat/rosetta-x64
kerdofficial Oct 14, 2026
be8008c
Merge integration into feat/environment-egress
kerdofficial Oct 14, 2026
72a8331
Merge integration into vectis/environment-validation-20261010
kerdofficial Oct 14, 2026
27842c1
Merge integration into test/validate-container-services
kerdofficial Oct 14, 2026
8cf7f2f
Merge integration into feat/runner-stage-labels
kerdofficial Oct 14, 2026
d4d473a
Merge main into feat/runner-stage-labels
kerdofficial Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions apps/desktop/src/renderer/components/activity-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import type { Activity, Operation } from "../../../../../packages/protocol/src/i
import { GitHubIcon } from "@/components/github-icon";
import { Details, ExpandableRow, Mono } from "@/components/layout";
import { OsTile } from "@/components/os-icon";
import { JobStatus, OperationStatus, StatusText } from "@/components/status";
import { JobStatus, OperationStatus, RunnerStage, StatusText } from "@/components/status";
import { Button } from "@/components/ui/button";
import { activityJob, activitySummary, activityTitle } from "@/lib/activities";
import { formatDateTime, formatRelative } from "@/lib/format";
import { isGeneratedStageMessage, activeRunnerStage } from "@/lib/runner-stage";
import { cancellableCommands, commandLabels } from "@/lib/operations";
import { MigrationResult } from "@/views/migration-result";
import { PreparationResult } from "@/views/preparation-result";
Expand Down Expand Up @@ -49,6 +50,7 @@ export function ActivityRow({
const reconcilable = activity.subject.type === "runner" && activity.status === "action_required";
const preparation = activity.subject.type === "preparation";
const closable = activity.status === "action_required" && !preparation && !reconcilable;
const stage = activeRunnerStage(activity, deciding, job?.status);
const result: unknown = deciding?.result;
const nextStep =
typeof result === "object" &&
Expand Down Expand Up @@ -86,13 +88,19 @@ export function ActivityRow({
{formatRelative(activity.updatedAt)}
</span>
{job?.status && <JobStatus status={job.status} conclusion={job.conclusion ?? null} />}
{!runnerQuiet && <OperationStatus status={activity.status} />}
{stage ? (
<RunnerStage stage={stage} />
) : (
!runnerQuiet && <OperationStatus status={activity.status} />
)}
</>
}
>
<p className="max-w-[72ch] text-foreground/85" data-selectable>
{activity.message}
</p>
{!(activity.subject.type === "runner" && isGeneratedStageMessage(activity.message)) && (
<p className="max-w-[72ch] text-foreground/85" data-selectable>
{activity.message}
</p>
)}
{nextStep && (
<p className="max-w-[72ch] text-muted-foreground" data-selectable>
{nextStep}
Expand Down Expand Up @@ -215,9 +223,11 @@ function Step({ step }: { step: Operation }) {
label={commandLabels[step.command] ?? step.command}
/>
<span className="text-muted-foreground tabular-nums">{formatRelative(step.updatedAt)}</span>
<span className="min-w-0 basis-full text-muted-foreground" data-selectable>
{step.message}
</span>
{!(step.command === "runner.run" && isGeneratedStageMessage(step.message)) && (
<span className="min-w-0 basis-full text-muted-foreground" data-selectable>
{step.message}
</span>
)}
</li>
);
}
6 changes: 6 additions & 0 deletions apps/desktop/src/renderer/components/status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ClockIcon,
TriangleAlertIcon,
} from "lucide-react";
import { runnerStagePresentation, type RunnerStageKey } from "@/lib/runner-stage";
import { cn } from "@/lib/utils";

export const tones = {
Expand Down Expand Up @@ -41,6 +42,11 @@ export function StatusBadge({ tone, label }: { tone: Tone; label: string }) {
);
}

export function RunnerStage({ stage }: { stage: RunnerStageKey }) {
const { tone, label } = runnerStagePresentation(stage);
return <StatusBadge tone={tone} label={label} />;
}

const operationStates = {
accepted: ["queued", "Accepted"],
running: ["running", "Running"],
Expand Down
69 changes: 69 additions & 0 deletions apps/desktop/src/renderer/lib/runner-stage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import type { Activity, Operation } from "../../../../../packages/protocol/src/index.js";
import {
activeRunnerStage,
findRunnerActivity,
isGeneratedStageMessage,
runnerStageFromResult,
runnerStagePresentation,
} from "./runner-stage";

const activity = (status: Activity["status"], jobId: number): Activity => ({
id: `activity-${jobId}`,
kind: "vectis",
subject: { type: "runner", requestedJob: { jobId } },
createdAt: "2026-01-01T00:00:00.000Z",
rootOperationId: `op-${jobId}`,
status,
message: "",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const operation = (jobId: number, result: unknown): Operation => ({
id: `op-${jobId}`,
key: `key-${jobId}`,
command: "runner.run",
status: "running",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
message: "",
result,
});

describe("runner stage", () => {
it("maps results to stages and hides finished", () => {
expect(runnerStageFromResult({ stage: "preparing_guest" })).toBe("preparing_guest");
expect(runnerStageFromResult({ stage: "finished" })).toBeUndefined();
expect(runnerStageFromResult({ stage: "finished", code: "runner_idle" })).toBe("runner_idle");
expect(runnerStageFromResult("nope")).toBeUndefined();
});

it("leaves a running job to GitHub's own status", () => {
expect(runnerStagePresentation("listening").label).toBe("Waiting for job");
const op = operation(1, { stage: "listening" });
expect(activeRunnerStage(activity("running", 1), op, "in_progress")).toBeUndefined();
expect(
findRunnerActivity({ jobId: 1, status: "in_progress" }, [activity("running", 1)], [op]),
).toBeUndefined();
});

it("only reports stages for active runners or released idle runners", () => {
const op = operation(1, { stage: "listening" });
expect(activeRunnerStage(activity("running", 1), op)).toBe("listening");
expect(activeRunnerStage(activity("succeeded", 1), op)).toBeUndefined();
const idle = operation(1, { stage: "finished", code: "runner_idle" });
expect(activeRunnerStage(activity("cancelled", 1), idle)).toBe("runner_idle");
});

it("links a job to the active runner that requested it", () => {
const activities = [activity("succeeded", 1), activity("running", 2)];
const operations = [operation(1, { stage: "listening" }), operation(2, { stage: "cleaning" })];
expect(findRunnerActivity({ jobId: 2 }, activities, operations)).toBe("cleaning");
expect(findRunnerActivity({ jobId: 1 }, activities, operations)).toBeUndefined();
expect(findRunnerActivity({ jobId: 3 }, activities, operations)).toBeUndefined();
});

it("recognizes only the generated stage message", () => {
expect(isGeneratedStageMessage("Runner: preparing_guest.")).toBe(true);
expect(isGeneratedStageMessage("Runner failed: boom.")).toBe(false);
});
});
66 changes: 66 additions & 0 deletions apps/desktop/src/renderer/lib/runner-stage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Activity, Operation } from "../../../../../packages/protocol/src/index.js";
import { Schema } from "effect";
import { RunnerProgress } from "../../../../../packages/protocol/src/runners.js";
import type { Tone } from "@/components/status";

export type RunnerStageKey = Exclude<RunnerProgress["stage"], "finished"> | "runner_idle";

const stages = {
starting: ["queued", "Starting VM"],
preparing_guest: ["running", "Preparing runner"],
registering: ["running", "Registering"],
listening: ["queued", "Waiting for job"],
cleaning: ["running", "Cleaning up"],
runner_idle: ["cancelled", "Released idle runner"],
} as const satisfies Record<RunnerStageKey, readonly [Tone, string]>;

export function runnerStagePresentation(stage: RunnerStageKey) {
const [tone, label] = stages[stage];
return { tone, label };
}

const StageResult = Schema.Struct({
stage: RunnerProgress.fields.stage,
code: Schema.optional(Schema.String),
});
const isStageResult = Schema.is(StageResult);

export function runnerStageFromResult(result: unknown): RunnerStageKey | undefined {
if (!isStageResult(result)) return undefined;
if (result.code === "runner_idle") return "runner_idle";
return result.stage === "finished" ? undefined : result.stage;
}

// The stage is only news while the runner works or when it handed back an idle runner.
export function activeRunnerStage(
activity: Activity,
root: Operation | undefined,
jobStatus?: string,
) {
if (activity.subject.type !== "runner" || !root) return undefined;
const stage = runnerStageFromResult(root.result);
if (stage === "runner_idle") return activity.status === "cancelled" ? stage : undefined;
// Once GitHub reports the job running, its own status says the same thing.
if (stage === "listening" && jobStatus === "in_progress") return undefined;
return activity.status === "accepted" || activity.status === "running" ? stage : undefined;
}

export function isGeneratedStageMessage(message: string) {
return /^Runner: [a-z_]+\.$/.test(message);
}

export function findRunnerActivity(
job: { jobId: number; status?: string },
activities: readonly Activity[],
operations: readonly Operation[],
) {
for (const activity of activities) {
if (activity.subject.type !== "runner") continue;
const linked = (activity.subject.actualJob ?? activity.subject.requestedJob)?.jobId;
if (linked !== job.jobId) continue;
const root = operations.find((operation) => operation.id === activity.rootOperationId);
const stage = activeRunnerStage(activity, root, job.status);
if (stage) return stage;
}
return undefined;
}
57 changes: 34 additions & 23 deletions apps/desktop/src/renderer/views/repository-jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { MachineRepositories } from "../../../../../packages/protocol/src/r
import { Jobs, type Job } from "../../../../../packages/protocol/src/jobs.js";
import { Notice } from "@/components/layout";
import { Reason } from "@/components/hint";
import { JobStatus, StatusBadge } from "@/components/status";
import { JobStatus, RunnerStage, StatusBadge } from "@/components/status";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand All @@ -25,6 +25,7 @@ import { Input } from "@/components/ui/input";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { formatRelative } from "@/lib/format";
import { approvalBinding, awaitingApproval, jobAccessPresentation } from "@/lib/repository-access";
import { findRunnerActivity } from "@/lib/runner-stage";
import { useStateApi } from "@/state";

const maxApprovalRepositories = 5;
Expand Down Expand Up @@ -155,28 +156,38 @@ export function RepositoryJobs({ bindingId }: { bindingId: string }) {
)}
{jobs && jobs.length > 0 && (
<div className="flex flex-col divide-y divide-border rounded-xl ring-1 ring-border ring-inset">
{jobs.map((job) => (
<div key={job.jobId} className="flex items-center gap-3 px-3.5 py-2.5">
<Tooltip>
<TooltipTrigger
render={<span tabIndex={0} />}
className="min-w-0 flex-1 truncate rounded outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
>
{job.name}
</TooltipTrigger>
<TooltipContent>
Run {job.runId}, job {job.jobId}
{job.runnerName ? `, runner ${job.runnerName}` : ""}
</TooltipContent>
</Tooltip>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{formatRelative(job.updatedAt)}
</span>
{job.access && job.status !== "completed" && <JobAccessStatus access={job.access} />}
{awaitingApproval(job) && <ApproveJobButton bindingId={bindingId} job={job} />}
<JobStatus status={job.status} conclusion={job.conclusion} />
</div>
))}
{jobs.map((job) => {
const stage = findRunnerActivity(
job,
snapshot?.activities ?? [],
snapshot?.operations ?? [],
);
return (
<div key={job.jobId} className="flex items-center gap-3 px-3.5 py-2.5">
<Tooltip>
<TooltipTrigger
render={<span tabIndex={0} />}
className="min-w-0 flex-1 truncate rounded outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
>
{job.name}
</TooltipTrigger>
<TooltipContent>
Run {job.runId}, job {job.jobId}
{job.runnerName ? `, runner ${job.runnerName}` : ""}
</TooltipContent>
</Tooltip>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{formatRelative(job.updatedAt)}
</span>
{job.access && job.status !== "completed" && (
<JobAccessStatus access={job.access} />
)}
{awaitingApproval(job) && <ApproveJobButton bindingId={bindingId} job={job} />}
{stage && <RunnerStage stage={stage} />}
<JobStatus status={job.status} conclusion={job.conclusion} />
</div>
);
})}
</div>
)}
<Dialog open={recovering} onOpenChange={setRecovering}>
Expand Down
Loading