From 00081d9611d033c90b6e86aaf2c0c02627b05a4e Mon Sep 17 00:00:00 2001 From: vczh Date: Mon, 2 Mar 2026 04:51:08 -0800 Subject: [PATCH] Update agent --- .../CopilotPortal/assets/flowChartMermaid.js | 45 ++++++++- .../CopilotPortal/assets/jobTracking.css | 51 ++++++++++ .../CopilotPortal/assets/jobTracking.html | 1 + .../CopilotPortal/assets/jobTracking.js | 92 ++++++++++++------ .../Agent/packages/CopilotPortal/package.json | 2 +- .../CopilotPortal/src/copilotSession.ts | 3 +- .../Agent/packages/CopilotPortal/src/index.ts | 31 ++++-- .../packages/CopilotPortal/src/jobsData.ts | 96 ++++++++++--------- .../packages/CopilotPortal/test/api.test.mjs | 60 ++++++++++++ .../prompts/snapshot/CopilotPortal/API.md | 2 + .../prompts/snapshot/CopilotPortal/Jobs.md | 15 +++ .../Agent/prompts/spec/CopilotPortal/API.md | 2 + .../Agent/prompts/spec/CopilotPortal/Jobs.md | 15 +++ .github/Agent/yarn.lock | 78 +++++++-------- .github/prompts/1-design.prompt.md | 15 +-- 15 files changed, 372 insertions(+), 136 deletions(-) diff --git a/.github/Agent/packages/CopilotPortal/assets/flowChartMermaid.js b/.github/Agent/packages/CopilotPortal/assets/flowChartMermaid.js index 295399b4..aa86d913 100644 --- a/.github/Agent/packages/CopilotPortal/assets/flowChartMermaid.js +++ b/.github/Agent/packages/CopilotPortal/assets/flowChartMermaid.js @@ -138,18 +138,53 @@ async function renderFlowChartMermaid(chart, container, onInspect) { const zoomMin = 0.2; const zoomMax = 3; const zoomStep = 0.1; - container.addEventListener("wheel", (e) => { - if (!e.ctrlKey) return; - e.preventDefault(); - const delta = e.deltaY > 0 ? -zoomStep : zoomStep; - zoomLevel = Math.min(zoomMax, Math.max(zoomMin, zoomLevel + delta)); + + function applyZoom() { const svgInner = container.querySelector("svg"); if (svgInner) { svgInner.style.transformOrigin = "top left"; svgInner.style.transform = `scale(${zoomLevel})`; } + } + + container.addEventListener("wheel", (e) => { + if (!e.ctrlKey) return; + e.preventDefault(); + const delta = e.deltaY > 0 ? -zoomStep : zoomStep; + zoomLevel = Math.min(zoomMax, Math.max(zoomMin, zoomLevel + delta)); + applyZoom(); }, { passive: false }); + // Touch pinch-to-zoom (two fingers) + let lastTouchDist = null; + container.addEventListener("touchstart", (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + lastTouchDist = Math.hypot(dx, dy); + } + }, { passive: false }); + + container.addEventListener("touchmove", (e) => { + if (e.touches.length === 2 && lastTouchDist !== null) { + e.preventDefault(); + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const newDist = Math.hypot(dx, dy); + const scale = newDist / lastTouchDist; + zoomLevel = Math.min(zoomMax, Math.max(zoomMin, zoomLevel * scale)); + lastTouchDist = newDist; + applyZoom(); + } + }, { passive: false }); + + container.addEventListener("touchend", (e) => { + if (e.touches.length < 2) { + lastTouchDist = null; + } + }); + // Track currently selected TaskNode/CondNode let currentSelectedGroup = null; let currentSelectedOriginalStrokeWidth = null; diff --git a/.github/Agent/packages/CopilotPortal/assets/jobTracking.css b/.github/Agent/packages/CopilotPortal/assets/jobTracking.css index 669f8338..372fe59a 100644 --- a/.github/Agent/packages/CopilotPortal/assets/jobTracking.css +++ b/.github/Agent/packages/CopilotPortal/assets/jobTracking.css @@ -172,3 +172,54 @@ html, body { height: 100%; overflow: auto; } + +/* ---- Phone-specific layout ---- */ + +/* Back button: hidden on desktop, visible only in phone mode */ +.phone-back-btn { + display: none; + padding: 8px 16px; + border: 1px solid #444; + border-bottom: none; + border-radius: 4px 4px 0 0; + background: #dc2626; + color: #fff; + cursor: pointer; + font-size: 14px; + margin-left: auto; + flex-shrink: 0; +} + +.phone-back-btn:hover { + background: #b91c1c; +} + +@media (max-width: 768px) { + .phone-back-btn { + display: block; + } + + #resize-bar { + display: none !important; + } + + #right-part { + display: none !important; + position: fixed; + top: 0; + left: 0; + width: 100% !important; + height: 100%; + z-index: 1000; + background: #ffffff; + } + + #right-part.phone-visible { + display: flex !important; + } + + #left-part { + width: 100% !important; + flex: none; + } +} diff --git a/.github/Agent/packages/CopilotPortal/assets/jobTracking.html b/.github/Agent/packages/CopilotPortal/assets/jobTracking.html index ab989be4..607ec1f7 100644 --- a/.github/Agent/packages/CopilotPortal/assets/jobTracking.html +++ b/.github/Agent/packages/CopilotPortal/assets/jobTracking.html @@ -2,6 +2,7 @@ + Copilot Portal - Job Tracking diff --git a/.github/Agent/packages/CopilotPortal/assets/jobTracking.js b/.github/Agent/packages/CopilotPortal/assets/jobTracking.js index 741218a1..63522c8c 100644 --- a/.github/Agent/packages/CopilotPortal/assets/jobTracking.js +++ b/.github/Agent/packages/CopilotPortal/assets/jobTracking.js @@ -21,6 +21,7 @@ let chartController = null; // returned from renderFlowChartMermaid let jobStatus = isPreviewMode ? "PREVIEW" : "RUNNING"; // PREVIEW | RUNNING | SUCCEEDED | FAILED | CANCELED let jobStopped = false; + // Map: workId -> { taskId, sessions: Map, attemptCount } const workIdData = {}; @@ -116,13 +117,6 @@ function showJsonView() { function showTaskSessionTabs(workId) { sessionResponsePart.innerHTML = ""; const data = workIdData[workId]; - if (!data) { - sessionResponsePart.textContent = "No session data for this task."; - return; - } - - // Reset active tab tracking so the first switchTabForWork always applies - data.activeTabSessionId = null; tabContainer = document.createElement("div"); tabContainer.className = "tab-container"; @@ -137,33 +131,56 @@ function showTaskSessionTabs(workId) { sessionResponsePart.appendChild(tabContainer); - // Ensure "Driving" tab always appears first - const sortedEntries = [...data.sessions.entries()].sort((a, b) => { - if (a[1].name === "Driving") return -1; - if (b[1].name === "Driving") return 1; - return 0; - }); + if (!data || data.sessions.size === 0) { + // No session data yet — show placeholder in tab content + const placeholder = document.createElement("div"); + placeholder.style.padding = "16px"; + placeholder.textContent = "No session data for this task."; + tabContent.appendChild(placeholder); + } else { + // Reset active tab tracking so the first switchTabForWork always applies + data.activeTabSessionId = null; - for (const [sessionId, sessionInfo] of sortedEntries) { - const tabBtn = document.createElement("button"); - tabBtn.className = "tab-header-btn"; - tabBtn.textContent = sessionInfo.name; - tabBtn.dataset.sessionId = sessionId; - tabBtn.addEventListener("click", () => { - switchTabForWork(workId, tabBtn.dataset.sessionId); + // Ensure "Driving" tab always appears first + const sortedEntries = [...data.sessions.entries()].sort((a, b) => { + if (a[1].name === "Driving") return -1; + if (b[1].name === "Driving") return 1; + return 0; }); - tabHeaders.appendChild(tabBtn); - // Append the session's div to tab content (hidden by default) - sessionInfo.div.style.display = "none"; - tabContent.appendChild(sessionInfo.div); + for (const [sessionId, sessionInfo] of sortedEntries) { + const tabBtn = document.createElement("button"); + tabBtn.className = "tab-header-btn"; + tabBtn.textContent = sessionInfo.name; + tabBtn.dataset.sessionId = sessionId; + tabBtn.addEventListener("click", () => { + switchTabForWork(workId, tabBtn.dataset.sessionId); + }); + tabHeaders.appendChild(tabBtn); + + // Append the session's div to tab content (hidden by default) + sessionInfo.div.style.display = "none"; + tabContent.appendChild(sessionInfo.div); + } + + // Activate the first tab + const firstEntry = sortedEntries[0]; + if (firstEntry) { + switchTabForWork(workId, firstEntry[0]); + } } - // Activate the first tab - const firstEntry = sortedEntries[0]; - if (firstEntry) { - switchTabForWork(workId, firstEntry[0]); - } + // Add "Back" button (visible only in phone mode via CSS) + const backBtn = document.createElement("button"); + backBtn.className = "phone-back-btn"; + backBtn.textContent = "Back"; + backBtn.addEventListener("click", () => { + // Unselect the inspected node + inspectedWorkId = null; + rightPart.classList.remove("phone-visible"); + showJsonView(); + }); + tabHeaders.appendChild(backBtn); } function refreshSessionResponsePart() { @@ -177,6 +194,12 @@ function refreshSessionResponsePart() { function onInspect(workId) { inspectedWorkId = workId; refreshSessionResponsePart(); + if (workId !== null) { + // In phone mode CSS makes this a full-screen overlay; on desktop it has no effect + rightPart.classList.add("phone-visible"); + } else { + rightPart.classList.remove("phone-visible"); + } } // ---- Live Polling Helpers ---- @@ -317,6 +340,9 @@ function startTaskPolling(taskId, workId) { div.style.display = "none"; tabContent.insertBefore(div, tabContent.firstChild); } + // Ensure Back button stays at the end + const existingBackBtn = tabHeaders.querySelector(".phone-back-btn"); + if (existingBackBtn) tabHeaders.appendChild(existingBackBtn); } } @@ -349,7 +375,13 @@ function startTaskPolling(taskId, workId) { tabBtn.addEventListener("click", () => { switchTabForWork(workId, sessionId); }); - tabHeaders.appendChild(tabBtn); + // Insert before phone Back button if present + const phoneBackBtn = tabHeaders.querySelector(".phone-back-btn"); + if (phoneBackBtn) { + tabHeaders.insertBefore(tabBtn, phoneBackBtn); + } else { + tabHeaders.appendChild(tabBtn); + } div.style.display = "none"; tabContent.appendChild(div); diff --git a/.github/Agent/packages/CopilotPortal/package.json b/.github/Agent/packages/CopilotPortal/package.json index 81589f40..533b8e3e 100644 --- a/.github/Agent/packages/CopilotPortal/package.json +++ b/.github/Agent/packages/CopilotPortal/package.json @@ -12,7 +12,7 @@ "testExecute": "node test/runTests.mjs" }, "dependencies": { - "@github/copilot-sdk": "^0.1.4" + "@github/copilot-sdk": "^0.1.29" }, "devDependencies": { "@playwright/test": "^1.49.0", diff --git a/.github/Agent/packages/CopilotPortal/src/copilotSession.ts b/.github/Agent/packages/CopilotPortal/src/copilotSession.ts index 795036eb..61bb9d3d 100644 --- a/.github/Agent/packages/CopilotPortal/src/copilotSession.ts +++ b/.github/Agent/packages/CopilotPortal/src/copilotSession.ts @@ -1,4 +1,4 @@ -import { CopilotClient, defineTool, type CopilotSession } from "@github/copilot-sdk"; +import { CopilotClient, defineTool, approveAll, type CopilotSession } from "@github/copilot-sdk"; export interface ICopilotSession { get rawSection(): CopilotSession; @@ -82,6 +82,7 @@ export async function startSession( streaming: true, workingDirectory, tools: jobTools, + onPermissionRequest: approveAll, hooks: { onPreToolUse: async (input) => { if (input.toolName === "glob") { diff --git a/.github/Agent/packages/CopilotPortal/src/index.ts b/.github/Agent/packages/CopilotPortal/src/index.ts index 0e8155a3..b1a14fa4 100644 --- a/.github/Agent/packages/CopilotPortal/src/index.ts +++ b/.github/Agent/packages/CopilotPortal/src/index.ts @@ -17,6 +17,7 @@ import { apiCopilotSessionQuery, apiCopilotSessionLive, hasRunningSessions, + helperGetModels, } from "./copilotApi.js"; import { apiTaskList, @@ -96,6 +97,19 @@ async function installJobsEntry(entryValue: Entry): Promise { if (hasRunningSessions()) { throw new Error("Cannot call installJobsEntry while sessions are running."); } + const models = await helperGetModels(); + const validModelIds = new Set(models.map(m => m.id)); + for (const [category, modelId] of Object.entries(entryValue.models)) { + if (!validModelIds.has(modelId)) { + throw new Error(`entry.models["${category}"] refers to model "${modelId}" which is not a valid model.`); + } + } + for (let i = 0; i < entryValue.drivingSessionRetries.length; i++) { + const modelId = entryValue.drivingSessionRetries[i].modelId; + if (!validModelIds.has(modelId)) { + throw new Error(`entry.drivingSessionRetries[${i}].modelId refers to model "${modelId}" which is not a valid model.`); + } + } installedEntry = entryValue; } @@ -308,12 +322,15 @@ const server = http.createServer((req, res) => { serveStaticFile(res, filePath); }); -// Install the jobs entry (only if not in test mode) -if (!testMode) { - installJobsEntry(entry); +// Install the jobs entry (only if not in test mode), then start server +async function startServer(): Promise { + if (!testMode) { + await installJobsEntry(entry); + } + server.listen(port, () => { + console.log(`http://localhost:${port}`); + console.log(`http://localhost:${port}/api/stop`); + }); } -server.listen(port, () => { - console.log(`http://localhost:${port}`); - console.log(`http://localhost:${port}/api/stop`); -}); +startServer(); diff --git a/.github/Agent/packages/CopilotPortal/src/jobsData.ts b/.github/Agent/packages/CopilotPortal/src/jobsData.ts index c6e0134a..8111a5a2 100644 --- a/.github/Agent/packages/CopilotPortal/src/jobsData.ts +++ b/.github/Agent/packages/CopilotPortal/src/jobsData.ts @@ -52,8 +52,8 @@ const entryInput: Entry = { planning: "gpt-5.2", coding: "gpt-5.2-codex", reviewers1: "gpt-5.3-codex", - reviewers2: "claude-opus-4.5", - reviewers3: "gemini-3-pro-preview" + reviewers2: "claude-opus-4.6", + reviewers3: "claude-sonnet-4.6" }, drivingSessionRetries: [ { modelId: "gpt-5-mini", retries: 5 }, @@ -64,121 +64,123 @@ const entryInput: Entry = { promptVariables: { reviewerBoardFiles: [ "## Your Identity", - "You are $task-model, one of the reviewers in the review board.", + "- You are $task-model, one of the reviewers in the review board.", + "- The review document you are going to create is Copilot_Review_Writing_{YOUR-MODEL}.md", + "- You are going to review a target document, as well as any existing Copilot_Review_Finished_{OTHER-MODEL}.md.", "## Reviewer Board Files", "- gpt -> Copilot_Review_*_GPT.md", "- claude opus -> Copilot_Review_*_OPUS.md", - "- gemini -> Copilot_Review_*_GEMINI.md", + "- claude sonnet -> Copilot_Review_*_SONNET.md", ], copilotSdkTips: [ - "NOTE: If you can't find the file, try different ways to make sure, including absolute path, relative path, powershell tool, view tool, slash and backslash, etc.", - "AVOID the glob tool to find any files, it does not work on Windows." + "- NOTE: If you can't find the file, try different ways to make sure, including absolute path, relative path, powershell tool, view tool, slash and backslash, etc.", + "- AVOID the glob tool to find any files, it does not work on Windows." ], defineRepoRoot: [ - "REPO-ROOT is the root directory of the repo (aka the working directory you are currently in)" + "- REPO-ROOT is the root directory of the repo (aka the working directory you are currently in)" ], noQuestion: [ - "DO NOT ask user if you can start doing something, especially after you made a plan, always perform your job automatically and proactively til the end." + "- DO NOT ask user if you can start doing something, especially after you made a plan, always perform your job automatically and proactively til the end." ], cppjob: [ "$defineRepoRoot", "$copilotSdkTips", - "YOU MUST FOLLOW REPO-ROOT/.github/copilot-instructions.md as a general guideline for all your tasks." + "- YOU MUST FOLLOW REPO-ROOT/.github/copilot-instructions.md as a general guideline for all your tasks." ], scrum: [ - "Execute the instruction in REPO-ROOT/.github/prompts/0-scrum.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/0-scrum.prompt.md immediately.", "$noQuestion" ], design: [ - "Execute the instruction in REPO-ROOT/.github/prompts/1-design.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/1-design.prompt.md immediately.", "$noQuestion" ], plan: [ - "Execute the instruction in REPO-ROOT/.github/prompts/2-planning.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/2-planning.prompt.md immediately.", "$noQuestion" ], summary: [ - "Execute the instruction in REPO-ROOT/.github/prompts/3-summarizing.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/3-summarizing.prompt.md immediately.", "$noQuestion" ], codingPrefix: [ - "**IMPORT**: It is FORBIDDEN to modify any script files in `REPO-ROOT/.github/Scripts`. If you are getting trouble, the only reason is your code has problem. Fix the code instead of any other kind of working around.", + "- **IMPORT**: It is FORBIDDEN to modify any script files in `REPO-ROOT/.github/Scripts`. If you are getting trouble, the only reason is your code has problem. Fix the code instead of any other kind of working around.", ], execute: [ "$codingPrefix", - "Execute the instruction in REPO-ROOT/.github/prompts/4-execution.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/4-execution.prompt.md immediately.", "$noQuestion" ], verify: [ "$codingPrefix", - "Execute the instruction in REPO-ROOT/.github/prompts/5-verifying.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/5-verifying.prompt.md immediately.", "$noQuestion" ], refine: [ - "Execute the instruction in REPO-ROOT/.github/prompts/refine.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/refine.prompt.md immediately.", "$noQuestion" ], review: [ - "Execute the instruction in REPO-ROOT/.github/prompts/review.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/review.prompt.md immediately.", "$noQuestion" ], ask: [ - "Execute the instruction in REPO-ROOT/.github/prompts/ask.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/ask.prompt.md immediately.", "$noQuestion" ], code: [ - "Execute the instruction in REPO-ROOT/.github/prompts/code.prompt.md immediately.", + "- Execute the instruction in REPO-ROOT/.github/prompts/code.prompt.md immediately.", "$noQuestion" ], reportDocument: [ - "YOU MUST use the job_prepare_document tool with an argument: an absolute path of the document you are about to create or update.", - "YOU MUST use the job_prepare_document tool even when you think nothing needs to be updated, it is to make sure you are clear about which document to work on." + "- YOU MUST use the job_prepare_document tool with an argument: an absolute path of the document you are about to create or update.", + "- YOU MUST use the job_prepare_document tool even when you think nothing needs to be updated, it is to make sure you are clear about which document to work on." ], reportBoolean: [ - "YOU MUST use either job_boolean_true tool or job_boolean_false tool to answer an yes/no question, with the reason in the argument." + "- YOU MUST use either job_boolean_true tool or job_boolean_false tool to answer an yes/no question, with the reason in the argument." ], simpleCondition: [ "$defineRepoRoot", "$copilotSdkTips", "$reportBoolean", - "Use job_boolean_true tool if the below condition satisfies, or use job_boolean_false tool if it does not satisfy." + "- Use job_boolean_true tool if the below condition satisfies, or use job_boolean_false tool if it does not satisfy." ], scrumDocReady: [ "$simpleCondition", - "REPO-ROOT/.github/TaskLogs/Copilot_Scrum.md should exist and its content should not be just a title." + "- REPO-ROOT/.github/TaskLogs/Copilot_Scrum.md should exist and its content should not be just a title." ], designDocReady: [ "$simpleCondition", - "REPO-ROOT/.github/TaskLogs/Copilot_Task.md should exist and its content should not be just a title." + "- REPO-ROOT/.github/TaskLogs/Copilot_Task.md should exist and its content should not be just a title." ], planDocReady: [ "$simpleCondition", - "REPO-ROOT/.github/TaskLogs/Copilot_Planning.md should exist and its content should not be just a title." + "- REPO-ROOT/.github/TaskLogs/Copilot_Planning.md should exist and its content should not be just a title." ], execDocReady: [ "$simpleCondition", - "REPO-ROOT/.github/TaskLogs/Copilot_Execution.md should exist and its content should not be just a title." + "- REPO-ROOT/.github/TaskLogs/Copilot_Execution.md should exist and its content should not be just a title." ], execDocVerified: [ "$simpleCondition", - "REPO-ROOT/.github/TaskLogs/Copilot_Execution.md should exist and it has a `# !!!VERIFIED!!!`." + "- REPO-ROOT/.github/TaskLogs/Copilot_Execution.md should exist and it has a `# !!!VERIFIED!!!`." ], reviewDocReady: [ "$simpleCondition", - "REPO-ROOT/.github/TaskLogs/Copilot_Review.md should exist and its content should not be just a title." + "- REPO-ROOT/.github/TaskLogs/Copilot_Review.md should exist and its content should not be just a title." ], reportedDocReady: [ "$simpleCondition", - "$reported-document should exist and its content should not be just a title." + "- $reported-document should exist and its content should not be just a title." ], clearBuildTestLog: [ - "In REPO-ROOT/.github/Scripts, delete both Build.log and Execute.log." + "- In REPO-ROOT/.github/Scripts, delete both Build.log and Execute.log." ], buildSucceededFragment: [ - "REPO-ROOT/.github/Scripts/Build.log must exist and the last several lines shows there is no error" + "- REPO-ROOT/.github/Scripts/Build.log must exist and the last several lines shows there is no error" ], testPassedFragment: [ - "REPO-ROOT/.github/Scripts/Execute.log must exist and the last several lines shows how many test files and test cases passed" + "- REPO-ROOT/.github/Scripts/Execute.log must exist and the last several lines shows how many test files and test cases passed" ] }, tasks: { @@ -334,7 +336,7 @@ const entryInput: Entry = { }, criteria: { runConditionInSameSession: false, - condition: ["$simpleCondition", "All REPO-ROOT/.github/TaskLogs/Copilot_(Task|Planning|Execution).md must have been deleted."], + condition: ["$simpleCondition", "- All REPO-ROOT/.github/TaskLogs/Copilot_(Task|Planning|Execution).md must have been deleted."], failureAction: retryFailedCondition() } }, @@ -404,7 +406,7 @@ const entryInput: Entry = { prompt: ["$cppjob", "$review", "# Apply", "$reviewerBoardFiles"], criteria: { runConditionInSameSession: false, - condition: ["$simpleCondition", "Every REPO-ROOT/.github/TaskLogs/Copilot_Review*.md must have been deleted."], + condition: ["$simpleCondition", "- Every REPO-ROOT/.github/TaskLogs/Copilot_Review*.md must have been deleted."], failureAction: retryFailedCondition() } }, @@ -419,7 +421,7 @@ const entryInput: Entry = { prompt: ["$cppjob", "$code", "$user-input"], criteria: { runConditionInSameSession: true, - condition: ["$simpleCondition", "Both conditions satisfy: 1) $buildSucceededFragment; 2) $testPassedFragment."], + condition: ["$simpleCondition", "- Both conditions satisfy: 1) $buildSucceededFragment; 2) $testPassedFragment."], failureAction: retryFailedCondition() } }, @@ -428,27 +430,27 @@ const entryInput: Entry = { requireUserInput: false, prompt: [ "$defineRepoRoot", - "Call REPO-ROOT/.github/Scripts/copilotGitCommit.ps1", - "DO NOT git push." + "- Call REPO-ROOT/.github/Scripts/copilotGitCommit.ps1", + "- DO NOT git push." ] }, "git-push": { model: { category: "driving" }, requireUserInput: false, prompt: [ - "`git add` to add all files.", - "`git status` to list affected files.", - "`git commit -am` everything with this message: [BOT] Backup.", - "`git branch` to see the current branch.", - "`git push` to the current branch.", - "DO NOT run multiple commands at once." + "- `git add` to add all files.", + "- `git status` to list affected files.", + "- `git commit -am` everything with this message: [BOT] Backup.", + "- `git branch` to see the current branch.", + "- `git push` to the current branch.", + "- DO NOT run multiple commands at once." ], criteria: { runConditionInSameSession: true, condition: [ "$simpleCondition", - "`git status` to list file affected, make sure there is nothing uncommited.", - "But it is fine if all uncommited changes are only whitespace related." + "- `git status` to list file affected, make sure there is nothing uncommited.", + "- But it is fine if all uncommited changes are only whitespace related." ], failureAction: retryFailedCondition() } diff --git a/.github/Agent/packages/CopilotPortal/test/api.test.mjs b/.github/Agent/packages/CopilotPortal/test/api.test.mjs index b58f051e..d5bd98f7 100644 --- a/.github/Agent/packages/CopilotPortal/test/api.test.mjs +++ b/.github/Agent/packages/CopilotPortal/test/api.test.mjs @@ -306,6 +306,66 @@ describe("API: copilot/test/installJobsEntry", () => { assert.strictEqual(data.result, "OK", `installJobsEntry should succeed: ${JSON.stringify(data)}`); }); + it("rejects entry with invalid model IDs", async () => { + const invalidModelEntryPath = path.join(__dirname, "invalidModelEntry.json"); + const fs = await import("node:fs"); + // A structurally valid entry but with a model ID that doesn't exist + fs.writeFileSync(invalidModelEntryPath, JSON.stringify({ + models: { driving: "nonexistent-model-xyz" }, + drivingSessionRetries: [{ modelId: "nonexistent-model-xyz", retries: 1 }], + promptVariables: {}, + grid: [], + tasks: { + "dummy-task": { model: { category: "driving" }, prompt: ["hello"], requireUserInput: false } + }, + jobs: {} + })); + try { + const data = await fetchJson("/api/copilot/test/installJobsEntry", { + method: "POST", + body: invalidModelEntryPath, + }); + assert.strictEqual(data.result, "Rejected", `should reject invalid model: ${JSON.stringify(data)}`); + assert.ok(data.error, "should have error message about invalid model"); + assert.ok(data.error.includes("nonexistent-model-xyz"), "error should mention the invalid model name"); + } finally { + fs.unlinkSync(invalidModelEntryPath); + } + }); + + it("rejects entry with invalid drivingSessionRetries model IDs", async () => { + const invalidRetryEntryPath = path.join(__dirname, "invalidRetryEntry.json"); + const fs = await import("node:fs"); + const modelsData = await fetchJson("/api/copilot/models"); + const freeModel = modelsData.models.find((m) => m.multiplier === 0); + // Use a valid model for entry.models but an invalid model in drivingSessionRetries + fs.writeFileSync(invalidRetryEntryPath, JSON.stringify({ + models: { driving: freeModel.id }, + drivingSessionRetries: [ + { modelId: freeModel.id, retries: 1 }, + { modelId: "nonexistent-retry-model-xyz", retries: 2 } + ], + promptVariables: {}, + grid: [], + tasks: { + "dummy-task": { model: { category: "driving" }, prompt: ["hello"], requireUserInput: false } + }, + jobs: {} + })); + try { + const data = await fetchJson("/api/copilot/test/installJobsEntry", { + method: "POST", + body: invalidRetryEntryPath, + }); + assert.strictEqual(data.result, "Rejected", `should reject invalid drivingSessionRetries model: ${JSON.stringify(data)}`); + assert.ok(data.error, "should have error message about invalid model"); + assert.ok(data.error.includes("nonexistent-retry-model-xyz"), "error should mention the invalid model name"); + assert.ok(data.error.includes("drivingSessionRetries"), "error should mention drivingSessionRetries"); + } finally { + fs.unlinkSync(invalidRetryEntryPath); + } + }); + it("rejects when session is running", async () => { const modelsData = await fetchJson("/api/copilot/models"); const freeModel = modelsData.models.find((m) => m.multiplier === 0); diff --git a/.github/Agent/prompts/snapshot/CopilotPortal/API.md b/.github/Agent/prompts/snapshot/CopilotPortal/API.md index 022b7cb3..b8ee54f4 100644 --- a/.github/Agent/prompts/snapshot/CopilotPortal/API.md +++ b/.github/Agent/prompts/snapshot/CopilotPortal/API.md @@ -45,6 +45,8 @@ Prints the following URL for shortcut: ### installJobsEntry `async installJobsEntry(entry: Entry): Promise;` +- Verify if all `entry.model[name]` is a valid model with `helperGetModels`. +- Verify if all `entry.drivingSessionRetries[index].modelId` is a valid model with `helperGetModels`. - Use the entry. It could be `entry` from `jobsData.ts` or whatever. - This function can only be called when no session is running, otherwise throws. diff --git a/.github/Agent/prompts/snapshot/CopilotPortal/Jobs.md b/.github/Agent/prompts/snapshot/CopilotPortal/Jobs.md index e375adda..a5c7fe91 100644 --- a/.github/Agent/prompts/snapshot/CopilotPortal/Jobs.md +++ b/.github/Agent/prompts/snapshot/CopilotPortal/Jobs.md @@ -234,6 +234,8 @@ When the user holds **Ctrl** and scrolls the mouse wheel over the `#chart-contai - CSS `transform: scale(...)` with `transform-origin: top left` is applied to the SVG element. - The default browser scroll/zoom behavior is suppressed (`preventDefault`). +User should be able to zoom in and zoom out also using two touch points (two fingers). + #### Interaction with `ChartNode` which has a `TaskNode` or `CondNode` hint Clicking it select (exclusive) or unselect the text: @@ -252,3 +254,16 @@ When a task is being inspected: - Each task session has its own tab. - Clicking a tab shows responses from a session using `Session Response Rendering` from `Shared.md`. - When the selected `ChartNode` is restarted, tabs should be cleared before adding new sessions. + +### Phone Specific Layout/Behavior + +**IMPORTANT**: +- Phone mode is defined by `max=width: 768px`. The session only applies when the webpage is in phone mode. +- On a PC browser while resizing, it should be able to switch between phone mode and PC mode in any status. +- Build the webpage fully reactive, which means it is not allowed to save the "mode" in anyway even temporarily. + +- The `Session Response Part` is not visible at the beginning. +- There is also no draggable bar between it and the `Job Part`. +- When a `ChartNode` is clicked, the `Session Response Part` becomes visible and occupy the whole window. +- At the very right of the tab header of the `Session Response Part`, there should be a "Back" button. + - Clicking the button hide the `Session Response Part` and the `Job Part` becomes available again. diff --git a/.github/Agent/prompts/spec/CopilotPortal/API.md b/.github/Agent/prompts/spec/CopilotPortal/API.md index 022b7cb3..b8ee54f4 100644 --- a/.github/Agent/prompts/spec/CopilotPortal/API.md +++ b/.github/Agent/prompts/spec/CopilotPortal/API.md @@ -45,6 +45,8 @@ Prints the following URL for shortcut: ### installJobsEntry `async installJobsEntry(entry: Entry): Promise;` +- Verify if all `entry.model[name]` is a valid model with `helperGetModels`. +- Verify if all `entry.drivingSessionRetries[index].modelId` is a valid model with `helperGetModels`. - Use the entry. It could be `entry` from `jobsData.ts` or whatever. - This function can only be called when no session is running, otherwise throws. diff --git a/.github/Agent/prompts/spec/CopilotPortal/Jobs.md b/.github/Agent/prompts/spec/CopilotPortal/Jobs.md index e375adda..a5c7fe91 100644 --- a/.github/Agent/prompts/spec/CopilotPortal/Jobs.md +++ b/.github/Agent/prompts/spec/CopilotPortal/Jobs.md @@ -234,6 +234,8 @@ When the user holds **Ctrl** and scrolls the mouse wheel over the `#chart-contai - CSS `transform: scale(...)` with `transform-origin: top left` is applied to the SVG element. - The default browser scroll/zoom behavior is suppressed (`preventDefault`). +User should be able to zoom in and zoom out also using two touch points (two fingers). + #### Interaction with `ChartNode` which has a `TaskNode` or `CondNode` hint Clicking it select (exclusive) or unselect the text: @@ -252,3 +254,16 @@ When a task is being inspected: - Each task session has its own tab. - Clicking a tab shows responses from a session using `Session Response Rendering` from `Shared.md`. - When the selected `ChartNode` is restarted, tabs should be cleared before adding new sessions. + +### Phone Specific Layout/Behavior + +**IMPORTANT**: +- Phone mode is defined by `max=width: 768px`. The session only applies when the webpage is in phone mode. +- On a PC browser while resizing, it should be able to switch between phone mode and PC mode in any status. +- Build the webpage fully reactive, which means it is not allowed to save the "mode" in anyway even temporarily. + +- The `Session Response Part` is not visible at the beginning. +- There is also no draggable bar between it and the `Job Part`. +- When a `ChartNode` is clicked, the `Session Response Part` becomes visible and occupy the whole window. +- At the very right of the tab header of the `Session Response Part`, there should be a "Back" button. + - Clicking the button hide the `Session Response Part` and the `Job Part` becomes available again. diff --git a/.github/Agent/yarn.lock b/.github/Agent/yarn.lock index 41dc5a3b..b7e0cd78 100644 --- a/.github/Agent/yarn.lock +++ b/.github/Agent/yarn.lock @@ -2,56 +2,56 @@ # yarn lockfile v1 -"@github/copilot-darwin-arm64@0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.403.tgz#e1cbc91f73639c8217fd2cc61498bc311e2af45c" - integrity sha512-dOw8IleA0d1soHnbr/6wc6vZiYWNTKMgfTe/NET1nCfMzyKDt/0F0I7PT5y+DLujJknTla/ZeEmmBUmliTW4Cg== +"@github/copilot-darwin-arm64@0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.420.tgz#560ca002fa491c04fdb6f74f84fee87e52575c53" + integrity sha512-sj8Oxcf3oKDbeUotm2gtq5YU1lwCt3QIzbMZioFD/PMLOeqSX/wrecI+c0DDYXKofFhALb0+DxxnWgbEs0mnkQ== -"@github/copilot-darwin-x64@0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.403.tgz#b246b0b91f31e13650d99c59716fd74ab87465f5" - integrity sha512-aK2jSNWgY8eiZ+TmrvGhssMCPDTKArc0ip6Ul5OaslpytKks8hyXoRbxGD0N9sKioSUSbvKUf+1AqavbDpJO+w== +"@github/copilot-darwin-x64@0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.420.tgz#1d5cf40ac4e04bbd69fb0a79abf3743897c5f795" + integrity sha512-2acA93IqXz1uuz3TVUm0Y7BVrBr0MySh1kQa8LqMILhTsG0YHRMm8ybzTp2HA7Mi1tl5CjqMSk163kkS7OzfUA== -"@github/copilot-linux-arm64@0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.403.tgz#5453ec3bd565cc92676b450b2edb66e49f60909d" - integrity sha512-KhoR2iR70O6vCkzf0h8/K+p82qAgOvMTgAPm9bVEHvbdGFR7Py9qL5v03bMbPxsA45oNaZAkzDhfTAqWhIAZsQ== +"@github/copilot-linux-arm64@0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.420.tgz#e247517854927a14f5c076bfa99309160afec2d7" + integrity sha512-h/IvEryTOYm1HzR2GNq8s2aDtN4lvT4MxldfZuS42CtWJDOfVG2jLLsoHWU1T3QV8j1++PmDgE//HX0JLpLMww== -"@github/copilot-linux-x64@0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.403.tgz#17a7eba380be8553610ee6632d6a81ba229722eb" - integrity sha512-eoswUc9vo4TB+/9PgFJLVtzI4dPjkpJXdCsAioVuoqPdNxHxlIHFe9HaVcqMRZxUNY1YHEBZozy+IpUEGjgdfQ== +"@github/copilot-linux-x64@0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.420.tgz#00d22974499f0fab6354fe4e22f6be59b800ab98" + integrity sha512-iL2NpZvXIDZ+3lw7sO2fo5T0nKmP5dZbU2gdYcv+SFBm/ONhCxIY5VRX4yN/9VkFaa9ePv5JzCnsl3vZINiDxg== -"@github/copilot-sdk@^0.1.4": - version "0.1.23" - resolved "https://registry.yarnpkg.com/@github/copilot-sdk/-/copilot-sdk-0.1.23.tgz#120986bf5719880dedf076c0f2a55f855566ff40" - integrity sha512-0by81bsBQlDKE5VbcegZfUMvPyPm1aXwSGS2rGaMAFxv3ps+dACf1Voruxik7hQTae0ziVFJjuVrlxZoRaXBLw== +"@github/copilot-sdk@^0.1.29": + version "0.1.29" + resolved "https://registry.yarnpkg.com/@github/copilot-sdk/-/copilot-sdk-0.1.29.tgz#8809df61ab53f100f8390234d9946cdc2acae24b" + integrity sha512-GdcN6bJTeesr1HP6IrhN2MznIf1B3ufqd3PX+uKbDLXNriOmP65Ai29/hxzTidNLHyOf6rW4NwmFfkMXiKfCBw== dependencies: - "@github/copilot" "^0.0.403" + "@github/copilot" "^0.0.420" vscode-jsonrpc "^8.2.1" zod "^4.3.6" -"@github/copilot-win32-arm64@0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.403.tgz#3cdaee25b2454ceb6d8293f06b258c95307b94ae" - integrity sha512-djWjzCsp2xPNafMyOZ/ivU328/WvWhdroGie/DugiJBTgQL2SP0quWW1fhTlDwE81a3g9CxfJonaRgOpFTJTcg== +"@github/copilot-win32-arm64@0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.420.tgz#733c45aced1e42c2877ae44012074abbcce3d55d" + integrity sha512-Njlc2j9vYSBAL+lC6FIEhQ3C+VxO3xavwKnw0ecVRiNLcGLyPrTdzPfPQOmEjC63gpVCqLabikoDGv8fuLPA2w== -"@github/copilot-win32-x64@0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.403.tgz#d22bdd50d9b0674d73981a413df881fd8229f5ce" - integrity sha512-lju8cHy2E6Ux7R7tWyLZeksYC2MVZu9i9ocjiBX/qfG2/pNJs7S5OlkwKJ0BSXSbZEHQYq7iHfEWp201bVfk9A== +"@github/copilot-win32-x64@0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.420.tgz#d45f47f2f08d4bba87760b8afb21af19d1988780" + integrity sha512-rZlH35oNehAP2DvQbu4vQFVNeCh/1p3rUjafBYaEY0Nkhx7RmdrYBileL5U3PtRPPRsBPaq3Qp+pVIrGoCDLzQ== -"@github/copilot@^0.0.403": - version "0.0.403" - resolved "https://registry.yarnpkg.com/@github/copilot/-/copilot-0.0.403.tgz#56e44b5a0640685f0b34507bbb12f47229798940" - integrity sha512-v5jUdtGJReLmE1rmff/LZf+50nzmYQYAaSRNtVNr9g0j0GkCd/noQExe31i1+PudvWU0ZJjltR0B8pUfDRdA9Q== +"@github/copilot@^0.0.420": + version "0.0.420" + resolved "https://registry.yarnpkg.com/@github/copilot/-/copilot-0.0.420.tgz#596349de076566a310836a7e06e6807b87ea6bfe" + integrity sha512-UpPuSjxUxQ+j02WjZEFffWf0scLb23LvuGHzMFtaSsweR+P/BdbtDUI5ZDIA6T0tVyyt6+X1/vgfsJiRqd6jig== optionalDependencies: - "@github/copilot-darwin-arm64" "0.0.403" - "@github/copilot-darwin-x64" "0.0.403" - "@github/copilot-linux-arm64" "0.0.403" - "@github/copilot-linux-x64" "0.0.403" - "@github/copilot-win32-arm64" "0.0.403" - "@github/copilot-win32-x64" "0.0.403" + "@github/copilot-darwin-arm64" "0.0.420" + "@github/copilot-darwin-x64" "0.0.420" + "@github/copilot-linux-arm64" "0.0.420" + "@github/copilot-linux-x64" "0.0.420" + "@github/copilot-win32-arm64" "0.0.420" + "@github/copilot-win32-x64" "0.0.420" "@playwright/test@^1.49.0": version "1.58.2" diff --git a/.github/prompts/1-design.prompt.md b/.github/prompts/1-design.prompt.md index 20e3d200..6a4cf1b6 100644 --- a/.github/prompts/1-design.prompt.md +++ b/.github/prompts/1-design.prompt.md @@ -10,7 +10,8 @@ ## Goal and Constraints - Your goal is to finish a design document in `Copilot_Task.md` to address a problem. -- You are only allowed to update `Copilot_Task.md` and mark a task being taken in `Copilot_Scrum.md`. +- You are only allowed to update `Copilot_Task.md` + - When the task comes from `Copilot_Scrum.md`, you should mark a task being taken in `Copilot_Scrum.md`. - You are not allowed to modify any other files. - The phrasing of the request may look like asking for code change, but your actual work is to write the design document. @@ -37,12 +38,14 @@ Ignore this section if there is no "# Problem" in the LATEST chat message I am starting a fresh new request. - Find and execute `copilotPrepare.ps1` to clean up everything from the last run. + - This script will clean up everything in `Copilot_Task.md`, `Copilot_Planning.md` and `Copilot_Execution.md`. + - It is normal to find large amount of changes in these 3 files, DO NOT panic. - After `copilotPrepare.ps1` finishes, copy precisely my problem description in `# Problem` from the LATEST chat message under a `# PROBLEM DESCRIPTION`. - - If the problem description is `Next`: - - Find the first incomplete task in `Copilot_Scrum.md`. - - If the problem description is like `Complete task No.X`: - - Locate the specific task in `Copilot_Scrum.md`. - - There is a bullet list of all tasks at the beginning of `# TASKS`. Mark the specific task as being taken by changing `[ ]` to `[x]`. + - Find task from `Copilot_Scrum.md` if the problem description is in the following format: + - `Next`: Find the first incomplete task in `Copilot_Scrum.md`. + - `Complete task No.X`: Locate the specific task in `Copilot_Scrum.md`. + - There is a bullet list of all tasks at the beginning of `# TASKS`. Mark the specific task as being taken by changing `[ ]` to `[x]`. + - Otherwise, the task is the problem description itself. - Find the details of the specific task, copy everything in this task to `# PROBLEM DESCRIPTION`. - Add an empty `# UPDATES` section after `# PROBLEM DESCRIPTION`.