Fix canvas repo detection and shrink oversized preview images (#2227)
Canvas extensions (accessibility-kanban, where-was-i) resolved git state
against the extension's install directory or the session-state folder
instead of the repo the session was opened in. This broke repo/branch
detection for user-scoped installs and worktrees ("unknown/unknown",
"detached HEAD"). Both now read the active session's working directory
from the canvas request context on open and on each action, so they
follow whichever project/worktree opened them.
where-was-i also re-gathers context on open when the saved branch is
blank, so it never opens stuck on stale data.
Also compress oversized extension preview images (pngquant, with a mild
resize for the photographic arcade-canvas art) so every preview.png is
comfortably under the installer's inline-byte cap that was blocking
release-notes-showcase from installing.
Update release-notes-showcase author to Kayla Cinnamon.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@@ -14,10 +14,26 @@ const VALID_COLUMNS = new Set(COLUMNS);
|
|||||||
|
|
||||||
let repoInfoCache = null;
|
let repoInfoCache = null;
|
||||||
let githubTokenCache;
|
let githubTokenCache;
|
||||||
let sessionRef = null;
|
let workspaceCwd = null;
|
||||||
|
|
||||||
|
// The canvas request context reports the active session's *working directory*
|
||||||
|
// (i.e. the repo checkout). Note this is different from `session.workspacePath`,
|
||||||
|
// which points at the session-state folder (~/.copilot/session-state/<id>) and is
|
||||||
|
// NOT a git repo — using it for git resolution is what caused the board to fail
|
||||||
|
// with "Unable to detect the current repository from this workspace."
|
||||||
|
function setWorkspaceCwd(dir) {
|
||||||
|
if (typeof dir !== "string" || !dir.trim() || dir === workspaceCwd) return;
|
||||||
|
workspaceCwd = dir;
|
||||||
|
repoInfoCache = null;
|
||||||
|
githubTokenCache = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureCwd(ctx) {
|
||||||
|
setWorkspaceCwd(ctx?.session?.workingDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
function getWorkspaceCwd() {
|
function getWorkspaceCwd() {
|
||||||
return sessionRef?.workspacePath || process.cwd();
|
return workspaceCwd || process.cwd();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Repo resolution ───
|
// ─── Repo resolution ───
|
||||||
@@ -61,12 +77,12 @@ function parseRepoFromRemoteUrl(remoteUrl) {
|
|||||||
function candidateCwds(preferredCwd) {
|
function candidateCwds(preferredCwd) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
preferredCwd,
|
preferredCwd,
|
||||||
sessionRef?.workspacePath,
|
workspaceCwd,
|
||||||
|
process.cwd(),
|
||||||
__dirname,
|
__dirname,
|
||||||
path.dirname(__dirname),
|
path.dirname(__dirname),
|
||||||
path.dirname(path.dirname(__dirname)),
|
path.dirname(path.dirname(__dirname)),
|
||||||
path.dirname(path.dirname(path.dirname(__dirname))),
|
path.dirname(path.dirname(path.dirname(__dirname))),
|
||||||
process.cwd(),
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return [...new Set(candidates)];
|
return [...new Set(candidates)];
|
||||||
}
|
}
|
||||||
@@ -564,7 +580,8 @@ const canvas = createCanvas({
|
|||||||
name: "get_state",
|
name: "get_state",
|
||||||
description: "Get the current board state including open repository issues and selected label filters.",
|
description: "Get the current board state including open repository issues and selected label filters.",
|
||||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||||
async handler() {
|
async handler(ctx) {
|
||||||
|
captureCwd(ctx);
|
||||||
return refreshIssuesSafe();
|
return refreshIssuesSafe();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -580,8 +597,9 @@ const canvas = createCanvas({
|
|||||||
required: ["issue_number", "column"],
|
required: ["issue_number", "column"],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
handler({ input }) {
|
handler(ctx) {
|
||||||
const { issue } = moveIssue(input.issue_number, input.column);
|
captureCwd(ctx);
|
||||||
|
const { issue } = moveIssue(ctx.input.issue_number, ctx.input.column);
|
||||||
return { issue, state: currentState() };
|
return { issue, state: currentState() };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -620,8 +638,9 @@ const canvas = createCanvas({
|
|||||||
required: ["issues"],
|
required: ["issues"],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
handler({ input }) {
|
handler(ctx) {
|
||||||
return replaceIssues(input.issues);
|
captureCwd(ctx);
|
||||||
|
return replaceIssues(ctx.input.issues);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -635,21 +654,24 @@ const canvas = createCanvas({
|
|||||||
required: ["labels"],
|
required: ["labels"],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
handler({ input }) {
|
handler(ctx) {
|
||||||
return setSelectedLabels(input.labels);
|
captureCwd(ctx);
|
||||||
|
return setSelectedLabels(ctx.input.labels);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "reset_state",
|
name: "reset_state",
|
||||||
description: "Reset all cards to backlog and clear label filters, then refresh from live repo issues.",
|
description: "Reset all cards to backlog and clear label filters, then refresh from live repo issues.",
|
||||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||||
async handler() {
|
async handler(ctx) {
|
||||||
|
captureCwd(ctx);
|
||||||
resetBoard();
|
resetBoard();
|
||||||
return refreshIssuesSafe();
|
return refreshIssuesSafe();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
async open() {
|
async open(ctx) {
|
||||||
|
captureCwd(ctx);
|
||||||
const state = await refreshIssuesSafe();
|
const state = await refreshIssuesSafe();
|
||||||
broadcast("state", state);
|
broadcast("state", state);
|
||||||
return {
|
return {
|
||||||
@@ -662,7 +684,7 @@ const canvas = createCanvas({
|
|||||||
|
|
||||||
// ─── Join session (tools + canvas) ───
|
// ─── Join session (tools + canvas) ───
|
||||||
|
|
||||||
const session = await joinSession({
|
await joinSession({
|
||||||
canvases: [canvas],
|
canvases: [canvas],
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
@@ -702,5 +724,3 @@ const session = await joinSession({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
sessionRef = session;
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 980 KiB After Width: | Height: | Size: 686 KiB |
|
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 334 KiB |
@@ -3,8 +3,8 @@
|
|||||||
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
"description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "James Montemagno",
|
"name": "Kayla Cinnamon",
|
||||||
"url": "https://github.com/jamesmontemagno"
|
"url": "https://github.com/cinnamon-msft"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"changelog",
|
"changelog",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 523 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 252 KiB |
@@ -14,11 +14,28 @@ const contextCache = new Map(); // instanceId → contextData
|
|||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === "win32";
|
||||||
|
|
||||||
// Derive repo root from extension location (.github/extensions/where-was-i/)
|
// Fallback repo root derived from extension location. Only used when the
|
||||||
|
// session's real working directory is unavailable (see captureCwd below).
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const REPO_ROOT = join(__dirname, "..", "..", "..");
|
const REPO_ROOT = join(__dirname, "..", "..", "..");
|
||||||
|
|
||||||
|
// The canvas request context reports the active session's working directory —
|
||||||
|
// the actual repo checkout or worktree the user opened the canvas in. This is
|
||||||
|
// what git commands must run against; REPO_ROOT (the extension's install dir)
|
||||||
|
// and session.workspacePath (the session-state folder) are NOT the repo, which
|
||||||
|
// is why the board previously showed an empty branch as "detached HEAD".
|
||||||
|
let workspaceCwd = null;
|
||||||
|
|
||||||
|
function captureCwd(ctx) {
|
||||||
|
const dir = ctx?.session?.workingDirectory;
|
||||||
|
if (typeof dir === "string" && dir.trim()) workspaceCwd = dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function repoCwd() {
|
||||||
|
return workspaceCwd || REPO_ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Shell helpers ---
|
// --- Shell helpers ---
|
||||||
|
|
||||||
function run(cmd, cwd) {
|
function run(cmd, cwd) {
|
||||||
@@ -34,7 +51,7 @@ function run(cmd, cwd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherContext(cwd) {
|
async function gatherContext(cwd) {
|
||||||
cwd = cwd || REPO_ROOT;
|
cwd = cwd || repoCwd();
|
||||||
const authorCmd = isWindows
|
const authorCmd = isWindows
|
||||||
? 'git log --oneline -5 --format="%h %s" --author="$(git config user.name)"'
|
? 'git log --oneline -5 --format="%h %s" --author="$(git config user.name)"'
|
||||||
: 'git log --oneline -5 --format="%h %s" --author="$(git config user.name)"';
|
: 'git log --oneline -5 --format="%h %s" --author="$(git config user.name)"';
|
||||||
@@ -605,7 +622,7 @@ async function startServer(instanceId, sessionRef, cwd, workspacePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === "/refresh" && req.method === "POST") {
|
if (url.pathname === "/refresh" && req.method === "POST") {
|
||||||
const data = await gatherContext(cwd);
|
const data = await gatherContext(repoCwd());
|
||||||
contextCache.set(instanceId, data);
|
contextCache.set(instanceId, data);
|
||||||
await saveContext(workspacePath, data);
|
await saveContext(workspacePath, data);
|
||||||
broadcast(instanceId, data);
|
broadcast(instanceId, data);
|
||||||
@@ -672,7 +689,8 @@ const session = await joinSession({
|
|||||||
name: "refresh",
|
name: "refresh",
|
||||||
description: "Re-gather all git/project context and push updates to the canvas",
|
description: "Re-gather all git/project context and push updates to the canvas",
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const data = await gatherContext(REPO_ROOT);
|
captureCwd(ctx);
|
||||||
|
const data = await gatherContext(repoCwd());
|
||||||
contextCache.set(ctx.instanceId, data);
|
contextCache.set(ctx.instanceId, data);
|
||||||
if (sessionRef) await saveContext(sessionRef.workspacePath, data);
|
if (sessionRef) await saveContext(sessionRef.workspacePath, data);
|
||||||
broadcast(ctx.instanceId, data);
|
broadcast(ctx.instanceId, data);
|
||||||
@@ -713,16 +731,20 @@ const session = await joinSession({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
open: async (ctx) => {
|
open: async (ctx) => {
|
||||||
|
captureCwd(ctx);
|
||||||
let entry = servers.get(ctx.instanceId);
|
let entry = servers.get(ctx.instanceId);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
entry = await startServer(ctx.instanceId, sessionRef, REPO_ROOT, sessionRef?.workspacePath);
|
entry = await startServer(ctx.instanceId, sessionRef, repoCwd(), sessionRef?.workspacePath);
|
||||||
servers.set(ctx.instanceId, entry);
|
servers.set(ctx.instanceId, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load persisted context or gather fresh
|
// Load persisted context or gather fresh. Re-gather when the
|
||||||
|
// saved context is missing or has no branch (e.g. it was saved
|
||||||
|
// before the working directory was known), so the board never
|
||||||
|
// opens stuck on a stale "detached HEAD".
|
||||||
let data = await loadContext(sessionRef?.workspacePath);
|
let data = await loadContext(sessionRef?.workspacePath);
|
||||||
if (!data) {
|
if (!data || !data.branch) {
|
||||||
data = await gatherContext(REPO_ROOT);
|
data = await gatherContext(repoCwd());
|
||||||
await saveContext(sessionRef?.workspacePath, data);
|
await saveContext(sessionRef?.workspacePath, data);
|
||||||
}
|
}
|
||||||
contextCache.set(ctx.instanceId, data);
|
contextCache.set(ctx.instanceId, data);
|
||||||
|
|||||||