Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/extensions/replica-prototype/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Replica prototype canvas

The Replica prototype canvas puts a local prototype in the Copilot app alongside repository status and a Publish control. Open it from **+ → Canvas** to find running HTTP servers in this checkout, then select **Connect** beside the one you want. When none is running, **Start dev server** is the primary action beside **Scan again**. Opening a canvas never executes a repository script; starting requires your click and does not send a chat request.
The Replica prototype canvas puts a local prototype in the Copilot app alongside repository status and a Publish control. Open it from **+ → Canvas**. Without a running server, the canvas keeps its toolbar and shows Hub's creation form with optional details and template selection. An existing repository connection is reused without another approval. Submitting sends the brief to the current conversation. The outlined section below the form offers **Start prototype**, **Scan again**, and **Connect** for servers in this checkout. Opening a canvas never executes a repository script; starting requires your click and does not send a chat request. See [the creation and connection states](../../../hub/docs/prototype-canvas.md#before-a-prototype-is-running).

## Local prototype controls

Expand Down Expand Up @@ -84,7 +84,7 @@ Copilot CLI runs extensions only while its extensions feature is on. In Copilot

For every connected user with repository access, including Read and Triage, **More actions → Improve context** requests the bundled `/review-hub-feedback` skill in the current chat. Hub checks access again on click. The skill identifies reusable feedback themes; it does not generate solutions or edit context. See [feedback review](../../../hub/docs/prototype-canvas.md#feedback-review) for access and recovery.

Agents can use actions on the existing `replica-prototype` canvas to retrieve conversations for the current prototype or every reviewable prototype in the checkout's repository, across all Builds. Both use the approved repository connection. Repository retrieval requires a Hub deployment with the repository comments API; it needs no migration, MCP server or separate comment store. Open the canvas, but no running or connected local preview is required for comment actions. They reuse the saved Hub selection and approved credentials; missing authorization still requires connecting to Hub. Preview interactions and publishing still require a connected local preview.
Agents can use actions on the existing `replica-prototype` canvas to retrieve conversations for the current prototype or every reviewable prototype in the checkout's repository, across all Builds. Both use the approved repository connection. Repository retrieval requires a Hub deployment with the repository comments API; it needs no migration, MCP server or separate comment store. Open the canvas, but no running or connected local preview is required for comment actions. They reuse the saved Hub selection and approved credentials; missing authorization still requires connecting to Hub. Preview interactions require a connected local preview; repository status, publication, metadata, and sharing remain available without one.

Before a local preview is connected, each comment action resolves the current checkout and saved Hub selection afresh. Changing that selection does not require reopening the canvas; continuations from a different Hub or branch are rejected as stale.

Expand Down Expand Up @@ -225,7 +225,7 @@ Treat the complete canvas URL as a secret. The document capability is separate f

### Start a dev server

When an empty scan finds a supported `dev`, `start`, or `serve` script in the current directory's `package.json`, the panel shows **Start dev server** beside **Scan again**. If several scripts qualify, choose one from the script selector. Clicking starts repository code, including package-manager lifecycle hooks; only do this for a checkout you trust. The manifest and lockfiles are rechecked before execution; changed scripts require a new scan.
When a scan finds a supported `dev`, `start`, or `serve` script in the current directory's `package.json`, the panel shows **Start prototype** beside **Scan again**. If several scripts qualify, choose one from the script selector. Clicking starts repository code, including package-manager lifecycle hooks; only do this for a checkout you trust. The manifest and lockfiles are rechecked before execution; changed scripts require a new scan.

Automatic startup recognizes direct Vite, Next.js, Astro, webpack dev-server, Vue CLI, and Create React App commands. It does not search subdirectories or infer monorepo workspace targets, compound shell commands, or custom wrappers. The package manager comes from `packageManager`, then the lockfile, then npm if neither is present. Conflicting package-manager declarations stop startup. Missing runtimes, package managers, and dependencies require manual setup; the extension does not install them or allow Corepack to download a manager.

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions .github/extensions/replica-prototype/assets/renderer-vendor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/extensions/replica-prototype/assets/renderer.css

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions .github/extensions/replica-prototype/assets/renderer.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export async function createCanvasContext({
"This Copilot session has no working directory, so repository status cannot be read.",
)
}
const normalizedUrl = normalizePrototypeUrl(prototypeUrl)
const normalizedUrl = prototypeUrl == null ? null : normalizePrototypeUrl(prototypeUrl)
if (typeof improvementThreadsSample !== "boolean")
throw new Error("improvementThreadsSample must be a boolean.")
const stateUrl = new URL(normalizedUrl)
stateUrl.hash = ""
const stateUrl = normalizedUrl ? new URL(normalizedUrl) : null
if (stateUrl) stateUrl.hash = ""
const resolvedRepositoryPath = path.resolve(repositoryPath)
const target = await resolvePublishTarget({
repository,
Expand All @@ -41,7 +41,7 @@ export async function createCanvasContext({
.update(
[
path.resolve(sessionWorkspacePath),
stateUrl.toString(),
stateUrl?.toString() ?? "disconnected",
target.repository || "",
target.branch || "",
target.commitSha || "",
Expand All @@ -60,7 +60,7 @@ export async function createCanvasContext({
stateDirectory,
stateFile: path.join(stateDirectory, `${stateId}.json`),
stateId,
urlHost: new URL(normalizedUrl).host,
urlHost: normalizedUrl ? new URL(normalizedUrl).host : null,
...(improvementThreadsSample ? { improvementThreadsSample: true } : {}),
}
}
Expand Down Expand Up @@ -113,7 +113,7 @@ function normalizePrototypeUrl(value) {
}
function normalizeName(name, normalizedUrl) {
if (name === undefined) {
return new URL(normalizedUrl).hostname
return normalizedUrl ? new URL(normalizedUrl).hostname : "New prototype"
}
if (typeof name !== "string" || name.trim() === "") {
throw new Error("name must contain visible text.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { readRepositoryName } from "../read-repository-name/index.mjs"
import { runCommand } from "../run-command/index.mjs"

type CanvasContextInput = {
prototypeUrl: string
prototypeUrl?: string | null
name?: unknown
repository?: unknown
branch?: unknown
Expand Down Expand Up @@ -47,11 +47,11 @@ export async function createCanvasContext({
)
}

const normalizedUrl = normalizePrototypeUrl(prototypeUrl)
const normalizedUrl = prototypeUrl == null ? null : normalizePrototypeUrl(prototypeUrl)
if (typeof improvementThreadsSample !== "boolean")
throw new Error("improvementThreadsSample must be a boolean.")
const stateUrl = new URL(normalizedUrl)
stateUrl.hash = ""
const stateUrl = normalizedUrl ? new URL(normalizedUrl) : null
if (stateUrl) stateUrl.hash = ""
const resolvedRepositoryPath = path.resolve(repositoryPath)
const target = await resolvePublishTarget({
repository,
Expand All @@ -64,7 +64,7 @@ export async function createCanvasContext({
.update(
[
path.resolve(sessionWorkspacePath),
stateUrl.toString(),
stateUrl?.toString() ?? "disconnected",
target.repository || "",
target.branch || "",
target.commitSha || "",
Expand All @@ -84,7 +84,7 @@ export async function createCanvasContext({
stateDirectory,
stateFile: path.join(stateDirectory, `${stateId}.json`),
stateId,
urlHost: new URL(normalizedUrl).host,
urlHost: normalizedUrl ? new URL(normalizedUrl).host : null,
...(improvementThreadsSample ? { improvementThreadsSample: true } : {}),
}
}
Expand Down Expand Up @@ -141,9 +141,9 @@ function normalizePrototypeUrl(value: string) {
return url.toString()
}

function normalizeName(name: unknown, normalizedUrl: string) {
function normalizeName(name: unknown, normalizedUrl: string | null) {
if (name === undefined) {
return new URL(normalizedUrl).hostname
return normalizedUrl ? new URL(normalizedUrl).hostname : "New prototype"
}
if (typeof name !== "string" || name.trim() === "") {
throw new Error("name must contain visible text.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ const base = {
}

describe("createCanvasContext()", () => {
it("retains the verified checkout without requiring a running prototype", async () => {
await withContext(async (fixture) => {
const input = { ...base, ...paths(fixture), prototypeUrl: undefined, name: undefined }
const context = await createCanvasContext(input)
assert.equal(context.normalizedUrl, null)
assert.equal(context.name, "New prototype")
assert.equal(context.target.repository, "github/replica")
assert.equal(context.target.branch, "prototype")
assert.equal((await createCanvasContext(input)).stateId, context.stateId)
execFileSync("git", ["checkout", "-qb", "feature/new-prototype"], {
cwd: fixture.repositoryPath,
})
const current = await currentCanvasContext(context)
assert.equal(current.normalizedUrl, null)
assert.equal(current.target.branch, "feature/new-prototype")
assert.notEqual(current.stateId, context.stateId)
})
})

it("keeps thread samples explicitly opted in without changing saved-note identity", async () => {
await withContext(async (fixture) => {
const input = { ...base, ...paths(fixture) }
Expand Down
Loading
Loading