Merge branch 'develop' into hare/develop
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
|
||||
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
|
||||
"build": "deno run -A npm:vite@7.2.7 build",
|
||||
"preview": "deno run -A npm:vite@7.2.7 preview",
|
||||
|
||||
@@ -219,6 +219,12 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||
const workersNav = await Deno.readTextFile(
|
||||
new URL("../sidebar/WorkersNavSection.svelte", import.meta.url),
|
||||
);
|
||||
const workerWorkdirMeta = await Deno.readTextFile(
|
||||
new URL("../sidebar/worker-workdir-meta.ts", import.meta.url),
|
||||
);
|
||||
const workerSubscription = await Deno.readTextFile(
|
||||
new URL("../sidebar/worker-subscription.ts", import.meta.url),
|
||||
);
|
||||
const sidebar = await Deno.readTextFile(
|
||||
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
|
||||
);
|
||||
@@ -256,12 +262,23 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||
workersNav.includes("worker.display_name || worker.label") &&
|
||||
workersNav.includes("worker-status-dot") &&
|
||||
workersNav.includes("worker-status-spinner") &&
|
||||
workersNav.includes("workdirMeta(worker)") &&
|
||||
workersNav.includes("attachment.alias") &&
|
||||
workersNav.includes("attachment.repository_key ?? '—'") &&
|
||||
workersNav.includes("attachment.working_directory_id") &&
|
||||
workersNav.includes("sidebarWorkdirMeta(worker.workdir_attachments)") &&
|
||||
workersNav.includes("title={workdir.details}") &&
|
||||
workersNav.includes("aria-label={workdir.details}") &&
|
||||
!workersNav.includes("attachment.alias") &&
|
||||
workerWorkdirMeta.includes("workdir.current_selector") &&
|
||||
workerWorkdirMeta.includes("workdir.creation_selector") &&
|
||||
workerWorkdirMeta.includes("explicitReferenceFallback(") &&
|
||||
workerWorkdirMeta.includes('"detached"') &&
|
||||
workerSubscription.includes(
|
||||
'workspaceApiPath(workspaceId, "/working-directories")',
|
||||
) &&
|
||||
!workerSubscription.includes('topic: "workspace_workdirs"') &&
|
||||
sidebarCss.includes(".worker-nav-meta") &&
|
||||
sidebarCss.includes("text-overflow: ellipsis") &&
|
||||
sidebarCss.includes("white-space: nowrap") &&
|
||||
!workersNav.includes('aria-disabled="true"'),
|
||||
"Workers sidebar should link to the Worker list page and show state indicators with repository/workdir metadata",
|
||||
"Workers sidebar should show truncated repo:branch Workdir metadata with detached fallback and detail-only attachment identifiers",
|
||||
);
|
||||
assert(
|
||||
workersNav.includes("COLLAPSED_WORKER_COUNT = 6") &&
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
type SidebarWorker,
|
||||
} from './worker-subscription';
|
||||
import { canShowWorkerInSidebar, sidebarWorkerActivity } from './workers';
|
||||
import { sidebarWorkdirMeta } from './worker-workdir-meta';
|
||||
|
||||
const COLLAPSED_WORKER_COUNT = 6;
|
||||
type WorkerActionKind = 'stop' | 'delete';
|
||||
@@ -37,14 +38,6 @@
|
||||
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}`;
|
||||
}
|
||||
@@ -180,6 +173,7 @@
|
||||
{@const activity = sidebarWorkerActivity(worker)}
|
||||
{@const key = workerKey(worker)}
|
||||
{@const label = worker.display_name || worker.label}
|
||||
{@const workdir = sidebarWorkdirMeta(worker.workdir_attachments)}
|
||||
<li class="worker-nav-item" data-worker-actions={key}>
|
||||
<a
|
||||
href={href}
|
||||
@@ -197,8 +191,8 @@
|
||||
{/if}
|
||||
</span>
|
||||
<span class="worker-nav-label">{label}</span>
|
||||
<small class="worker-nav-meta">
|
||||
{workdirMeta(worker)}
|
||||
<small class="worker-nav-meta" title={workdir.details} aria-label={workdir.details}>
|
||||
{workdir.text}
|
||||
</small>
|
||||
</a>
|
||||
<button
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import type {
|
||||
SubscriptionWorker,
|
||||
SubscriptionWorkerWorkdirAttachment,
|
||||
} from '$lib/generated/protocol';
|
||||
import { workspaceMultiplexer } from '$lib/workspace/multiplexer';
|
||||
import { type Readable, readable } from "svelte/store";
|
||||
import type { WorkingDirectorySummary } from "$lib/generated/workdir-api";
|
||||
import type { SubscriptionWorker } from "$lib/generated/protocol";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
|
||||
import { workspaceMultiplexer } from "$lib/workspace/multiplexer";
|
||||
import {
|
||||
applyWorkspaceWorkersFrame,
|
||||
createWorkspaceWorkersProjection,
|
||||
} from './worker-subscription-model';
|
||||
import { liveWorkerState } from './worker-state';
|
||||
import { compareWorkersForSidebar } from './workers';
|
||||
import type { Worker } from './types';
|
||||
} from "./worker-subscription-model";
|
||||
import { liveWorkerState } from "./worker-state";
|
||||
import type { SidebarWorkdirAttachment } from "./worker-workdir-meta";
|
||||
import { compareWorkersForSidebar } from "./workers";
|
||||
import type { Worker } from "./types";
|
||||
|
||||
export type SidebarWorker = Omit<Worker, 'workdir_attachments'> & {
|
||||
workdir_attachments: SubscriptionWorkerWorkdirAttachment[];
|
||||
export type SidebarWorker = Omit<Worker, "workdir_attachments"> & {
|
||||
workdir_attachments: SidebarWorkdirAttachment[];
|
||||
has_running_internal_workers: boolean;
|
||||
};
|
||||
|
||||
@@ -29,7 +30,9 @@ export function disposeWorkspaceWorkersStore(workspaceId: string): void {
|
||||
stores.delete(workspaceId);
|
||||
}
|
||||
|
||||
export function workspaceWorkersStore(workspaceId: string): Readable<WorkspaceWorkersState> {
|
||||
export function workspaceWorkersStore(
|
||||
workspaceId: string,
|
||||
): Readable<WorkspaceWorkersState> {
|
||||
const cached = stores.get(workspaceId);
|
||||
if (cached) return cached;
|
||||
const store = readable<WorkspaceWorkersState>(
|
||||
@@ -40,48 +43,95 @@ export function workspaceWorkersStore(workspaceId: string): Readable<WorkspaceWo
|
||||
return;
|
||||
}
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
const publish = (loading = false, error: string | null = null) => {
|
||||
let workdirs = new Map<string, WorkingDirectorySummary>();
|
||||
let loading = true;
|
||||
let error: string | null = null;
|
||||
let disposed = false;
|
||||
const publish = () => {
|
||||
const workers = [...projection.workers.values()]
|
||||
.map(projectWorker)
|
||||
.map((worker) => projectWorker(worker, workdirs))
|
||||
.sort(compareWorkersForSidebar);
|
||||
set({ loading, error, workers });
|
||||
};
|
||||
const loadWorkdirs = async () => {
|
||||
const result = await loadJson(
|
||||
fetch,
|
||||
workspaceApiPath(workspaceId, "/working-directories"),
|
||||
undefined,
|
||||
parseWorkingDirectoryListResponse,
|
||||
);
|
||||
if (disposed || !result.data) return;
|
||||
workdirs = new Map(
|
||||
result.data.items.map((
|
||||
workdir,
|
||||
) => [workdir.working_directory_id, workdir]),
|
||||
);
|
||||
publish();
|
||||
};
|
||||
const subscription = workspaceMultiplexer(workspaceId).subscribe(
|
||||
{ topic: 'workspace_workers' },
|
||||
{ topic: "workspace_workers" },
|
||||
{
|
||||
onFrame: (frame) => {
|
||||
try {
|
||||
if (frame.frame === 'event' && frame.message.event === 'subscription_closed') {
|
||||
if (
|
||||
frame.frame === "event" &&
|
||||
frame.message.event === "subscription_closed"
|
||||
) {
|
||||
throw new Error(frame.message.data.message);
|
||||
}
|
||||
if (
|
||||
frame.frame === 'response' &&
|
||||
frame.message.result === 'subscription_rejected'
|
||||
frame.frame === "response" &&
|
||||
frame.message.result === "subscription_rejected"
|
||||
) {
|
||||
throw new Error(frame.message.payload.message);
|
||||
}
|
||||
applyWorkspaceWorkersFrame(projection, frame);
|
||||
publish(false, null);
|
||||
} catch (error) {
|
||||
publish(false, error instanceof Error ? error.message : 'invalid Worker subscription frame');
|
||||
loading = false;
|
||||
error = null;
|
||||
publish();
|
||||
} catch (cause) {
|
||||
loading = false;
|
||||
error = cause instanceof Error
|
||||
? cause.message
|
||||
: "invalid Worker subscription frame";
|
||||
publish();
|
||||
}
|
||||
},
|
||||
onStatus: (status, message) => {
|
||||
if (status === 'connecting') publish(projection.workers.size === 0, null);
|
||||
if (status === 'closed') publish(projection.workers.size === 0, message ?? null);
|
||||
if (status === "connecting") {
|
||||
loading = projection.workers.size === 0;
|
||||
error = null;
|
||||
publish();
|
||||
}
|
||||
if (status === "closed") {
|
||||
loading = projection.workers.size === 0;
|
||||
error = message ?? null;
|
||||
publish();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
return () => subscription.close();
|
||||
void loadWorkdirs();
|
||||
return () => {
|
||||
disposed = true;
|
||||
subscription.close();
|
||||
};
|
||||
},
|
||||
);
|
||||
stores.set(workspaceId, store);
|
||||
return store;
|
||||
}
|
||||
|
||||
function projectWorker(worker: SubscriptionWorker): SidebarWorker {
|
||||
if (!worker.runtime_id) throw new Error('Workspace Worker projection is missing runtime_id');
|
||||
if (!worker.resource_key) throw new Error('Workspace Worker projection is missing resource_key');
|
||||
function projectWorker(
|
||||
worker: SubscriptionWorker,
|
||||
workdirs: ReadonlyMap<string, WorkingDirectorySummary>,
|
||||
): SidebarWorker {
|
||||
if (!worker.runtime_id) {
|
||||
throw new Error("Workspace Worker projection is missing runtime_id");
|
||||
}
|
||||
if (!worker.resource_key) {
|
||||
throw new Error("Workspace Worker projection is missing resource_key");
|
||||
}
|
||||
const displayName = worker.display_name ?? `Worker ${worker.worker_id}`;
|
||||
return {
|
||||
runtime_id: worker.runtime_id,
|
||||
@@ -92,20 +142,29 @@ function projectWorker(worker: SubscriptionWorker): SidebarWorker {
|
||||
label: displayName,
|
||||
profile: worker.profile ?? null,
|
||||
tags: [],
|
||||
workspace: { visibility: 'workspace', identity: 'runtime_subscription_worker' },
|
||||
workspace: {
|
||||
visibility: "workspace",
|
||||
identity: "runtime_subscription_worker",
|
||||
},
|
||||
state: liveWorkerState(worker),
|
||||
worker_state: worker.worker_state,
|
||||
pinned: false,
|
||||
retention_state: 'transient',
|
||||
retention_state: "transient",
|
||||
implementation: {
|
||||
kind: 'runtime_subscription_worker',
|
||||
display_hint: 'Workspace-authorized Runtime Worker',
|
||||
kind: "runtime_subscription_worker",
|
||||
display_hint: "Workspace-authorized Runtime Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_stop: worker.availability !== 'unavailable' && worker.state !== 'stopped',
|
||||
can_stop: worker.availability !== "unavailable" &&
|
||||
worker.state !== "stopped",
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
workdir_attachments: worker.workdir_attachments ?? [],
|
||||
workdir_attachments: (worker.workdir_attachments ?? []).map((
|
||||
attachment,
|
||||
) => ({
|
||||
...attachment,
|
||||
working_directory: workdirs.get(attachment.working_directory_id),
|
||||
})),
|
||||
has_running_internal_workers: worker.has_running_internal_workers,
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { WorkingDirectorySummary } from "$lib/generated/workdir-api";
|
||||
import {
|
||||
type SidebarWorkdirAttachment,
|
||||
sidebarWorkdirMeta,
|
||||
} from "./worker-workdir-meta.ts";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(
|
||||
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition: boolean, message: string): void {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
};
|
||||
|
||||
function workdir(
|
||||
overrides: Partial<WorkingDirectorySummary> = {},
|
||||
): WorkingDirectorySummary {
|
||||
return {
|
||||
working_directory_id: "workdir-1234567890abcdef",
|
||||
repository_key: "main",
|
||||
materializer_kind: "runtime_git_clone",
|
||||
status: "active",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function attachment(
|
||||
alias: string,
|
||||
overrides: Partial<WorkingDirectorySummary> = {},
|
||||
): SidebarWorkdirAttachment {
|
||||
const workingDirectory = workdir(overrides);
|
||||
return {
|
||||
alias,
|
||||
repository_key: workingDirectory.repository_key,
|
||||
working_directory_id: workingDirectory.working_directory_id,
|
||||
working_directory: workingDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("Worker sidebar Workdir label prefers the current branch selector", () => {
|
||||
const meta = sidebarWorkdirMeta([
|
||||
attachment("checkout", {
|
||||
creation_selector: "develop",
|
||||
current_selector: "refs/heads/work/T-642-sidebar",
|
||||
current_ref: "0123456789abcdef0123456789abcdef01234567",
|
||||
}),
|
||||
]);
|
||||
|
||||
assertEquals(meta.text, "main:work/T-642-sidebar");
|
||||
assert(
|
||||
meta.details.includes("checkout — main:work/T-642-sidebar"),
|
||||
"attachment alias should remain available in details",
|
||||
);
|
||||
assert(
|
||||
meta.details.includes("Workdir workdir-1234567890abcdef"),
|
||||
"opaque Workdir id should remain available in details",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Worker sidebar Workdir label uses the creation branch before observation", () => {
|
||||
const meta = sidebarWorkdirMeta([
|
||||
attachment("checkout", { creation_selector: "develop" }),
|
||||
]);
|
||||
|
||||
assertEquals(meta.text, "main:develop");
|
||||
});
|
||||
|
||||
Deno.test("Worker sidebar Workdir label marks detached HEAD instead of reusing creation branch", () => {
|
||||
const meta = sidebarWorkdirMeta([
|
||||
attachment("checkout", {
|
||||
creation_selector: "develop",
|
||||
current_ref: "0123456789abcdef0123456789abcdef01234567",
|
||||
}),
|
||||
]);
|
||||
|
||||
assertEquals(meta.text, "main:detached@0123456789ab");
|
||||
});
|
||||
|
||||
Deno.test("Worker sidebar Workdir label marks an untrusted selector as a detached hash", () => {
|
||||
const meta = sidebarWorkdirMeta([
|
||||
attachment("checkout", {
|
||||
creation_selector: "develop",
|
||||
current_selector: "fedcba9876543210fedcba9876543210fedcba98",
|
||||
}),
|
||||
]);
|
||||
|
||||
assertEquals(meta.text, "main:detached@fedcba987654");
|
||||
});
|
||||
|
||||
Deno.test("Worker sidebar Workdir label has an explicit unavailable-projection fallback", () => {
|
||||
const meta = sidebarWorkdirMeta([{
|
||||
alias: "checkout",
|
||||
repository_key: "main",
|
||||
working_directory_id: "workdir-1234567890abcdef",
|
||||
}]);
|
||||
|
||||
assertEquals(meta.text, "main:unknown@workdir-1234");
|
||||
});
|
||||
|
||||
Deno.test("Worker sidebar Workdir metadata preserves long names for truncated display details", () => {
|
||||
const repository = "repository-with-a-name-that-exceeds-the-sidebar-width";
|
||||
const branch =
|
||||
"feature/workdir-label-with-a-name-that-also-exceeds-the-sidebar-width";
|
||||
const meta = sidebarWorkdirMeta([
|
||||
attachment("long-checkout", {
|
||||
repository_key: repository,
|
||||
current_selector: branch,
|
||||
}),
|
||||
]);
|
||||
|
||||
assertEquals(meta.text, `${repository}:${branch}`);
|
||||
assert(
|
||||
meta.details.includes(`long-checkout — ${repository}:${branch}`),
|
||||
"full repository and branch should remain available in details when the primary line is truncated",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Worker sidebar Workdir metadata keeps multiple attachments distinguishable", () => {
|
||||
const meta = sidebarWorkdirMeta([
|
||||
attachment("frontend", { current_selector: "develop" }),
|
||||
attachment("backend", {
|
||||
working_directory_id: "workdir-fedcba0987654321",
|
||||
repository_key: "server",
|
||||
current_selector: "release",
|
||||
}),
|
||||
]);
|
||||
|
||||
assertEquals(meta.text, "main:develop · server:release");
|
||||
assert(
|
||||
meta.details.includes("frontend — main:develop"),
|
||||
"first alias should be in details",
|
||||
);
|
||||
assert(
|
||||
meta.details.includes("backend — server:release"),
|
||||
"second alias should be in details",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { WorkingDirectorySummary } from "$lib/generated/workdir-api";
|
||||
import type { SubscriptionWorkerWorkdirAttachment } from "$lib/generated/protocol";
|
||||
|
||||
const SHORT_REFERENCE_LENGTH = 12;
|
||||
|
||||
export type SidebarWorkdirAttachment = SubscriptionWorkerWorkdirAttachment & {
|
||||
working_directory?: WorkingDirectorySummary;
|
||||
};
|
||||
|
||||
export type SidebarWorkdirMeta = {
|
||||
text: string;
|
||||
details: string;
|
||||
};
|
||||
|
||||
export function sidebarWorkdirMeta(
|
||||
attachments: readonly SidebarWorkdirAttachment[],
|
||||
): SidebarWorkdirMeta {
|
||||
if (attachments.length === 0) {
|
||||
return { text: "—", details: "No Workdir attachments" };
|
||||
}
|
||||
|
||||
return {
|
||||
text: attachments.map(attachmentLabel).join(" · "),
|
||||
details: attachments.map(attachmentDetails).join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function attachmentLabel(attachment: SidebarWorkdirAttachment): string {
|
||||
const workdir = attachment.working_directory;
|
||||
const repository = clean(workdir?.repository_key) ??
|
||||
clean(attachment.repository_key) ?? "unknown-repo";
|
||||
return `${repository}:${revisionLabel(attachment)}`;
|
||||
}
|
||||
|
||||
function revisionLabel(attachment: SidebarWorkdirAttachment): string {
|
||||
const workdir = attachment.working_directory;
|
||||
if (!workdir) {
|
||||
return `unknown@${shortReference(attachment.working_directory_id)}`;
|
||||
}
|
||||
|
||||
const currentSelector = clean(workdir.current_selector);
|
||||
const currentBranch = branchName(currentSelector);
|
||||
if (currentBranch) return currentBranch;
|
||||
|
||||
const currentReference = clean(workdir.current_ref);
|
||||
if (currentSelector || currentReference) {
|
||||
return explicitReferenceFallback(
|
||||
"detached",
|
||||
currentReference ?? currentSelector,
|
||||
);
|
||||
}
|
||||
|
||||
const creationSelector = clean(workdir.creation_selector);
|
||||
const creationBranch = branchName(creationSelector);
|
||||
if (creationBranch) return creationBranch;
|
||||
|
||||
const creationReference = clean(workdir.creation_ref);
|
||||
if (creationSelector || creationReference) {
|
||||
return explicitReferenceFallback(
|
||||
"revision",
|
||||
creationReference ?? creationSelector,
|
||||
);
|
||||
}
|
||||
|
||||
return `unknown@${shortReference(attachment.working_directory_id)}`;
|
||||
}
|
||||
|
||||
function branchName(selector: string | null): string | null {
|
||||
if (!selector || selector === "HEAD" || isHash(selector)) return null;
|
||||
if (selector.startsWith("refs/heads/")) {
|
||||
return clean(selector.slice("refs/heads/".length));
|
||||
}
|
||||
if (selector.startsWith("refs/")) return null;
|
||||
return selector;
|
||||
}
|
||||
|
||||
function explicitReferenceFallback(
|
||||
kind: "detached" | "revision",
|
||||
reference: string | null,
|
||||
): string {
|
||||
return reference ? `${kind}@${shortReference(reference)}` : kind;
|
||||
}
|
||||
|
||||
function attachmentDetails(attachment: SidebarWorkdirAttachment): string {
|
||||
const workdir = attachment.working_directory;
|
||||
const details = [
|
||||
`${attachment.alias} — ${attachmentLabel(attachment)}`,
|
||||
`Workdir ${attachment.working_directory_id}`,
|
||||
];
|
||||
if (clean(workdir?.current_selector)) {
|
||||
details.push(`Current selector ${workdir?.current_selector?.trim()}`);
|
||||
}
|
||||
if (clean(workdir?.current_ref)) {
|
||||
details.push(`Current ref ${workdir?.current_ref?.trim()}`);
|
||||
}
|
||||
if (clean(workdir?.creation_selector)) {
|
||||
details.push(`Creation selector ${workdir?.creation_selector?.trim()}`);
|
||||
}
|
||||
return details.join(" · ");
|
||||
}
|
||||
|
||||
function clean(value: string | null | undefined): string | null {
|
||||
const cleaned = value?.trim();
|
||||
return cleaned ? cleaned : null;
|
||||
}
|
||||
|
||||
function shortReference(reference: string): string {
|
||||
return reference.length > SHORT_REFERENCE_LENGTH
|
||||
? reference.slice(0, SHORT_REFERENCE_LENGTH)
|
||||
: reference;
|
||||
}
|
||||
|
||||
function isHash(value: string): boolean {
|
||||
return /^[0-9a-f]{7,64}$/i.test(value);
|
||||
}
|
||||
Reference in New Issue
Block a user