Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
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
11891d2
Merge commit '8dcd14c4bcd319a7d5dbb87ccf5c2c7c15c51270' into lukasopp…
lukasoppermann Oct 22, 2026
8f1bac4
merge: sync collaboration with publication parent
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
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { mkdtemp, rm } from "node:fs/promises"
import path from "node:path"

import { runCommand } from "../run-command/index.mjs"
import { gitShaPattern } from "../identity-patterns/index.mjs"

export async function captureReviewRevision(context) {
const gitDirectory = await runCommand(
"git",
Expand All @@ -18,7 +16,9 @@ export async function captureReviewRevision(context) {
const result = await runCommand("git", [...config, ...args], context.repositoryPath, { env })
if (!result.ok && !(optional && result.exitCode === 1))
throw new Error(`Could not capture the local review revision: ${result.error}`)
return result
if (typeof result.stdout !== "string")
throw new Error("Git did not return output while capturing the local review revision.")
return { ...result, stdout: result.stdout }
}
try {
// Snapshotting must not execute repository-defined clean filters or filter processes.
Expand Down Expand Up @@ -52,7 +52,7 @@ export async function captureReviewRevision(context) {
])
).stdout.split("\0")
for (let index = 2; index < attributes.length; index += 3) {
if (!["unspecified", "unset"].includes(attributes[index]))
if (!["unspecified", "unset"].includes(attributes[index] ?? ""))
throw new Error("Local review revisions do not support Git clean filters.")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { mkdtemp, rm } from "node:fs/promises"
import path from "node:path"

import { runCommand } from "../run-command/index.mjs"
import { gitShaPattern } from "../identity-patterns/index.mjs"
import type { CanvasContext } from "../create-canvas-context/index.mjs"

export type ReviewRevision = Awaited<ReturnType<typeof captureReviewRevision>>

export async function captureReviewRevision(context: Pick<CanvasContext, "repositoryPath">) {
const gitDirectory = await runCommand(
"git",
["rev-parse", "--absolute-git-dir"],
context.repositoryPath,
)
if (!gitDirectory.ok) throw new Error("Could not locate the local review Git directory.")
const directory = await mkdtemp(path.join(gitDirectory.stdout.trim(), "replica-review-"))
const env = { ...process.env, GIT_INDEX_FILE: path.join(directory, "index") }
const config = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null"]
async function git(args: string[], optional = false) {
const result = await runCommand("git", [...config, ...args], context.repositoryPath, { env })
if (!result.ok && !(optional && result.exitCode === 1))
throw new Error(`Could not capture the local review revision: ${result.error}`)
if (typeof result.stdout !== "string")
throw new Error("Git did not return output while capturing the local review revision.")
return { ...result, stdout: result.stdout }
}
try {
// Snapshotting must not execute repository-defined clean filters or filter processes.
const filters = await git(
["config", "--name-only", "--get-regexp", "^filter\\..*\\.(clean|process|required)$"],
true,
)
for (const key of filters.stdout.trim().split("\n").filter(Boolean)) {
config.push("-c", `${key}=${key.endsWith(".required") ? "false" : ""}`)
}
const head = (await git(["rev-parse", "HEAD"])).stdout.trim()
const branch = (await git(["branch", "--show-current"])).stdout.trim()
const headTree = (await git(["rev-parse", "HEAD^{tree}"])).stdout.trim()
if (!gitShaPattern.test(head) || !branch)
throw new Error("Open a committed, named branch before commenting.")
await git(["read-tree", head])
const files = (
await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard"])
).stdout
.split("\0")
.filter(Boolean)
for (let offset = 0; offset < files.length; offset += 100) {
const attributes = (
await git([
"--literal-pathspecs",
"check-attr",
"-z",
"filter",
"--",
...files.slice(offset, offset + 100),
])
).stdout.split("\0")
for (let index = 2; index < attributes.length; index += 3) {
if (!["unspecified", "unset"].includes(attributes[index] ?? ""))
throw new Error("Local review revisions do not support Git clean filters.")
}
}
const submodules = await git([
"diff",
"--no-ext-diff",
"--no-textconv",
"--ignore-submodules=none",
"--name-only",
"-z",
"--diff-filter=M",
head,
"--",
])
const changedPaths = new Set(submodules.stdout.split("\0"))
const entries = (await git(["ls-files", "--stage", "-z"])).stdout
if (
entries
.split("\0")
.some(
(entry) =>
entry.startsWith("160000 ") && changedPaths.has(entry.slice(entry.indexOf("\t") + 1)),
)
)
throw new Error("Commit or discard submodule changes before commenting on this preview.")
await git(["add", "--all", "--", "."])
const treeSha = (await git(["write-tree"])).stdout.trim()
const currentHead = (await git(["rev-parse", "HEAD"])).stdout.trim()
const currentBranch = (await git(["branch", "--show-current"])).stdout.trim()
const remaining = await git(
["diff", "--no-ext-diff", "--no-textconv", "--quiet", "--ignore-submodules=none"],
true,
)
const untracked = (await git(["ls-files", "--others", "--exclude-standard"])).stdout
if (currentHead !== head || currentBranch !== branch || !remaining.ok || untracked)
throw new Error("The checkout changed while capturing this review revision. Try again.")
if (!gitShaPattern.test(treeSha))
throw new Error("Could not verify the local review source tree.")
return { treeSha, commitSha: head, clean: treeSha === headTree }
} finally {
await rm(directory, { recursive: true, force: true })
}
}
89 changes: 49 additions & 40 deletions .github/extensions/replica-prototype/comment-actions/index.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { readHubError } from "../read-hub-error/index.mjs"
import { readHubObject } from "../read-hub-object/index.mjs"
import { uuidInputPattern } from "../identity-patterns/index.mjs"
const targetSchema = {
type: "object",
additionalProperties: false,
Expand Down Expand Up @@ -151,7 +154,6 @@ const definitions = [
required: ["target", "threadId", "activityVersion"],
},
]

export function commentActions(invoke, CanvasError) {
return definitions.map(({ name, description, properties, required }) => ({
name,
Expand All @@ -165,40 +167,43 @@ export function commentActions(invoke, CanvasError) {
handler: async (ctx) => {
try {
return await invoke(ctx.instanceId, name, ctx.input ?? {})
} catch (error) {
if (error instanceof CanvasError) throw error
const code = [
"stale_comment_target",
"reply_delivery_unknown",
"comment_response_limit",
"comment_snapshot_expired",
].includes(error.code)
? error.code
: error.statusCode === 401
? "hub_disconnected"
: error.statusCode === 403
? "hub_access_denied"
: error.statusCode === 404
? "comments_not_found"
: error.statusCode === 413 && /^(list|get)_/.test(name)
? "comment_response_limit"
: error.statusCode === 429
? "rate_limited"
: error.statusCode === 409
? "comment_conflict"
: error.statusCode === 400 || error.statusCode === 422
? "invalid_comment_request"
: "comment_action_failed"
const mutationWarning =
{
reply_to_comment:
" A reply may have been posted. Preserve its request ID and inspect Hub before deciding what to do.",
mark_comment_read:
" The thread may have been marked read. Inspect Hub before deciding what to do.",
resolve_comment:
" The thread may have been resolved and marked read. Inspect Hub before deciding what to do.",
}[name] ?? ""
const failureMessage = {
} catch (caught) {
if (caught instanceof CanvasError) throw caught
const error = readHubError(caught)
const code =
typeof error.code === "string" &&
[
"stale_comment_target",
"reply_delivery_unknown",
"comment_response_limit",
"comment_snapshot_expired",
].includes(error.code)
? error.code
: error.statusCode === 401
? "hub_disconnected"
: error.statusCode === 403
? "hub_access_denied"
: error.statusCode === 404
? "comments_not_found"
: error.statusCode === 413 && /^(list|get)_/.test(name)
? "comment_response_limit"
: error.statusCode === 429
? "rate_limited"
: error.statusCode === 409
? "comment_conflict"
: error.statusCode === 400 || error.statusCode === 422
? "invalid_comment_request"
: "comment_action_failed"
const mutationWarnings = {
reply_to_comment:
" A reply may have been posted. Preserve its request ID and inspect Hub before deciding what to do.",
mark_comment_read:
" The thread may have been marked read. Inspect Hub before deciding what to do.",
resolve_comment:
" The thread may have been resolved and marked read. Inspect Hub before deciding what to do.",
}
const mutationWarning = mutationWarnings[name] ?? ""
const failureMessages = {
list_comments: "Could not retrieve comments.",
list_repository_comments: "Could not retrieve repository comments.",
list_repository_comment_summaries: "Could not retrieve repository comment summaries.",
Expand All @@ -208,8 +213,10 @@ export function commentActions(invoke, CanvasError) {
reply_to_comment: "Could not complete the reply action.",
mark_comment_read: "Could not complete the mark-read action.",
resolve_comment: "Could not complete the resolution action.",
}[name]
const message = {
}
const failureMessage = failureMessages[name]
const rateLimitMessage = readHubObject(error)?.rateLimitMessage
const messages = {
stale_comment_target: `The Hub or checkout target changed. Retrieve comments again.${mutationWarning}`,
hub_disconnected: "Connect to Hub before accessing comments.",
hub_access_denied: "Hub repository access was denied.",
Expand All @@ -225,13 +232,15 @@ export function commentActions(invoke, CanvasError) {
"The comment snapshot is unavailable for this Hub identity. Retrieve comments again.",
invalid_comment_request: "Invalid comment input, cursor, read version, or thread state.",
rate_limited:
error.rateLimitMessage ||
(typeof rateLimitMessage === "string" && rateLimitMessage) ||
"Too many comment changes. No changes were made. Wait 60 seconds, then try again.",
comment_action_failed: `${failureMessage}${mutationWarning}`,
}[code]
}
const message = messages[code]
if (message === undefined)
throw new Error("Missing comment action error message.", { cause: caught })
throw new CanvasError(code, message)
}
},
}))
}
import { uuidInputPattern } from "../identity-patterns/index.mjs"
Loading
Loading