diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 2d157d72..21cbc0bc 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -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 --allow-env=LOG,VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.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 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": "deno test --allow-read=src,test --allow-env=LOG,VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repository-access/ui.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.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 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", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, diff --git a/web/workspace/src/lib/workspace/settings/model.test.ts b/web/workspace/src/lib/workspace/settings/model.test.ts index 493fda14..645fc09b 100644 --- a/web/workspace/src/lib/workspace/settings/model.test.ts +++ b/web/workspace/src/lib/workspace/settings/model.test.ts @@ -53,6 +53,19 @@ Deno.test("settings shell advertises scoped account authority", () => { ); }); +Deno.test("Repository settings expose the canonical list and Add route", () => { + const section = SETTINGS_SECTIONS.find((entry) => entry.id === "repositories"); + assert(section?.status === "editable", "Repositories should be editable"); + assert( + settingsSectionHref("repositories") === "/settings/repositories", + "Repositories should have a dedicated settings route", + ); + assert( + section?.bullets.join("\n").includes("without network access"), + "Repository registration copy should preserve the no-network boundary", + ); +}); + Deno.test("Repository access settings are editable and canonically routed", () => { const section = SETTINGS_SECTIONS.find((entry) => entry.id === "repository-access" diff --git a/web/workspace/src/lib/workspace/settings/model.ts b/web/workspace/src/lib/workspace/settings/model.ts index 620ee05f..97c1f68c 100644 --- a/web/workspace/src/lib/workspace/settings/model.ts +++ b/web/workspace/src/lib/workspace/settings/model.ts @@ -7,6 +7,7 @@ export type Diagnostic = { export type SettingsSectionId = | "runtimes" | "configuration-sources" + | "repositories" | "repository-access" | "profile-sources" | "workspace-identity"; @@ -54,6 +55,18 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ "Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.", ], }, + { + id: "repositories", + label: "Repositories", + status: "editable", + summary: + "Register the local and remote Git repositories available to this Workspace.", + bullets: [ + "Repository identity and source registration are Workspace-scoped Server authority.", + "Adding a Repository performs validation and persistence without network access.", + "SSH credentials and pinned host trust remain separate Repository Access resources.", + ], + }, { id: "repository-access", label: "Repository Access", @@ -116,6 +129,8 @@ export function settingsSectionHref(id: SettingsSectionId): string { return `${SETTINGS_ROUTE}/runtimes`; case "configuration-sources": return `${SETTINGS_ROUTE}/configuration`; + case "repositories": + return `${SETTINGS_ROUTE}/repositories`; case "repository-access": return `${SETTINGS_ROUTE}/repository-access`; case "profile-sources": diff --git a/web/workspace/src/lib/workspace/styles/settings.css b/web/workspace/src/lib/workspace/styles/settings.css index 5ed3cba6..0a1d1b82 100644 --- a/web/workspace/src/lib/workspace/styles/settings.css +++ b/web/workspace/src/lib/workspace/styles/settings.css @@ -680,6 +680,96 @@ } } + .repositories-page { + display: grid; + gap: var(--space-5); + } + .settings-repository-form { + display: grid; + gap: var(--space-4); + padding: var(--space-5); + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-raised); + } + .settings-repository-form h2 { + margin: 0; + } + .settings-repository-form label { + display: grid; + gap: var(--space-2); + color: var(--text-muted); + font-size: 0.78rem; + font-weight: 700; + } + .settings-repository-form input { + min-width: 0; + border: 1px solid var(--line); + border-radius: 0.45rem; + background: var(--bg); + color: var(--text-strong); + padding: 0.65rem 0.75rem; + font: inherit; + font-family: var(--font-mono); + } + .settings-repository-form small, + .settings-repository-table small { + color: var(--text-muted); + font-weight: 400; + } + .settings-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-4); + } + .settings-form-field-wide { + grid-column: 1 / -1; + } + .settings-repository-table-wrap { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-raised); + } + .settings-repository-table { + width: 100%; + border-collapse: collapse; + } + .settings-repository-table th, + .settings-repository-table td { + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: top; + } + .settings-repository-table th { + color: var(--text-muted); + font-size: 0.72rem; + text-transform: uppercase; + } + .settings-repository-table tbody tr:last-child td { + border-bottom: 0; + } + .settings-repository-table td:first-child, + .settings-repository-table td:nth-child(2) { + display: grid; + gap: var(--space-1); + } + .settings-repository-table code { + overflow-wrap: anywhere; + } + .settings-muted-action { + color: var(--text-muted); + } + @media (max-width: 48rem) { + .settings-form-grid { + grid-template-columns: 1fr; + } + .settings-form-field-wide { + grid-column: auto; + } + } + .status-message.error { color: var(--danger); } diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/repositories/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/repositories/+page.svelte new file mode 100644 index 00000000..3ba35527 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/settings/repositories/+page.svelte @@ -0,0 +1,152 @@ + + + + Repositories · Settings · Yoi Workspace + + + +
+
+
+

owner only

+

Repositories

+

Register the local and remote Git repositories available to this Workspace.

+
+ +
+ + {#if showAddRepository} +
+

Add Repository

+
+ + + + +
+

+ SSH credentials and pinned host keys are managed separately in + Repository Access. +

+
+ + +
+
+ {/if} + + {#if requestError} +

{requestError}

+ {/if} + + {#if data.repositoriesError} +

{data.repositoriesError}

+ {:else if !data.repositories} +

Loading Repositories…

+ {:else if data.repositories.items.length === 0} +

No Repositories are registered.

+ {:else} +
+ + + + + + + + + + + + {#each data.repositories.items as repository (repository.repository_id)} + + + + + + + + {/each} + +
RepositorySourceDefault refStatusAccess
+ + {repository.display_name} + + {repository.repository_id} + + {repository.source.kind === 'local_path' ? 'Local' : 'Remote Git'} + {repository.source.uri} + {repository.default_selector ?? '—'}{repository.observed.status} + {#if repository.source.kind === 'remote_git'} + Configure access + {:else} + Not required + {/if} +
+
+ {/if} +
diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/repositories/+page.ts b/web/workspace/src/routes/w/[workspaceId]/settings/repositories/+page.ts new file mode 100644 index 00000000..807b4224 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/settings/repositories/+page.ts @@ -0,0 +1,38 @@ +import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import type { PageLoad } from "./$types"; + +export type RepositorySource = { + kind: "local_path" | "remote_git"; + uri: string; +}; + +export type RepositorySummary = { + repository_id: string; + display_name: string; + kind: string; + provider: string; + source: RepositorySource; + default_selector?: string | null; + observed: { + status: string; + observed_at?: string | null; + }; + diagnostics: Array<{ code: string; message: string }>; +}; + +type RepositoryListResponse = { + items: RepositorySummary[]; + diagnostics: Array<{ code: string; message: string }>; +}; + +export const load: PageLoad = async ({ fetch, params }) => { + const repositories = await loadJson( + fetch, + workspaceApiPath(params.workspaceId, "/repositories"), + ); + return { + workspaceId: params.workspaceId, + repositories: repositories.data, + repositoriesError: repositories.error, + }; +}; diff --git a/web/workspace/test/repositories/ui.test.ts b/web/workspace/test/repositories/ui.test.ts new file mode 100644 index 00000000..e826b984 --- /dev/null +++ b/web/workspace/test/repositories/ui.test.ts @@ -0,0 +1,56 @@ +type TestRegistrar = (name: string, fn: () => void | Promise) => void; + +const test = + (globalThis as unknown as { Deno: { test: TestRegistrar } }).Deno.test; + +function assert(condition: boolean, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +const pageSource = await Deno.readTextFile( + new URL( + "../../src/routes/w/[workspaceId]/settings/repositories/+page.svelte", + import.meta.url, + ), +); +const loadSource = await Deno.readTextFile( + new URL( + "../../src/routes/w/[workspaceId]/settings/repositories/+page.ts", + import.meta.url, + ), +); + +test("Repository settings use the scoped list and typed create collection", () => { + for (const token of [ + "workspaceApiPath(params.workspaceId, \"/repositories\")", + "workspaceApiPath(data.workspaceId, '/repositories')", + "method: 'POST'", + "repository_id: repositoryId", + "display_name: displayName", + "default_ref: defaultRef || null", + "await invalidateAll()", + ]) { + assert( + pageSource.includes(token) || loadSource.includes(token), + `Repository settings should include ${token}`, + ); + } +}); + +test("Repository Add form keeps access secrets outside registration input", () => { + for (const forbidden of [ + "private_key", + "passphrase", + "credential_id", + "host_trust_id", + ]) { + assert( + !pageSource.includes(forbidden), + `Repository registration must not accept ${forbidden}`, + ); + } + assert( + pageSource.includes("/settings/repository-access"), + "remote Repository rows should link to Repository Access", + ); +});