Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 1 addition & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
version: 2
updates:
- package-ecosystem: bundler
directory: "/hub"
schedule:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: npm
directories:
- "/hub"
- "/actions/build"
- "/starter"
- "/scripts"
schedule:
interval: weekly
cooldown:
Expand Down
12 changes: 7 additions & 5 deletions .github/extensions/replica-prototype/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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.

Local preview and Controls work without Hub. The [new Hub](../../../hub/README.md) cannot publish prototypes or accept comments yet. The extension keeps its Hub connection features for compatible existing services, but the new Hub does not support them or provide sign-in, storage, or deployment setup.

## Local prototype controls

**Controls** in the canvas toolbar reads `/controls.replica.json` from the preview and binds its eleven supported field types to ordinary app behavior. Existing sets, flat groups and conditions render in the shared panel. Controls work independently of Hub and comments; dismissal keeps the same iframe and route.
Expand All @@ -27,7 +29,7 @@ copilot plugin install replica-prototype@replica

Use `github/replica#main` to select `main` explicitly.

To pin an immutable release, replace `<approved-release-tag>` with an approved `hub-v*` tag containing the catalog:
To keep using one release, replace `<approved-release-tag>` with an approved existing tag containing the catalog. The new Hub has no release workflow:

```sh
copilot plugin marketplace add 'github/replica#<approved-release-tag>'
Expand Down Expand Up @@ -76,7 +78,7 @@ New improvements remain local until their captured source matches a successful p

## Learnings

When this repository has a Hub connection and a Hub administrator connected Copilot, each prompt you submit gets the [learnings](../../../hub/docs/learnings.md) your repository's administrators accepted in Hub. The extension never sends your prompt to Hub. Copilot treats the learnings as guidance from your team, not as commands. Its first successful answer replaces earlier learning context, including after a restart or session resume; an empty answer explicitly clears it. Later unchanged answers add nothing. When learnings change, or Learnings is turned off, the next successful answer replaces the earlier guidance. It adds nothing when Hub's answer is not in the form that `replica-learning` writes. It never delays a prompt by more than three seconds and never fails it. Without a Hub connection it adds nothing and never asks you to connect. If you connected before the approval page mentioned learnings, disconnect and connect again to get them.
When connected to a compatible external Hub service with Copilot configured, each prompt gets the accepted [learnings](../../../packages/learning/README.md) that service returns. The extension never sends your prompt to Hub. Copilot treats the learnings as guidance from your team, not as commands. Its first successful answer replaces earlier learning context, including after a restart or session resume; an empty answer explicitly clears it. Later unchanged answers add nothing. When learnings change, or Learnings is turned off, the next successful answer replaces the earlier guidance. It adds nothing when Hub's answer is not in the form that `replica-learning` writes. It never delays a prompt by more than three seconds and never fails it. Without a Hub connection it adds nothing and never asks you to connect. If you connected before the approval page mentioned learnings, disconnect and connect again to get them.

Copilot CLI runs extensions only while its extensions feature is on. In Copilot CLI 1.0.96 that is an experimental feature: start Copilot CLI with `--experimental`, or run `/experimental on`. In prompt mode (`copilot -p`), it loads a repository's project copy only when `GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS=true`.

Expand Down Expand Up @@ -248,7 +250,7 @@ Teams can configure their Hub by merging `.github/replica.json` into the prototy

This is separate from `.github/github-app.yml`; no startup script or app-specific configuration key is required. With `gh` available and authenticated for the origin repository, opening the canvas discovers its configured Hub and offers **Connect to the hub** without URL entry. Branch edits and repository hints cannot override that configuration. Discovery uses a five-minute cache; the **Reset to repository Hub URL** icon beside the editable URL checks again immediately and fills the draft. **Save Hub** is still required to switch. Invalid or unavailable configuration is shown separately from missing configuration.

The same file may contain a `templates` array for [Hub's Templates gallery](https://github.com/github/replica/blob/main/hub/docs/gallery.md). Discovery validates only `hub.origin`, not the full Hub configuration schema. Other fields, including `version`, the `templates` catalog, and extra Hub settings, are ignored and preserved. Discovery has no configuration size limit, but requires a complete, nontruncated JSON blob. Template routes never select a Hub.
The same file may contain a `templates` array retained for compatible Hub services. The [new Hub](../../../hub/README.md) has no gallery. Discovery validates only `hub.origin`, not the full Hub configuration schema. Other fields, including `version`, the `templates` catalog, and extra Hub settings, are ignored and preserved. Discovery has no configuration size limit, but requires a complete, nontruncated JSON blob. Template routes never select a Hub.

A missing file, missing or null `hub.origin`, or blank URL falls back to manual Hub URL entry when no saved selection or explicit environment origin exists. A configured URL must be a valid origin without credentials, paths, query strings, or fragments; remote origins require HTTPS. Malformed JSON and invalid configured URLs remain explicit errors, with manual URL entry still available.

Expand All @@ -262,7 +264,7 @@ Seven-day access tokens renew automatically while repository access remains vali

The canvas follows branch changes. If Hub has not discovered the branch, the repository remains connected and comments can be saved locally; push the branch and wait for discovery to use its published Builds and reviews. Once Hub imports a successful Build of the current checkout, the canvas automatically registers eligible prototypes using the branch name as the initial title. Existing metadata is preserved. Use **Edit metadata** in **Hub** to change the title or description afterward. Registration requires write access and Hub's branch eligibility checks; errors appear in the shared critical banner and status polling retries automatically. Updated Hub APIs supply a `registrationReason` code explaining missing write access, excluded branches, missing remote heads, or branch authorship requirements. Unknown codes and older Hubs retain a generic error; independently verified permission and remote-ref facts can supply a specific explanation. Installing the extension does not deploy the updated Hub API.

For a Hub started in [local development mode](https://github.com/github/replica/blob/main/hub/docs/local-development.md), open Copilot in Hub's `.local/prototypes/example` checkout and pass `"repository": "local/example"` when opening the canvas. Local repository hints only apply to matching checkouts under `.local/prototypes/` with a loopback Hub origin; they cannot redirect a remote checkout to another Hub repository. Start that example's dev server on a port reachable from the canvas before opening it.
Open the canvas in an independent starter checkout for local preview. The [new Hub](../../../hub/README.md) does not create example repositories or support publishing and review. Existing local-repository hints apply only to matching checkouts under `.local/prototypes/` with a compatible Hub running on the same computer; they cannot redirect a remote checkout to another Hub repository.

Selecting **Publish** checks for uncommitted changes and, when present without a saved commit preference, opens a dialog offering **Commit changes & publish** or **Publish committed version only**. Dismiss it with the close icon or Escape. The dialog lists the changed paths. Local agent skills under `.github/skills/`, `.agents/skills/`, or `.claude/skills/` do not count as prototype changes and stay uncommitted. Copilot selects task-related changes only when committing is chosen, and publishes the current commit when none are found. **Always commit changes before publishing for this prototype** remembers this permission locally for the current repository and branch, not other prototypes; later Publish clicks on that branch skip the choice and request task-related commits. After the first registered, published Build, change it through the immediately saved toggle under **Settings** at the bottom of **Edit details**. The committed-only option excludes local edits. Clean checkouts need no choice. See the [canonical publishing boundary](../../../hub/docs/prototype-canvas.md#publishing-boundary) for preference scope, failures, and target safeguards.

Expand All @@ -280,7 +282,7 @@ Copilot uses the existing push workflow, not manual Actions dispatch or another

**Add a comment** targets an element or highlighted area. Comments on unpublished changes stay on the author's device until a matching Build is published; comments on a clean checkout of the published Build save directly to Hub. See [local preview comments](../../../hub/docs/prototype-canvas.md#local-preview-comments) for requirements, storage and recovery.

The header's comment count opens a sidebar with Hub's search, filters, unread state and pin visibility controls. Select a marker or row to open its conversation beside the target. Published conversations support replies, edits, deletion, reactions and resolution. Hub sends live updates, with periodic refresh when unavailable. The **Hub** panel remains focused on connection, metadata and publication. Hub credentials never reach the local preview.
The header's comment count opens a sidebar with Hub's search, filters, unread state and pin visibility controls. Select a marker or row to open its conversation beside the target. Published conversations support replies, edits, deletion, reactions and resolution when connected to a compatible Hub. This branch removes the old live-update connection; the sidebar keeps its existing periodic refresh. The new Hub API does not yet support reviews. The **Hub** panel remains focused on connection, metadata and publication. Hub credentials never reach the local preview.

Open an already-running unauthenticated loopback prototype in this canvas. The extension's per-panel reverse proxy supplies its bundled bridge to ordinary Vite dev/preview and static HTML servers. Multiple prototypes, including servers in the same checkout, have independent proxy ports and lifecycles. Closing a panel stops only its proxy.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,52 +744,5 @@ export function createHubClient(context, options = {}) {
throw error
}
},
async subscribe(onChange, onStop) {
const status = await this.status()
if (status.state !== "connected") throw new Error("Connect to a discovered Hub prototype.")
const { cableUrl, protocol } = await this.request("/api/canvas/cable_ticket", {
method: "POST",
})
const expected = new URL(origin.origin.replace(/^http/, "ws"))
if (
typeof cableUrl !== "string" ||
new URL(cableUrl).origin !== expected.origin ||
typeof protocol !== "string" ||
!protocol.startsWith("replica-canvas.")
) {
throw new Error("Hub returned an invalid review connection.")
}
const socket = new WebSocket(cableUrl, ["actioncable-v1-json", protocol])
const identifier = JSON.stringify({
channel: "ReviewCommentsChannel",
repositoryId: status.prototype.repositoryId,
prototypeId: status.prototype.id,
})
socket.addEventListener("open", () =>
socket.send(JSON.stringify({ command: "subscribe", identifier })),
)
socket.addEventListener("message", (event) => {
try {
const message = JSON.parse(String(event.data))
if (message == null) throw new Error("Hub returned an invalid review message.")
const data = readHubObject(message)
if (
data?.identifier === identifier &&
readHubObject(data.message)?.type === "review_comments_changed"
)
onChange()
if (data?.type === "reject_subscription" && data.identifier === identifier) {
onStop(new Error("Hub rejected the review subscription."))
socket.close()
}
} catch (error) {
onStop(readHubError(error))
socket.close()
}
})
socket.addEventListener("error", () => onStop(new Error("Hub review connection failed.")))
socket.addEventListener("close", () => onStop(new Error("Hub review connection closed.")))
return socket
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -876,52 +876,5 @@ export function createHubClient(context: HubContext, options: HubClientOptions =
throw error
}
},
async subscribe(onChange: () => void, onStop: (error: Error) => void) {
const status = await this.status()
if (status.state !== "connected") throw new Error("Connect to a discovered Hub prototype.")
const { cableUrl, protocol } = await this.request("/api/canvas/cable_ticket", {
method: "POST",
})
const expected = new URL(origin.origin.replace(/^http/, "ws"))
if (
typeof cableUrl !== "string" ||
new URL(cableUrl).origin !== expected.origin ||
typeof protocol !== "string" ||
!protocol.startsWith("replica-canvas.")
) {
throw new Error("Hub returned an invalid review connection.")
}
const socket = new WebSocket(cableUrl, ["actioncable-v1-json", protocol])
const identifier = JSON.stringify({
channel: "ReviewCommentsChannel",
repositoryId: status.prototype.repositoryId,
prototypeId: status.prototype.id,
})
socket.addEventListener("open", () =>
socket.send(JSON.stringify({ command: "subscribe", identifier })),
)
socket.addEventListener("message", (event) => {
try {
const message: unknown = JSON.parse(String(event.data))
if (message == null) throw new Error("Hub returned an invalid review message.")
const data = readHubObject(message)
if (
data?.identifier === identifier &&
readHubObject(data.message)?.type === "review_comments_changed"
)
onChange()
if (data?.type === "reject_subscription" && data.identifier === identifier) {
onStop(new Error("Hub rejected the review subscription."))
socket.close()
}
} catch (error) {
onStop(readHubError(error))
socket.close()
}
})
socket.addEventListener("error", () => onStop(new Error("Hub review connection failed.")))
socket.addEventListener("close", () => onStop(new Error("Hub review connection closed.")))
return socket
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -356,48 +356,13 @@ esac
repositoryId: 5,
branch: new URL(request.url, "http://localhost").searchParams.get("branch"),
})
} else if (request.url === "/api/canvas/cable_ticket") {
json({
cableUrl: `ws://127.0.0.1:${server.address().port}/cable`,
protocol: "replica-canvas.signed-ticket",
})
} else if (request.url === "/api/canvas/connection" && request.method === "DELETE") {
response.writeHead(204)
response.end()
} else {
json({ error: "Unexpected request." }, 404)
}
})
server.on("upgrade", (request, socket) => {
assert.equal(request.headers.origin, undefined)
assert.match(request.headers["sec-websocket-protocol"], /replica-canvas\.signed-ticket/)
const accept = createHash("sha1")
.update(`${request.headers["sec-websocket-key"]}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
.digest("base64")
socket.write(
`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\nSec-WebSocket-Protocol: actioncable-v1-json\r\n\r\n`,
)
socket.once("data", () => {
const identifier = JSON.stringify({
channel: "ReviewCommentsChannel",
repositoryId: 5,
prototypeId: 4,
})
const payload = Buffer.from(
JSON.stringify({
identifier,
message: { type: "review_comments_changed" },
}),
)
socket.write(
Buffer.concat([
Buffer.from([0x81, 126, payload.length >> 8, payload.length & 255]),
payload,
]),
)
setTimeout(() => socket.end(), 100)
})
})
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
try {
await writeFile(
Expand Down Expand Up @@ -461,30 +426,6 @@ esac
assert.equal((await client.status()).state, "disconnected")
assert.equal(calls.filter(({ auth }) => auth === "Bearer hub-secret").length, 3)
assert.ok(!calls[0].auth)

await client.pair()
await client.poll()
await new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("No review invalidation arrived.")),
2_000,
)
void client
.subscribe(
() => {
clearTimeout(timeout)
resolve()
},
(error) => {
clearTimeout(timeout)
reject(error)
},
)
.catch((error) => {
clearTimeout(timeout)
reject(error)
})
})
} finally {
process.env.PATH = originalPath
delete process.env.TEST_BRANCH
Expand Down
Loading
Loading