Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
06f70a0
refactor(canvas): establish TypeScript extension foundation
lukasoppermann Oct 21, 2026
edfef38
refactor(canvas): migrate shared extension infrastructure
lukasoppermann Oct 21, 2026
c1869b9
refactor(canvas): migrate Hub connection modules to TypeScript
lukasoppermann Oct 21, 2026
eb94b7d
refactor(canvas): type publication and state orchestration
lukasoppermann Oct 21, 2026
816d8d4
refactor(canvas): type collaboration and diagnostics
lukasoppermann Oct 21, 2026
6d665a4
fix(canvas): validate saved comment anchors
lukasoppermann Oct 22, 2026
80d9a74
fix(canvas): accept null pairing refresh tokens
lukasoppermann Oct 22, 2026
0130a1e
fix(canvas): validate saved publication evidence
lukasoppermann Oct 22, 2026
87da450
refactor(canvas): migrate Hub connection modules to TypeScript
lukasoppermann Oct 21, 2026
552b466
fix(canvas): accept null pairing refresh tokens
lukasoppermann Oct 22, 2026
373f011
refactor(canvas): type publication and state orchestration
lukasoppermann Oct 21, 2026
f878199
refactor(canvas): type collaboration and diagnostics
lukasoppermann Oct 21, 2026
4b03275
fix(canvas): validate saved publication evidence
lukasoppermann Oct 22, 2026
10c6051
fix(canvas): validate saved comment anchors
lukasoppermann Oct 22, 2026
fdaeb9c
refactor(canvas): type local runtime and server lifecycle
lukasoppermann Oct 21, 2026
e652cca
Merge remote-tracking branch 'origin/lukasoppermann-canvas-typescript…
lukasoppermann Oct 22, 2026
77abc01
Merge main into canvas Hub connection
lukasoppermann Oct 22, 2026
424f989
merge: integrate latest canvas publication head
lukasoppermann Oct 22, 2026
457b508
merge: update canvas publication from hub connection base
lukasoppermann Oct 22, 2026
f7d437c
build(canvas): refresh generated publish target
lukasoppermann Oct 22, 2026
e69a68f
merge: align collaboration PR head
lukasoppermann Oct 22, 2026
8dcd14c
Merge latest main into canvas Hub connection
lukasoppermann Oct 22, 2026
d101ddf
merge: update collaboration against publication base
lukasoppermann Oct 22, 2026
b154b27
fix(extension): merge latest canvas collaboration changes
lukasoppermann Oct 22, 2026
11891d2
Merge commit '8dcd14c4bcd319a7d5dbb87ccf5c2c7c15c51270' into lukasopp…
lukasoppermann Oct 22, 2026
a06593f
fix(extension): serve improvement note renderer chunks
lukasoppermann Oct 22, 2026
8f1bac4
merge: sync collaboration with publication parent
lukasoppermann Oct 22, 2026
8d42387
Merge remote-tracking branch 'refs/remotes/origin/lukasoppermann-canv…
lukasoppermann Oct 22, 2026
0dff6e5
merge: sync publication branch with main
lukasoppermann Oct 22, 2026
b6915b0
merge: sync collaboration with current publication base
lukasoppermann Oct 22, 2026
5ee8398
merge: sync collaboration with main
lukasoppermann Oct 22, 2026
8f0a59d
fix(extension): merge latest collaboration base
lukasoppermann Oct 22, 2026
5711d9c
Merge remote-tracking branch 'refs/remotes/origin/lukasoppermann-canv…
lukasoppermann Oct 22, 2026
724159a
fix(extension): merge retargeted main base
lukasoppermann Oct 22, 2026
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
21 changes: 10 additions & 11 deletions .github/extensions/replica-prototype/agent-commands/index.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { randomUUID } from "node:crypto"

import { readHubObject } from "../read-hub-object/index.mjs"
// Allow the 8 KiB Controls query plus the underlying prototype route.
export const MAX_RELOAD_ADDRESS_LENGTH = 16_384

export function agentCommands() {
let pending = null
const queue = []
const history = []

function stage(command, name) {
command.trace.push({ stage: name, elapsedMs: Date.now() - command.queuedAt })
}
Expand All @@ -20,24 +18,23 @@ export function agentCommands() {
stages: command.trace.map((entry) => ({ ...entry })),
}
}

function startNext() {
if (pending || !queue.length) return
if (pending) return
const command = queue.shift()
if (!command) return
const timer = setTimeout(() => {
pending = null
command.outcome = "timeout"
command.reject(
new Error(
`The live canvas did not respond. Request ${command.id}; last stage: ${command.trace.at(-1).stage}. Keep its panel open and try again.`,
`The live canvas did not respond. Request ${command.id}; last stage: ${command.trace.at(-1)?.stage}. Keep its panel open and try again.`,
),
)
startNext()
}, command.timeoutMs)
command.timer = timer
pending = command
}

return {
invoke(type, input = {}, timeoutMs = 5_000) {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -70,17 +67,19 @@ export function agentCommands() {
stage(pending, "delivered")
return { id: pending.id, type: pending.type, input: pending.input }
},
progress({ id, stage: name }, rendererId) {
progress(input, rendererId) {
const { id, stage: name } = readHubObject(input) ?? {}
if (!pending || pending.id !== id || pending.rendererId !== rendererId)
throw new Error("No matching canvas action is pending for this renderer.")
const stages = ["handler started", "status collected"]
if (!stages.includes(name)) throw new Error("Invalid canvas action stage.")
if (name !== "handler started" && name !== "status collected")
throw new Error("Invalid canvas action stage.")
if (!pending.trace.some((entry) => entry.stage === name)) stage(pending, name)
},
diagnostics() {
return { requests: history.map(diagnostic) }
},
complete(result, rendererId) {
complete(input, rendererId) {
const result = readHubObject(input)
if (!pending || result?.id !== pending.id || typeof result.ok !== "boolean") {
throw new Error("No matching canvas action is pending.")
}
Expand Down
136 changes: 136 additions & 0 deletions .github/extensions/replica-prototype/agent-commands/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { randomUUID } from "node:crypto"
import { readHubObject } from "../read-hub-object/index.mjs"

// Allow the 8 KiB Controls query plus the underlying prototype route.
export const MAX_RELOAD_ADDRESS_LENGTH = 16_384

type ActionStage =
"queued" | "delivered" | "handler started" | "status collected" | "reply received"
type Command = {
id: string
type: string
input: unknown
timeoutMs: number
resolve: (value: unknown) => void
reject: (error: Error) => void
queuedAt: number
trace: { stage: ActionStage; elapsedMs: number }[]
timer?: ReturnType<typeof setTimeout>
sent?: boolean
rendererId?: string
outcome?: "timeout" | "completed" | "failed" | "cancelled"
}
export type AgentCommands = ReturnType<typeof agentCommands>

export function agentCommands() {
let pending: Command | null = null
const queue: Command[] = []
const history: Command[] = []

function stage(command: Command, name: ActionStage) {
command.trace.push({ stage: name, elapsedMs: Date.now() - command.queuedAt })
}
function diagnostic(command: Command) {
return {
id: command.id,
type: command.type,
rendererId: command.rendererId ?? null,
outcome: command.outcome ?? "pending",
stages: command.trace.map((entry) => ({ ...entry })),
}
}

function startNext() {
if (pending) return
const command = queue.shift()
if (!command) return
const timer = setTimeout(() => {
pending = null
command.outcome = "timeout"
command.reject(
new Error(
`The live canvas did not respond. Request ${command.id}; last stage: ${command.trace.at(-1)?.stage}. Keep its panel open and try again.`,
),
)
startNext()
}, command.timeoutMs)
command.timer = timer
pending = command
}

return {
invoke(type: string, input: unknown = {}, timeoutMs = 5_000) {
return new Promise<unknown>((resolve, reject) => {
const command: Command = {
id: randomUUID(),
type,
input,
timeoutMs,
resolve,
reject,
queuedAt: Date.now(),
trace: [],
}
stage(command, "queued")
history.push(command)
if (history.length > 20) history.shift()
queue.push(command)
startNext()
})
},
next(rendererId?: unknown) {
if (
rendererId !== undefined &&
(typeof rendererId !== "string" || !/^[a-zA-Z0-9-]{1,64}$/.test(rendererId))
)
throw new Error("Invalid canvas renderer ID.")
if (!pending || pending.sent) return null
pending.sent = true
pending.rendererId = rendererId
stage(pending, "delivered")
return { id: pending.id, type: pending.type, input: pending.input }
},
progress(input: unknown, rendererId?: unknown) {
const { id, stage: name } = readHubObject(input) ?? {}
if (!pending || pending.id !== id || pending.rendererId !== rendererId)
throw new Error("No matching canvas action is pending for this renderer.")
if (name !== "handler started" && name !== "status collected")
throw new Error("Invalid canvas action stage.")
if (!pending.trace.some((entry) => entry.stage === name)) stage(pending, name)
},
diagnostics() {
return { requests: history.map(diagnostic) }
},
complete(input: unknown, rendererId?: unknown) {
const result = readHubObject(input)
if (!pending || result?.id !== pending.id || typeof result.ok !== "boolean") {
throw new Error("No matching canvas action is pending.")
}
if (pending.rendererId !== rendererId)
throw new Error("This canvas action belongs to another renderer.")
const { resolve, reject, timer } = pending
stage(pending, "reply received")
pending.outcome = result.ok ? "completed" : "failed"
clearTimeout(timer)
pending = null
if (result.ok) resolve(result.value)
else
reject(new Error(typeof result.error === "string" ? result.error : "Canvas action failed."))
startNext()
},
close() {
const error = new Error("The canvas was closed or changed during this action.")
if (pending) {
pending.outcome = "cancelled"
clearTimeout(pending.timer)
pending.reject(error)
pending = null
}
for (const command of queue) {
command.outcome = "cancelled"
command.reject(error)
}
queue.length = 0
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type PrototypeServerStatus = Awaited<ReturnType<typeof checkPrototypeServer>>

export async function checkPrototypeServer(prototypeUrl: string, request = globalThis.fetch) {
try {
const response = await request(prototypeUrl, {
redirect: "manual",
signal: AbortSignal.timeout(2000),
})
await response.body?.cancel()
// HTTP errors can be useful dev-server diagnostics; only a failed connection needs setup.
return { available: true }
} catch (error) {
return {
available: false,
message: `Prototype server unavailable. Start it again or choose another server. ${error instanceof Error ? error.message : String(error)}`,
}
}
}
Loading
Loading