Merge branch 'develop' into hare/develop

This commit is contained in:
2026-09-22 03:01:55 +09:00
6 changed files with 380 additions and 50 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev", "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", "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", "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", "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", "build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview", "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( const workersNav = await Deno.readTextFile(
new URL("../sidebar/WorkersNavSection.svelte", import.meta.url), 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( const sidebar = await Deno.readTextFile(
new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url), 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.display_name || worker.label") &&
workersNav.includes("worker-status-dot") && workersNav.includes("worker-status-dot") &&
workersNav.includes("worker-status-spinner") && workersNav.includes("worker-status-spinner") &&
workersNav.includes("workdirMeta(worker)") && workersNav.includes("sidebarWorkdirMeta(worker.workdir_attachments)") &&
workersNav.includes("attachment.alias") && workersNav.includes("title={workdir.details}") &&
workersNav.includes("attachment.repository_key ?? '—'") && workersNav.includes("aria-label={workdir.details}") &&
workersNav.includes("attachment.working_directory_id") && !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"'), !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( assert(
workersNav.includes("COLLAPSED_WORKER_COUNT = 6") && workersNav.includes("COLLAPSED_WORKER_COUNT = 6") &&
@@ -12,6 +12,7 @@
type SidebarWorker, type SidebarWorker,
} from './worker-subscription'; } from './worker-subscription';
import { canShowWorkerInSidebar, sidebarWorkerActivity } from './workers'; import { canShowWorkerInSidebar, sidebarWorkerActivity } from './workers';
import { sidebarWorkdirMeta } from './worker-workdir-meta';
const COLLAPSED_WORKER_COUNT = 6; const COLLAPSED_WORKER_COUNT = 6;
type WorkerActionKind = 'stop' | 'delete'; type WorkerActionKind = 'stop' | 'delete';
@@ -37,14 +38,6 @@
Math.max(0, workers.length - COLLAPSED_WORKER_COUNT), 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 { function workerKey(worker: SidebarWorker): string {
return `${worker.runtime_id}:${worker.worker_id}`; return `${worker.runtime_id}:${worker.worker_id}`;
} }
@@ -180,6 +173,7 @@
{@const activity = sidebarWorkerActivity(worker)} {@const activity = sidebarWorkerActivity(worker)}
{@const key = workerKey(worker)} {@const key = workerKey(worker)}
{@const label = worker.display_name || worker.label} {@const label = worker.display_name || worker.label}
{@const workdir = sidebarWorkdirMeta(worker.workdir_attachments)}
<li class="worker-nav-item" data-worker-actions={key}> <li class="worker-nav-item" data-worker-actions={key}>
<a <a
href={href} href={href}
@@ -197,8 +191,8 @@
{/if} {/if}
</span> </span>
<span class="worker-nav-label">{label}</span> <span class="worker-nav-label">{label}</span>
<small class="worker-nav-meta"> <small class="worker-nav-meta" title={workdir.details} aria-label={workdir.details}>
{workdirMeta(worker)} {workdir.text}
</small> </small>
</a> </a>
<button <button
@@ -1,19 +1,20 @@
import { readable, type Readable } from 'svelte/store'; import { type Readable, readable } from "svelte/store";
import type { import type { WorkingDirectorySummary } from "$lib/generated/workdir-api";
SubscriptionWorker, import type { SubscriptionWorker } from "$lib/generated/protocol";
SubscriptionWorkerWorkdirAttachment, import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
} from '$lib/generated/protocol'; import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
import { workspaceMultiplexer } from '$lib/workspace/multiplexer'; import { workspaceMultiplexer } from "$lib/workspace/multiplexer";
import { import {
applyWorkspaceWorkersFrame, applyWorkspaceWorkersFrame,
createWorkspaceWorkersProjection, createWorkspaceWorkersProjection,
} from './worker-subscription-model'; } from "./worker-subscription-model";
import { liveWorkerState } from './worker-state'; import { liveWorkerState } from "./worker-state";
import { compareWorkersForSidebar } from './workers'; import type { SidebarWorkdirAttachment } from "./worker-workdir-meta";
import type { Worker } from './types'; import { compareWorkersForSidebar } from "./workers";
import type { Worker } from "./types";
export type SidebarWorker = Omit<Worker, 'workdir_attachments'> & { export type SidebarWorker = Omit<Worker, "workdir_attachments"> & {
workdir_attachments: SubscriptionWorkerWorkdirAttachment[]; workdir_attachments: SidebarWorkdirAttachment[];
has_running_internal_workers: boolean; has_running_internal_workers: boolean;
}; };
@@ -29,7 +30,9 @@ export function disposeWorkspaceWorkersStore(workspaceId: string): void {
stores.delete(workspaceId); stores.delete(workspaceId);
} }
export function workspaceWorkersStore(workspaceId: string): Readable<WorkspaceWorkersState> { export function workspaceWorkersStore(
workspaceId: string,
): Readable<WorkspaceWorkersState> {
const cached = stores.get(workspaceId); const cached = stores.get(workspaceId);
if (cached) return cached; if (cached) return cached;
const store = readable<WorkspaceWorkersState>( const store = readable<WorkspaceWorkersState>(
@@ -40,48 +43,95 @@ export function workspaceWorkersStore(workspaceId: string): Readable<WorkspaceWo
return; return;
} }
const projection = createWorkspaceWorkersProjection(); 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()] const workers = [...projection.workers.values()]
.map(projectWorker) .map((worker) => projectWorker(worker, workdirs))
.sort(compareWorkersForSidebar); .sort(compareWorkersForSidebar);
set({ loading, error, workers }); 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( const subscription = workspaceMultiplexer(workspaceId).subscribe(
{ topic: 'workspace_workers' }, { topic: "workspace_workers" },
{ {
onFrame: (frame) => { onFrame: (frame) => {
try { 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); throw new Error(frame.message.data.message);
} }
if ( if (
frame.frame === 'response' && frame.frame === "response" &&
frame.message.result === 'subscription_rejected' frame.message.result === "subscription_rejected"
) { ) {
throw new Error(frame.message.payload.message); throw new Error(frame.message.payload.message);
} }
applyWorkspaceWorkersFrame(projection, frame); applyWorkspaceWorkersFrame(projection, frame);
publish(false, null); loading = false;
} catch (error) { error = null;
publish(false, error instanceof Error ? error.message : 'invalid Worker subscription frame'); publish();
} catch (cause) {
loading = false;
error = cause instanceof Error
? cause.message
: "invalid Worker subscription frame";
publish();
} }
}, },
onStatus: (status, message) => { onStatus: (status, message) => {
if (status === 'connecting') publish(projection.workers.size === 0, null); if (status === "connecting") {
if (status === 'closed') publish(projection.workers.size === 0, message ?? null); 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); stores.set(workspaceId, store);
return store; return store;
} }
function projectWorker(worker: SubscriptionWorker): SidebarWorker { function projectWorker(
if (!worker.runtime_id) throw new Error('Workspace Worker projection is missing runtime_id'); worker: SubscriptionWorker,
if (!worker.resource_key) throw new Error('Workspace Worker projection is missing resource_key'); 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}`; const displayName = worker.display_name ?? `Worker ${worker.worker_id}`;
return { return {
runtime_id: worker.runtime_id, runtime_id: worker.runtime_id,
@@ -92,20 +142,29 @@ function projectWorker(worker: SubscriptionWorker): SidebarWorker {
label: displayName, label: displayName,
profile: worker.profile ?? null, profile: worker.profile ?? null,
tags: [], tags: [],
workspace: { visibility: 'workspace', identity: 'runtime_subscription_worker' }, workspace: {
visibility: "workspace",
identity: "runtime_subscription_worker",
},
state: liveWorkerState(worker), state: liveWorkerState(worker),
worker_state: worker.worker_state, worker_state: worker.worker_state,
pinned: false, pinned: false,
retention_state: 'transient', retention_state: "transient",
implementation: { implementation: {
kind: 'runtime_subscription_worker', kind: "runtime_subscription_worker",
display_hint: 'Workspace-authorized Runtime Worker', display_hint: "Workspace-authorized Runtime Worker",
}, },
capabilities: { capabilities: {
can_stop: worker.availability !== 'unavailable' && worker.state !== 'stopped', can_stop: worker.availability !== "unavailable" &&
worker.state !== "stopped",
can_spawn_followup: false, 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, has_running_internal_workers: worker.has_running_internal_workers,
diagnostics: [], 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);
}