feat: support multiple worker workdir attachments
This commit is contained in:
@@ -236,6 +236,12 @@ export type SubscriptionWorkerAvailability = "observed" | "unavailable";
|
||||
|
||||
export type EventSubscriptionSelector = { "topic": "runtime_workers" } | { "topic": "worker_lifecycle", worker_ids: SubscriptionWorkerIds, } | { "topic": "worker_protocol", worker_id: SubscriptionWorkerId, runtime_id?: string | null, } | { "topic": "workspace_workers" } | { "topic": "workspace_workdirs" };
|
||||
|
||||
export type SubscriptionWorkerWorkdirAttachment = {
|
||||
/**
|
||||
* Stable Worker-local routing alias.
|
||||
*/
|
||||
alias: string, repository_key?: string | null, working_directory_id: SubscriptionWorkdirId, };
|
||||
|
||||
export type SubscriptionWorker = { worker_id: SubscriptionWorkerId,
|
||||
/**
|
||||
* Set by the Workspace Server when projecting a Runtime-owned Worker to clients.
|
||||
@@ -264,14 +270,9 @@ worker_state?: WorkerStateSnapshot | null,
|
||||
/**
|
||||
* Runtime catalog lifecycle compatibility projection; not foreground-state authority.
|
||||
*/
|
||||
state: SubscriptionWorkerState, has_running_internal_workers: boolean, workspace_id?: string | null, display_name?: string | null, profile?: string | null,
|
||||
/**
|
||||
* Workspace-facing Repository key. Runtime producers leave this unset and
|
||||
* Workspace Server projections replace `repository_id` with this field.
|
||||
*/
|
||||
repository_key?: string | null, working_directory_id?: SubscriptionWorkdirId | null, };
|
||||
state: SubscriptionWorkerState, has_running_internal_workers: boolean, workspace_id?: string | null, display_name?: string | null, profile?: string | null, workdir_attachments?: Array<SubscriptionWorkerWorkdirAttachment>, };
|
||||
|
||||
export type WorkspaceSubscriptionWorkdir = { working_directory_id: SubscriptionWorkdirId, repository_key: string, state: string, primary_worker_id?: SubscriptionWorkerId | null, };
|
||||
export type WorkspaceSubscriptionWorkdir = { working_directory_id: SubscriptionWorkdirId, repository_key: string, state: string, };
|
||||
|
||||
export type SubscriptionSnapshot = { "topic": "workers", "data": { workers: Array<SubscriptionWorker>, } } | { "topic": "worker_protocol", "data": { worker_id: SubscriptionWorkerId, events: Array<Event>, } } | { "topic": "workspace_workdirs", "data": { workdirs: Array<WorkspaceSubscriptionWorkdir>, } };
|
||||
|
||||
|
||||
@@ -13,9 +13,17 @@ export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id
|
||||
|
||||
export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, };
|
||||
|
||||
export type WorkingDirectorySummary = { working_directory_id: string, repository_key: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, primary_worker_id?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
export type WorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, repository_key: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkingDirectoryCreateRequest = { runtime_id?: string | null, repository_key: string, selector?: string | null, operation_id?: string | null, };
|
||||
export type WorkingDirectoryCreateRequest = { runtime_id?: string | null,
|
||||
/**
|
||||
* Optional human-facing label, independent of every Worker attachment alias.
|
||||
*/
|
||||
display_name?: string | null, repository_key: string, selector?: string | null, operation_id?: string | null, };
|
||||
|
||||
export type WorkingDirectoryListResponse = { workspace_id: string, items: Array<WorkingDirectorySummary>, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
|
||||
@@ -15,11 +15,19 @@ export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id
|
||||
|
||||
export type RuntimeWorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_id: string, };
|
||||
|
||||
export type RuntimeWorkingDirectorySummary = { working_directory_id: string, repository_id: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, primary_worker_id?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
export type RuntimeWorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, repository_id: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: RuntimeWorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, };
|
||||
|
||||
export type WorkingDirectorySummary = { working_directory_id: string, repository_key: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, primary_worker_id?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
export type WorkingDirectorySummary = { working_directory_id: string,
|
||||
/**
|
||||
* Optional human-facing label, never a routing key.
|
||||
*/
|
||||
display_name?: string | null, repository_key: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||
|
||||
export type WorkerWorkspaceSummary = { visibility: string, identity: string, workspace_id?: string | null, };
|
||||
|
||||
@@ -27,7 +35,9 @@ export type WorkerImplementationSummary = { kind: string, display_hint: string,
|
||||
|
||||
export type WorkerCapabilitySummary = { can_stop: boolean, can_spawn_followup: boolean, };
|
||||
|
||||
export type WorkerLaunchWorkerSummary = { runtime_id: string, worker_id: string, host_id: string, display_name: string, label: string, profile: string | null, singleton_key: string | null, tags: Array<string>, workspace: WorkerWorkspaceSummary, state: string, last_seen_at: string | null, pinned: boolean, retention_state: string, implementation: WorkerImplementationSummary, capabilities: WorkerCapabilitySummary, working_directory?: RuntimeWorkingDirectorySummary | null, diagnostics: Array<Diagnostic>, };
|
||||
export type RuntimeWorkerWorkdirAttachmentSummary = { alias: string, working_directory: RuntimeWorkingDirectorySummary, };
|
||||
|
||||
export type WorkerLaunchWorkerSummary = { runtime_id: string, worker_id: string, host_id: string, display_name: string, label: string, profile: string | null, singleton_key: string | null, tags: Array<string>, workspace: WorkerWorkspaceSummary, state: string, last_seen_at: string | null, pinned: boolean, retention_state: string, implementation: WorkerImplementationSummary, capabilities: WorkerCapabilitySummary, workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
export type WorkerLaunchRuntimeOption = { runtime_id: string, display_name: string, built_in: boolean, worker_creation_available: boolean, working_directory_required: boolean, status: string, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
@@ -37,11 +47,15 @@ export type WorkingDirectoryRepositoryOption = { repository_key: string, default
|
||||
|
||||
export type WorkerLaunchOptionsResponse = { workspace_id: string, runtimes: Array<WorkerLaunchRuntimeOption>, default_profile: string | null, profiles: Array<WorkerLaunchProfileCandidate>, repositories: Array<WorkingDirectoryRepositoryOption>, working_directories: Array<WorkingDirectorySummary>, diagnostics: Array<Diagnostic>, };
|
||||
|
||||
export type BrowserWorkerWorkingDirectorySelection = { working_directory_id: string, relative_cwd: string | null, };
|
||||
export type BrowserWorkerWorkingDirectorySelection = {
|
||||
/**
|
||||
* Stable Worker-local routing alias.
|
||||
*/
|
||||
alias: string, working_directory_id: string, relative_cwd: string | null, };
|
||||
|
||||
export type CreateWorkspaceWorkerTicketAssignmentRequest = { ticket_id: string, operation_id: string, };
|
||||
|
||||
export type CreateWorkspaceWorkerRequest = { runtime_id: string, display_name: string, profile: string | null, ticket_assignment: CreateWorkspaceWorkerTicketAssignmentRequest | null, initial_submit: Array<Segment>, working_directory: BrowserWorkerWorkingDirectorySelection | null,
|
||||
export type CreateWorkspaceWorkerRequest = { runtime_id: string, display_name: string, profile: string | null, ticket_assignment: CreateWorkspaceWorkerTicketAssignmentRequest | null, initial_submit: Array<Segment>, workdir_attachments: Array<BrowserWorkerWorkingDirectorySelection>,
|
||||
/**
|
||||
* Backend idempotency key used only for authenticated Worker-owned spawn/control.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
|
||||
const SUMMARY_KEYS = new Set([
|
||||
"working_directory_id",
|
||||
"display_name",
|
||||
"repository_key",
|
||||
"creation_selector",
|
||||
"creation_ref",
|
||||
@@ -23,11 +24,11 @@ const SUMMARY_KEYS = new Set([
|
||||
"cleanup_target",
|
||||
"status",
|
||||
"cleanliness",
|
||||
"primary_worker_id",
|
||||
"occupied_by",
|
||||
]);
|
||||
const CREATE_REQUEST_KEYS = new Set([
|
||||
"runtime_id",
|
||||
"display_name",
|
||||
"repository_key",
|
||||
"selector",
|
||||
"operation_id",
|
||||
@@ -84,6 +85,7 @@ export function validateWorkingDirectoryCreateRequest(
|
||||
repository_key: stringField(record, "repository_key"),
|
||||
};
|
||||
assignOptionalString(request, record, "runtime_id");
|
||||
assignOptionalString(request, record, "display_name");
|
||||
assignOptionalString(request, record, "selector");
|
||||
assignOptionalString(request, record, "operation_id");
|
||||
return request;
|
||||
@@ -124,6 +126,7 @@ export function parseWorkingDirectorySummary(
|
||||
"unknown",
|
||||
]),
|
||||
};
|
||||
assignOptionalString(summary, record, "display_name");
|
||||
assignOptionalString(summary, record, "creation_selector");
|
||||
assignOptionalString(summary, record, "creation_ref");
|
||||
assignOptionalString(summary, record, "creation_tree");
|
||||
@@ -131,7 +134,6 @@ export function parseWorkingDirectorySummary(
|
||||
assignOptionalString(summary, record, "current_ref");
|
||||
assignOptionalString(summary, record, "current_tree");
|
||||
assignOptionalString(summary, record, "cleanliness");
|
||||
assignOptionalString(summary, record, "primary_worker_id");
|
||||
if (record.observed_at_epoch_seconds !== undefined) {
|
||||
const observedAt = record.observed_at_epoch_seconds;
|
||||
if (observedAt === null) {
|
||||
|
||||
@@ -139,10 +139,11 @@ Deno.test("Worker create request parser requires the complete shared request", (
|
||||
{ kind: "text", content: "Implement T-565." },
|
||||
{ kind: "flow", selector: "builtin:coder-review" },
|
||||
],
|
||||
working_directory: {
|
||||
workdir_attachments: [{
|
||||
alias: "source",
|
||||
working_directory_id: "workdir-a",
|
||||
relative_cwd: null,
|
||||
},
|
||||
}],
|
||||
control_operation_id: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
DiagnosticSeverity,
|
||||
RuntimeWorkingDirectoryCleanupTarget,
|
||||
RuntimeWorkingDirectorySummary,
|
||||
RuntimeWorkerWorkdirAttachmentSummary,
|
||||
WorkerCapabilitySummary,
|
||||
WorkerImplementationSummary,
|
||||
WorkerLaunchOptionsResponse,
|
||||
@@ -261,6 +262,7 @@ function runtimeWorkingDirectory(
|
||||
item,
|
||||
[
|
||||
"working_directory_id",
|
||||
"display_name",
|
||||
"repository_id",
|
||||
"creation_selector",
|
||||
"creation_ref",
|
||||
@@ -273,7 +275,6 @@ function runtimeWorkingDirectory(
|
||||
"cleanup_target",
|
||||
"status",
|
||||
"cleanliness",
|
||||
"primary_worker_id",
|
||||
"occupied_by",
|
||||
],
|
||||
label,
|
||||
@@ -318,6 +319,11 @@ function runtimeWorkingDirectory(
|
||||
item.working_directory_id,
|
||||
`${label}.working_directory_id`,
|
||||
),
|
||||
display_name: optional(
|
||||
item.display_name,
|
||||
`${label}.display_name`,
|
||||
string,
|
||||
),
|
||||
repository_id: string(item.repository_id, `${label}.repository_id`),
|
||||
creation_selector: optional(
|
||||
item.creation_selector,
|
||||
@@ -350,15 +356,25 @@ function runtimeWorkingDirectory(
|
||||
),
|
||||
status: status as RuntimeWorkingDirectorySummary["status"],
|
||||
cleanliness: optional(item.cleanliness, `${label}.cleanliness`, string),
|
||||
primary_worker_id: optional(
|
||||
item.primary_worker_id,
|
||||
`${label}.primary_worker_id`,
|
||||
string,
|
||||
),
|
||||
occupied_by: occupied,
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeWorkdirAttachment(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): RuntimeWorkerWorkdirAttachmentSummary {
|
||||
const item = record(value, label);
|
||||
exact(item, ["alias", "working_directory"], label);
|
||||
return {
|
||||
alias: string(item.alias, `${label}.alias`),
|
||||
working_directory: runtimeWorkingDirectory(
|
||||
item.working_directory,
|
||||
`${label}.working_directory`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function workerSummary(
|
||||
value: unknown,
|
||||
label: string,
|
||||
@@ -382,7 +398,7 @@ function workerSummary(
|
||||
"retention_state",
|
||||
"implementation",
|
||||
"capabilities",
|
||||
"working_directory",
|
||||
"workdir_attachments",
|
||||
"diagnostics",
|
||||
],
|
||||
label,
|
||||
@@ -406,11 +422,13 @@ function workerSummary(
|
||||
`${label}.implementation`,
|
||||
),
|
||||
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
|
||||
working_directory: optional(
|
||||
item.working_directory,
|
||||
`${label}.working_directory`,
|
||||
runtimeWorkingDirectory,
|
||||
),
|
||||
workdir_attachments: item.workdir_attachments === undefined
|
||||
? undefined
|
||||
: array(
|
||||
item.workdir_attachments,
|
||||
`${label}.workdir_attachments`,
|
||||
runtimeWorkdirAttachment,
|
||||
),
|
||||
diagnostics: array(item.diagnostics, `${label}.diagnostics`, diagnostic),
|
||||
};
|
||||
}
|
||||
@@ -464,8 +482,9 @@ function workingDirectorySelection(
|
||||
label: string,
|
||||
): BrowserWorkerWorkingDirectorySelection {
|
||||
const item = record(value, label);
|
||||
exact(item, ["working_directory_id", "relative_cwd"], label);
|
||||
exact(item, ["alias", "working_directory_id", "relative_cwd"], label);
|
||||
return {
|
||||
alias: string(item.alias, `${label}.alias`),
|
||||
working_directory_id: string(
|
||||
item.working_directory_id,
|
||||
`${label}.working_directory_id`,
|
||||
@@ -638,7 +657,7 @@ export function parseCreateWorkspaceWorkerRequest(
|
||||
"profile",
|
||||
"ticket_assignment",
|
||||
"initial_submit",
|
||||
"working_directory",
|
||||
"workdir_attachments",
|
||||
"control_operation_id",
|
||||
],
|
||||
"Worker create request",
|
||||
@@ -651,9 +670,11 @@ export function parseCreateWorkspaceWorkerRequest(
|
||||
? null
|
||||
: ticketAssignment(item.ticket_assignment, "ticket_assignment"),
|
||||
initial_submit: array(item.initial_submit, "initial_submit", segment),
|
||||
working_directory: item.working_directory === null
|
||||
? null
|
||||
: workingDirectorySelection(item.working_directory, "working_directory"),
|
||||
workdir_attachments: array(
|
||||
item.workdir_attachments,
|
||||
"workdir_attachments",
|
||||
workingDirectorySelection,
|
||||
),
|
||||
control_operation_id: nullableString(
|
||||
item.control_operation_id,
|
||||
"control_operation_id",
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
Math.max(0, workers.length - COLLAPSED_WORKER_COUNT),
|
||||
);
|
||||
|
||||
function workdirMeta(worker: SidebarWorker): string {
|
||||
const attachments = worker.workdir_attachments;
|
||||
if (attachments.length === 0) return '—';
|
||||
return attachments
|
||||
.map((attachment) => `${attachment.alias}: ${attachment.repository_key ?? '—'}・${attachment.working_directory_id}`)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function workerKey(worker: SidebarWorker): string {
|
||||
return `${worker.runtime_id}:${worker.worker_id}`;
|
||||
}
|
||||
@@ -190,7 +198,7 @@
|
||||
</span>
|
||||
<span class="worker-nav-label">{label}</span>
|
||||
<small class="worker-nav-meta">
|
||||
{worker.repository_key ?? '—'}・{worker.working_directory_id ?? '—'}
|
||||
{workdirMeta(worker)}
|
||||
</small>
|
||||
</a>
|
||||
<button
|
||||
|
||||
@@ -89,6 +89,11 @@ export type WorkerCapabilities = {
|
||||
can_spawn_followup: boolean;
|
||||
};
|
||||
|
||||
export type WorkerWorkdirAttachment = {
|
||||
alias: string;
|
||||
working_directory: WorkingDirectorySummary;
|
||||
};
|
||||
|
||||
export type Worker = {
|
||||
runtime_id: string;
|
||||
worker_id: string;
|
||||
@@ -107,7 +112,7 @@ export type Worker = {
|
||||
last_seen_at?: string | null;
|
||||
implementation: { kind: string; display_hint: string };
|
||||
capabilities: WorkerCapabilities;
|
||||
working_directory?: WorkingDirectorySummary | null;
|
||||
workdir_attachments?: WorkerWorkdirAttachment[];
|
||||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
type FetchFn = typeof fetch;
|
||||
type WorkerActionTarget = Pick<Worker, "runtime_id" | "worker_id" | "state">;
|
||||
|
||||
type WorkerLifecycleResponse = {
|
||||
state: string;
|
||||
diagnostics?: Diagnostic[];
|
||||
};
|
||||
|
||||
function workerPath(workspaceId: string, worker: Worker): string {
|
||||
function workerPath(workspaceId: string, worker: WorkerActionTarget): string {
|
||||
return workspaceApiPath(
|
||||
workspaceId,
|
||||
`/runtimes/${encodeURIComponent(worker.runtime_id)}/workers/${
|
||||
@@ -45,13 +46,13 @@ function diagnosticMessage(
|
||||
fallback;
|
||||
}
|
||||
|
||||
export function canDeleteSidebarWorker(worker: Worker): boolean {
|
||||
export function canDeleteSidebarWorker(worker: WorkerActionTarget): boolean {
|
||||
return worker.state === "stopped";
|
||||
}
|
||||
|
||||
export async function stopSidebarWorker(
|
||||
workspaceId: string,
|
||||
worker: Worker,
|
||||
worker: WorkerActionTarget,
|
||||
fetchFn: FetchFn = fetch,
|
||||
): Promise<void> {
|
||||
const response = await fetchFn(`${workerPath(workspaceId, worker)}/stop`, {
|
||||
@@ -71,7 +72,7 @@ export async function stopSidebarWorker(
|
||||
|
||||
export async function deleteSidebarWorker(
|
||||
workspaceId: string,
|
||||
worker: Worker,
|
||||
worker: WorkerActionTarget,
|
||||
fetchFn: FetchFn = fetch,
|
||||
): Promise<void> {
|
||||
const runtimePath = `/runtimes/${encodeURIComponent(worker.runtime_id)}`;
|
||||
|
||||
@@ -57,7 +57,6 @@ const options: WorkerLaunchOptionsResponse = {
|
||||
materializer_kind: "runtime_git_clone",
|
||||
status: "active",
|
||||
cleanliness: "clean",
|
||||
primary_worker_id: null,
|
||||
cleanup_target: {
|
||||
kind: "runtime_git_clone",
|
||||
working_directory_id: "wd-1-repo",
|
||||
@@ -180,7 +179,7 @@ Deno.test("defaultWorkerLaunchForm preserves a Ticket repository target", () =>
|
||||
assertEquals(form.working_directory_selector, "work/ticket");
|
||||
});
|
||||
|
||||
Deno.test("buildCreateWorkspaceWorkerRequest sends working_directory id and relative cwd only", () => {
|
||||
Deno.test("buildCreateWorkspaceWorkerRequest sends one aliased attachment", () => {
|
||||
const request = buildCreateWorkspaceWorkerRequest({
|
||||
runtime_id: "embedded",
|
||||
display_name: "Worker",
|
||||
@@ -198,10 +197,11 @@ Deno.test("buildCreateWorkspaceWorkerRequest sends working_directory id and rela
|
||||
profile: "builtin:coder",
|
||||
ticket_assignment: null,
|
||||
initial_submit: [{ kind: "text", content: "go" }],
|
||||
working_directory: {
|
||||
workdir_attachments: [{
|
||||
alias: "workdir",
|
||||
working_directory_id: "wd-1-repo",
|
||||
relative_cwd: "crates/yoi",
|
||||
},
|
||||
}],
|
||||
control_operation_id: null,
|
||||
});
|
||||
});
|
||||
@@ -221,7 +221,7 @@ Deno.test("buildCreateWorkspaceWorkerRequest sends no initial segments for an em
|
||||
assertEquals(request.initial_submit, []);
|
||||
});
|
||||
|
||||
Deno.test("buildCreateWorkspaceWorkerRequest emits null for embedded no-workdir launches", () => {
|
||||
Deno.test("buildCreateWorkspaceWorkerRequest emits an empty attachment list for workdirless launches", () => {
|
||||
const request = buildCreateWorkspaceWorkerRequest({
|
||||
runtime_id: "embedded",
|
||||
display_name: "Worker",
|
||||
@@ -239,7 +239,7 @@ Deno.test("buildCreateWorkspaceWorkerRequest emits null for embedded no-workdir
|
||||
profile: "builtin:companion",
|
||||
ticket_assignment: null,
|
||||
initial_submit: [{ kind: "text", content: "chat" }],
|
||||
working_directory: null,
|
||||
workdir_attachments: [],
|
||||
control_operation_id: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,6 @@ export function defaultWorkerLaunchForm(
|
||||
options?.working_directories.filter((directory) =>
|
||||
directory.status === "active" &&
|
||||
directory.cleanliness === "clean" &&
|
||||
directory.primary_worker_id == null &&
|
||||
directory.occupied_by == null
|
||||
) ?? [];
|
||||
const selectedRuntime = current.runtime_id
|
||||
@@ -97,12 +96,13 @@ export function buildCreateWorkspaceWorkerRequest(
|
||||
initial_submit: initialMessage
|
||||
? [{ kind: "text", content: form.initial_text }]
|
||||
: [],
|
||||
working_directory: form.working_directory_id
|
||||
? {
|
||||
workdir_attachments: form.working_directory_id
|
||||
? [{
|
||||
alias: "workdir",
|
||||
working_directory_id: form.working_directory_id,
|
||||
relative_cwd: form.relative_cwd.trim() || null,
|
||||
}
|
||||
: null,
|
||||
}]
|
||||
: [],
|
||||
control_operation_id: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ function worker(
|
||||
workspace_id: 'workspace-test',
|
||||
display_name: null,
|
||||
profile: null,
|
||||
working_directory_id: null,
|
||||
workdir_attachments: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import type { SubscriptionWorker } from '$lib/generated/protocol';
|
||||
import type {
|
||||
SubscriptionWorker,
|
||||
SubscriptionWorkerWorkdirAttachment,
|
||||
} from '$lib/generated/protocol';
|
||||
import { workspaceMultiplexer } from '$lib/workspace/multiplexer';
|
||||
import {
|
||||
applyWorkspaceWorkersFrame,
|
||||
@@ -9,9 +12,8 @@ import { liveWorkerState } from './worker-state';
|
||||
import { compareWorkersForSidebar } from './workers';
|
||||
import type { Worker } from './types';
|
||||
|
||||
export type SidebarWorker = Worker & {
|
||||
repository_key: string | null;
|
||||
working_directory_id: string | null;
|
||||
export type SidebarWorker = Omit<Worker, 'workdir_attachments'> & {
|
||||
workdir_attachments: SubscriptionWorkerWorkdirAttachment[];
|
||||
has_running_internal_workers: boolean;
|
||||
};
|
||||
|
||||
@@ -103,10 +105,8 @@ function projectWorker(worker: SubscriptionWorker): SidebarWorker {
|
||||
can_stop: worker.availability !== 'unavailable' && worker.state !== 'stopped',
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
repository_key: worker.repository_key ?? null,
|
||||
working_directory_id: worker.working_directory_id ?? null,
|
||||
workdir_attachments: worker.workdir_attachments ?? [],
|
||||
has_running_internal_workers: worker.has_running_internal_workers,
|
||||
working_directory: null,
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function worker(overrides: Partial<Worker>): Worker {
|
||||
can_stop: true,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
working_directory: null,
|
||||
workdir_attachments: [],
|
||||
diagnostics: [],
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -19,11 +19,15 @@ export function sidebarWorkerActivity(
|
||||
return 'none';
|
||||
}
|
||||
|
||||
export function canShowWorkerInSidebar(worker: Worker): boolean {
|
||||
export function canShowWorkerInSidebar(
|
||||
worker: Pick<Worker, 'implementation'>,
|
||||
): boolean {
|
||||
return worker.implementation.kind !== 'backend_worker_registry';
|
||||
}
|
||||
|
||||
export function canOpenWorkerConsole(worker: Worker): boolean {
|
||||
export function canOpenWorkerConsole(
|
||||
worker: Pick<Worker, 'implementation'>,
|
||||
): boolean {
|
||||
return canShowWorkerInSidebar(worker);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -138,7 +138,10 @@
|
||||
{#each workdirs as workdir}
|
||||
{@const cleanup = cleanupCandidate(workdir)}
|
||||
<tr>
|
||||
<td><code>{workdir.working_directory_id}</code></td>
|
||||
<td>
|
||||
<span>{workdir.display_name ?? '—'}</span>
|
||||
<small><code>{workdir.working_directory_id}</code></small>
|
||||
</td>
|
||||
<td>{workdir.repository_key}</td>
|
||||
<td><code>{currentRevision(workdir)}</code></td>
|
||||
<td>{workdir.status}</td>
|
||||
|
||||
@@ -145,11 +145,14 @@
|
||||
}
|
||||
|
||||
function workerDirectory(worker: Worker): string {
|
||||
const directory = worker.working_directory;
|
||||
if (!directory) return '—';
|
||||
const provider = data.repositories?.items.find((repository) => repository.repository_key === directory.repository_key)
|
||||
?.provider;
|
||||
return `${directory.repository_key} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||
const attachments = worker.workdir_attachments ?? [];
|
||||
if (attachments.length === 0) return '—';
|
||||
return attachments.map(({ alias, working_directory: directory }) => {
|
||||
const provider = data.repositories?.items.find((repository) => repository.repository_key === directory.repository_key)
|
||||
?.provider;
|
||||
const label = directory.display_name ?? directory.repository_key;
|
||||
return `${alias}: ${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||
}).join(', ');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
function workdirOptionLabel(directory: WorkingDirectorySummary): string {
|
||||
const provider = data.repositories?.items.find((repository) => repository.repository_key === directory.repository_key)
|
||||
?.provider;
|
||||
return `${directory.repository_key} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||
const label = directory.display_name ?? directory.repository_key;
|
||||
return `${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||
}
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
@@ -62,6 +63,7 @@
|
||||
);
|
||||
let initialText = $state(ticketContext?.initialInput ?? '');
|
||||
let workingDirectoryId = $state('');
|
||||
let workingDirectoryDisplayName = $state('');
|
||||
let workingDirectoryRepositoryKey = $state(ticketContext?.repositoryKey ?? '');
|
||||
let workingDirectorySelector = $state(ticketContext?.refSelector ?? 'HEAD');
|
||||
let relativeCwd = $state('');
|
||||
@@ -78,7 +80,6 @@
|
||||
: (options?.working_directories ?? []).filter((directory) =>
|
||||
directory.status === 'active' &&
|
||||
directory.cleanliness === 'clean' &&
|
||||
directory.primary_worker_id == null &&
|
||||
directory.occupied_by == null
|
||||
),
|
||||
);
|
||||
@@ -168,6 +169,7 @@
|
||||
try {
|
||||
const request = validateWorkingDirectoryCreateRequest({
|
||||
runtime_id: runtimeId,
|
||||
...(workingDirectoryDisplayName.trim() ? { display_name: workingDirectoryDisplayName.trim() } : {}),
|
||||
repository_key: workingDirectoryRepositoryKey,
|
||||
...(workingDirectorySelector ? { selector: workingDirectorySelector } : {}),
|
||||
});
|
||||
@@ -340,6 +342,10 @@
|
||||
<div class="new-working-directory-panel">
|
||||
<h3>New workdir</h3>
|
||||
<div class="new-working-directory-fields">
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input bind:value={workingDirectoryDisplayName} autocomplete="off" placeholder="Optional label" maxlength="80" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Repository</span>
|
||||
<select bind:value={workingDirectoryRepositoryKey}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
const summary = {
|
||||
working_directory_id: "workdir-1",
|
||||
display_name: "Checkout",
|
||||
repository_key: "main",
|
||||
materializer_kind: "runtime_git_clone",
|
||||
status: "active",
|
||||
@@ -27,6 +28,9 @@ Deno.test("Workdir REST validation accepts the generated list and create contrac
|
||||
items: [summary],
|
||||
diagnostics: [],
|
||||
});
|
||||
if (list.items[0]?.display_name !== "Checkout") {
|
||||
throw new Error("display name was not preserved");
|
||||
}
|
||||
if (list.items[0]?.occupied_by?.runtime_id !== "arcadia") {
|
||||
throw new Error("occupancy subject was not preserved");
|
||||
}
|
||||
@@ -60,6 +64,7 @@ Deno.test("Workdir REST validation rejects stale response JSON", () => {
|
||||
Deno.test("Workdir REST validation enforces create operation fields", () => {
|
||||
const request = validateWorkingDirectoryCreateRequest({
|
||||
runtime_id: "arcadia",
|
||||
display_name: "Review checkout",
|
||||
repository_key: "main",
|
||||
selector: "develop",
|
||||
operation_id: "operation-1",
|
||||
@@ -67,6 +72,9 @@ Deno.test("Workdir REST validation enforces create operation fields", () => {
|
||||
if (request.operation_id !== "operation-1") {
|
||||
throw new Error("operation id was not preserved");
|
||||
}
|
||||
if (request.display_name !== "Review checkout") {
|
||||
throw new Error("display name was not preserved");
|
||||
}
|
||||
|
||||
for (
|
||||
const invalid of [
|
||||
|
||||
Reference in New Issue
Block a user