repoburg 1.3.253 → 1.3.255
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-pane/backend/dist/index.js +40 -4
- package/agent-pane/backend/dist/server.js +40 -4
- package/agent-pane/examples/pr-review/app.ts +184 -0
- package/agent-pane/examples/pr-review/index.html +43 -0
- package/agent-pane/examples/pr-review/manifest.json +30 -0
- package/agent-pane/examples/pr-review/review.ts +47 -0
- package/agent-pane/examples/pr-review/types.ts +37 -0
- package/agent-pane/frontend/dist/assets/{index-hYkepxdY.js → index-BzDC6DcX.js} +86 -86
- package/agent-pane/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -43557,12 +43557,25 @@ function examplesRoot() {
|
|
|
43557
43557
|
}
|
|
43558
43558
|
return null;
|
|
43559
43559
|
}
|
|
43560
|
+
function isInAppFolder(cwd) {
|
|
43561
|
+
if (!cwd) return false;
|
|
43562
|
+
const rel = path3.relative(path3.resolve(appsRoot()), path3.resolve(cwd));
|
|
43563
|
+
return rel !== "" && !path3.isAbsolute(rel) && rel.split(path3.sep)[0] !== "..";
|
|
43564
|
+
}
|
|
43565
|
+
function appsConventions() {
|
|
43566
|
+
return APPS_MD_SKELETON;
|
|
43567
|
+
}
|
|
43560
43568
|
function ensureAppsRoot() {
|
|
43561
43569
|
const root = appsRoot();
|
|
43562
43570
|
fs.mkdirSync(root, { recursive: true });
|
|
43563
43571
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
43564
43572
|
const file = path3.join(root, name);
|
|
43565
|
-
|
|
43573
|
+
try {
|
|
43574
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf-8") === APPS_MD_FILE) continue;
|
|
43575
|
+
fs.writeFileSync(file, APPS_MD_FILE);
|
|
43576
|
+
} catch (err) {
|
|
43577
|
+
console.warn(`[apps] could not update ${file}: ${err.message}`);
|
|
43578
|
+
}
|
|
43566
43579
|
}
|
|
43567
43580
|
}
|
|
43568
43581
|
var APPS_MD_SKELETON = `# Mini Apps
|
|
@@ -43624,6 +43637,11 @@ open in AgentPane for a tool call to succeed in that case.
|
|
|
43624
43637
|
To give the app a structured result, put this requirement in the job prompt:
|
|
43625
43638
|
"End your answer with a single JSON code block matching this schema: ..."
|
|
43626
43639
|
`;
|
|
43640
|
+
var APPS_MD_FILE = `<!-- Generated by AgentPane: overwritten whenever it differs from the built-in
|
|
43641
|
+
conventions, so edits here are lost. Put app-specific notes in the app
|
|
43642
|
+
folder's own CLAUDE.md / AGENTS.md. -->
|
|
43643
|
+
|
|
43644
|
+
${APPS_MD_SKELETON}`;
|
|
43627
43645
|
function readManifestIn(dir) {
|
|
43628
43646
|
try {
|
|
43629
43647
|
const raw = fs.readFileSync(path3.join(dir, "manifest.json"), "utf-8");
|
|
@@ -44234,6 +44252,7 @@ function render(): void {
|
|
|
44234
44252
|
async function registerAppRoutes(fastify, opts) {
|
|
44235
44253
|
setAppsRootOverride(opts?.appsRoot ?? null);
|
|
44236
44254
|
setExamplesRootOverride(opts?.examplesRoot ?? null);
|
|
44255
|
+
ensureAppsRoot();
|
|
44237
44256
|
startAppsWatch((appId) => {
|
|
44238
44257
|
invalidateHeadlessRuntime(appId);
|
|
44239
44258
|
broadcast("app:source:updated", { appId });
|
|
@@ -44763,6 +44782,9 @@ function appServerClientOptions(_session, cwd) {
|
|
|
44763
44782
|
const launchArgs = codexMiniAppLaunchArgs(cwd);
|
|
44764
44783
|
return launchArgs ? { cwd, launchArgs } : { cwd, launchArgs: [] };
|
|
44765
44784
|
}
|
|
44785
|
+
function threadInstructionParams(cwd) {
|
|
44786
|
+
return isInAppFolder(cwd) ? { developerInstructions: appsConventions() } : {};
|
|
44787
|
+
}
|
|
44766
44788
|
function autoApproveToolUserInput(client) {
|
|
44767
44789
|
client.onRequest("item/tool/requestUserInput", async (params) => {
|
|
44768
44790
|
const questions = params.questions ?? [];
|
|
@@ -44787,6 +44809,7 @@ async function getOrCreateThread(session) {
|
|
|
44787
44809
|
}
|
|
44788
44810
|
const config = parseRuntimeConfig(session);
|
|
44789
44811
|
const sandbox = sandboxType(config);
|
|
44812
|
+
const instructions = threadInstructionParams(cwd);
|
|
44790
44813
|
let threadId;
|
|
44791
44814
|
if (session.external_thread_id) {
|
|
44792
44815
|
const result = await client.request("thread/resume", {
|
|
@@ -44794,7 +44817,8 @@ async function getOrCreateThread(session) {
|
|
|
44794
44817
|
cwd,
|
|
44795
44818
|
approvalPolicy: "never",
|
|
44796
44819
|
sandbox,
|
|
44797
|
-
model: session.model ?? void 0
|
|
44820
|
+
model: session.model ?? void 0,
|
|
44821
|
+
...instructions
|
|
44798
44822
|
});
|
|
44799
44823
|
threadId = result.thread.id;
|
|
44800
44824
|
} else {
|
|
@@ -44802,7 +44826,8 @@ async function getOrCreateThread(session) {
|
|
|
44802
44826
|
cwd,
|
|
44803
44827
|
approvalPolicy: "never",
|
|
44804
44828
|
sandbox,
|
|
44805
|
-
model: session.model ?? void 0
|
|
44829
|
+
model: session.model ?? void 0,
|
|
44830
|
+
...instructions
|
|
44806
44831
|
});
|
|
44807
44832
|
threadId = result.thread.id;
|
|
44808
44833
|
const db2 = getDb();
|
|
@@ -47716,6 +47741,16 @@ function turnImageDataUrls(turn) {
|
|
|
47716
47741
|
if (!attachments) return [];
|
|
47717
47742
|
return attachments.filter((a) => a?.media_type?.startsWith("image/") && a.data).map((a) => `data:${a.media_type};base64,${a.data}`);
|
|
47718
47743
|
}
|
|
47744
|
+
function runtimePrompt(session, prompt, firstInput) {
|
|
47745
|
+
if (!firstInput || !isInAppFolder(session.cwd)) return prompt;
|
|
47746
|
+
const conventions = `<mini_app_conventions>
|
|
47747
|
+
${appsConventions()}</mini_app_conventions>`;
|
|
47748
|
+
return prompt.startsWith("/") ? `${prompt}
|
|
47749
|
+
|
|
47750
|
+
${conventions}` : `${conventions}
|
|
47751
|
+
|
|
47752
|
+
${prompt}`;
|
|
47753
|
+
}
|
|
47719
47754
|
async function startTurn3(session, turn) {
|
|
47720
47755
|
const db2 = getDb();
|
|
47721
47756
|
let client;
|
|
@@ -47736,6 +47771,7 @@ async function startTurn3(session, turn) {
|
|
|
47736
47771
|
);
|
|
47737
47772
|
}
|
|
47738
47773
|
void registerMiniAppMcp(baseUrl, session.cwd ?? "");
|
|
47774
|
+
const firstInput = !turn.thread_id && !sessionMap3.has(session.id) && !session.external_thread_id;
|
|
47739
47775
|
let entry;
|
|
47740
47776
|
try {
|
|
47741
47777
|
entry = turn.thread_id ? await ensureThreadSession(session, turn, client, baseUrl) : await ensureSession(session, client, baseUrl);
|
|
@@ -47781,7 +47817,7 @@ async function startTurn3(session, turn) {
|
|
|
47781
47817
|
try {
|
|
47782
47818
|
const images = turnImageDataUrls(turn);
|
|
47783
47819
|
await entry.client.createInput(entry.repoburgSessionId, {
|
|
47784
|
-
user_prompt: turn.user_prompt,
|
|
47820
|
+
user_prompt: runtimePrompt(session, turn.user_prompt, firstInput),
|
|
47785
47821
|
execution_strategy: "auto_apply",
|
|
47786
47822
|
...images.length > 0 ? { images } : {}
|
|
47787
47823
|
});
|
|
@@ -43544,12 +43544,25 @@ function examplesRoot() {
|
|
|
43544
43544
|
}
|
|
43545
43545
|
return null;
|
|
43546
43546
|
}
|
|
43547
|
+
function isInAppFolder(cwd) {
|
|
43548
|
+
if (!cwd) return false;
|
|
43549
|
+
const rel = path3.relative(path3.resolve(appsRoot()), path3.resolve(cwd));
|
|
43550
|
+
return rel !== "" && !path3.isAbsolute(rel) && rel.split(path3.sep)[0] !== "..";
|
|
43551
|
+
}
|
|
43552
|
+
function appsConventions() {
|
|
43553
|
+
return APPS_MD_SKELETON;
|
|
43554
|
+
}
|
|
43547
43555
|
function ensureAppsRoot() {
|
|
43548
43556
|
const root = appsRoot();
|
|
43549
43557
|
fs.mkdirSync(root, { recursive: true });
|
|
43550
43558
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
43551
43559
|
const file = path3.join(root, name);
|
|
43552
|
-
|
|
43560
|
+
try {
|
|
43561
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf-8") === APPS_MD_FILE) continue;
|
|
43562
|
+
fs.writeFileSync(file, APPS_MD_FILE);
|
|
43563
|
+
} catch (err) {
|
|
43564
|
+
console.warn(`[apps] could not update ${file}: ${err.message}`);
|
|
43565
|
+
}
|
|
43553
43566
|
}
|
|
43554
43567
|
}
|
|
43555
43568
|
var APPS_MD_SKELETON = `# Mini Apps
|
|
@@ -43611,6 +43624,11 @@ open in AgentPane for a tool call to succeed in that case.
|
|
|
43611
43624
|
To give the app a structured result, put this requirement in the job prompt:
|
|
43612
43625
|
"End your answer with a single JSON code block matching this schema: ..."
|
|
43613
43626
|
`;
|
|
43627
|
+
var APPS_MD_FILE = `<!-- Generated by AgentPane: overwritten whenever it differs from the built-in
|
|
43628
|
+
conventions, so edits here are lost. Put app-specific notes in the app
|
|
43629
|
+
folder's own CLAUDE.md / AGENTS.md. -->
|
|
43630
|
+
|
|
43631
|
+
${APPS_MD_SKELETON}`;
|
|
43614
43632
|
function readManifestIn(dir) {
|
|
43615
43633
|
try {
|
|
43616
43634
|
const raw = fs.readFileSync(path3.join(dir, "manifest.json"), "utf-8");
|
|
@@ -44221,6 +44239,7 @@ function render(): void {
|
|
|
44221
44239
|
async function registerAppRoutes(fastify, opts) {
|
|
44222
44240
|
setAppsRootOverride(opts?.appsRoot ?? null);
|
|
44223
44241
|
setExamplesRootOverride(opts?.examplesRoot ?? null);
|
|
44242
|
+
ensureAppsRoot();
|
|
44224
44243
|
startAppsWatch((appId) => {
|
|
44225
44244
|
invalidateHeadlessRuntime(appId);
|
|
44226
44245
|
broadcast("app:source:updated", { appId });
|
|
@@ -44750,6 +44769,9 @@ function appServerClientOptions(_session, cwd) {
|
|
|
44750
44769
|
const launchArgs = codexMiniAppLaunchArgs(cwd);
|
|
44751
44770
|
return launchArgs ? { cwd, launchArgs } : { cwd, launchArgs: [] };
|
|
44752
44771
|
}
|
|
44772
|
+
function threadInstructionParams(cwd) {
|
|
44773
|
+
return isInAppFolder(cwd) ? { developerInstructions: appsConventions() } : {};
|
|
44774
|
+
}
|
|
44753
44775
|
function autoApproveToolUserInput(client) {
|
|
44754
44776
|
client.onRequest("item/tool/requestUserInput", async (params) => {
|
|
44755
44777
|
const questions = params.questions ?? [];
|
|
@@ -44774,6 +44796,7 @@ async function getOrCreateThread(session) {
|
|
|
44774
44796
|
}
|
|
44775
44797
|
const config = parseRuntimeConfig(session);
|
|
44776
44798
|
const sandbox = sandboxType(config);
|
|
44799
|
+
const instructions = threadInstructionParams(cwd);
|
|
44777
44800
|
let threadId;
|
|
44778
44801
|
if (session.external_thread_id) {
|
|
44779
44802
|
const result = await client.request("thread/resume", {
|
|
@@ -44781,7 +44804,8 @@ async function getOrCreateThread(session) {
|
|
|
44781
44804
|
cwd,
|
|
44782
44805
|
approvalPolicy: "never",
|
|
44783
44806
|
sandbox,
|
|
44784
|
-
model: session.model ?? void 0
|
|
44807
|
+
model: session.model ?? void 0,
|
|
44808
|
+
...instructions
|
|
44785
44809
|
});
|
|
44786
44810
|
threadId = result.thread.id;
|
|
44787
44811
|
} else {
|
|
@@ -44789,7 +44813,8 @@ async function getOrCreateThread(session) {
|
|
|
44789
44813
|
cwd,
|
|
44790
44814
|
approvalPolicy: "never",
|
|
44791
44815
|
sandbox,
|
|
44792
|
-
model: session.model ?? void 0
|
|
44816
|
+
model: session.model ?? void 0,
|
|
44817
|
+
...instructions
|
|
44793
44818
|
});
|
|
44794
44819
|
threadId = result.thread.id;
|
|
44795
44820
|
const db2 = getDb();
|
|
@@ -47703,6 +47728,16 @@ function turnImageDataUrls(turn) {
|
|
|
47703
47728
|
if (!attachments) return [];
|
|
47704
47729
|
return attachments.filter((a) => a?.media_type?.startsWith("image/") && a.data).map((a) => `data:${a.media_type};base64,${a.data}`);
|
|
47705
47730
|
}
|
|
47731
|
+
function runtimePrompt(session, prompt, firstInput) {
|
|
47732
|
+
if (!firstInput || !isInAppFolder(session.cwd)) return prompt;
|
|
47733
|
+
const conventions = `<mini_app_conventions>
|
|
47734
|
+
${appsConventions()}</mini_app_conventions>`;
|
|
47735
|
+
return prompt.startsWith("/") ? `${prompt}
|
|
47736
|
+
|
|
47737
|
+
${conventions}` : `${conventions}
|
|
47738
|
+
|
|
47739
|
+
${prompt}`;
|
|
47740
|
+
}
|
|
47706
47741
|
async function startTurn3(session, turn) {
|
|
47707
47742
|
const db2 = getDb();
|
|
47708
47743
|
let client;
|
|
@@ -47723,6 +47758,7 @@ async function startTurn3(session, turn) {
|
|
|
47723
47758
|
);
|
|
47724
47759
|
}
|
|
47725
47760
|
void registerMiniAppMcp(baseUrl, session.cwd ?? "");
|
|
47761
|
+
const firstInput = !turn.thread_id && !sessionMap3.has(session.id) && !session.external_thread_id;
|
|
47726
47762
|
let entry;
|
|
47727
47763
|
try {
|
|
47728
47764
|
entry = turn.thread_id ? await ensureThreadSession(session, turn, client, baseUrl) : await ensureSession(session, client, baseUrl);
|
|
@@ -47768,7 +47804,7 @@ async function startTurn3(session, turn) {
|
|
|
47768
47804
|
try {
|
|
47769
47805
|
const images = turnImageDataUrls(turn);
|
|
47770
47806
|
await entry.client.createInput(entry.repoburgSessionId, {
|
|
47771
|
-
user_prompt: turn.user_prompt,
|
|
47807
|
+
user_prompt: runtimePrompt(session, turn.user_prompt, firstInput),
|
|
47772
47808
|
execution_strategy: "auto_apply",
|
|
47773
47809
|
...images.length > 0 ? { images } : {}
|
|
47774
47810
|
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample mini app — PR Review (multi-file TypeScript).
|
|
3
|
+
*
|
|
4
|
+
* Entry file: owns the UI state and wires window.AgentPane (turn events,
|
|
5
|
+
* the submit_review tool call, persistent state). Domain helpers (review
|
|
6
|
+
* prompt, verdict parsing) live in review.ts; shared types in types.ts.
|
|
7
|
+
*
|
|
8
|
+
* The pane bundles this file together with every module it imports into
|
|
9
|
+
* ONE compiled script — edit freely, the imports just work.
|
|
10
|
+
*/
|
|
11
|
+
import type { AppUi, ReviewVerdict } from './types';
|
|
12
|
+
import { isReviewStatus } from './types';
|
|
13
|
+
import { REVIEW_PROMPT, parseToolVerdict, parseTurnVerdict } from './review';
|
|
14
|
+
|
|
15
|
+
const ui: AppUi = {
|
|
16
|
+
status: 'idle',
|
|
17
|
+
blockers: 0,
|
|
18
|
+
summary: '',
|
|
19
|
+
repo: '',
|
|
20
|
+
runtime: 'repoburg',
|
|
21
|
+
sessionId: null,
|
|
22
|
+
sessionRepo: null,
|
|
23
|
+
sessionRuntime: null,
|
|
24
|
+
running: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Typed DOM lookup (markup lives in index.html). */
|
|
28
|
+
function byId<T extends HTMLElement>(id: string): T | null {
|
|
29
|
+
return document.getElementById(id) as T | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Persist the UI state — survives pane reloads (app_instances row). */
|
|
33
|
+
function save(): Promise<unknown> {
|
|
34
|
+
return AgentPane.setState({ ...ui });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function render(): void {
|
|
38
|
+
const verdict = byId('verdict');
|
|
39
|
+
if (verdict) verdict.className = 'verdict ' + ui.status;
|
|
40
|
+
const count = byId('count');
|
|
41
|
+
if (count) count.textContent = ui.status === 'green' ? '\u2714' : String(ui.blockers);
|
|
42
|
+
const text = byId('verdict-text');
|
|
43
|
+
if (text) {
|
|
44
|
+
text.textContent =
|
|
45
|
+
ui.status === 'green' ? 'Review passed' :
|
|
46
|
+
ui.status === 'red' ? 'Review failed \u2014 ' + ui.blockers + ' blocker(s)' :
|
|
47
|
+
ui.status === 'running' ? 'Review running\u2026' :
|
|
48
|
+
ui.status === 'error' ? 'Review errored' : 'No review yet';
|
|
49
|
+
}
|
|
50
|
+
const summary = byId('summary');
|
|
51
|
+
if (summary) summary.textContent = ui.summary;
|
|
52
|
+
const repo = byId<HTMLInputElement>('repo');
|
|
53
|
+
if (repo) repo.value = ui.repo;
|
|
54
|
+
const runtime = byId<HTMLSelectElement>('runtime');
|
|
55
|
+
if (runtime) runtime.value = ui.runtime;
|
|
56
|
+
const run = byId<HTMLButtonElement>('run');
|
|
57
|
+
if (run) {
|
|
58
|
+
run.disabled = ui.running;
|
|
59
|
+
run.textContent = ui.running ? 'Running\u2026' : 'Run review';
|
|
60
|
+
}
|
|
61
|
+
if (!ui.running) {
|
|
62
|
+
const progress = byId('progress');
|
|
63
|
+
if (progress) progress.textContent = '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyVerdict(verdict: ReviewVerdict): void {
|
|
68
|
+
ui.status = verdict.status;
|
|
69
|
+
ui.blockers = verdict.blockers;
|
|
70
|
+
ui.summary = verdict.summary;
|
|
71
|
+
ui.running = false;
|
|
72
|
+
render();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Agent → app tool call (MCP gateway). Registered SYNCHRONOUSLY at startup
|
|
76
|
+
// so the headless runtime can serve it without an open tab. The verdict
|
|
77
|
+
// arrives structured — no JSON parsing of the agent answer is needed.
|
|
78
|
+
AgentPane.onToolCall(async (payload) => {
|
|
79
|
+
if (payload.tool !== 'submit_review') {
|
|
80
|
+
await AgentPane.resolveToolCall(payload.callId, { error: 'unknown tool: ' + payload.tool });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const verdict = parseToolVerdict(payload.args ?? {});
|
|
84
|
+
if (!verdict) {
|
|
85
|
+
await AgentPane.resolveToolCall(payload.callId, { error: 'invalid submit_review arguments' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
applyVerdict(verdict);
|
|
89
|
+
await save();
|
|
90
|
+
await AgentPane.resolveToolCall(payload.callId, { recorded: true, status: ui.status });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
async function init(): Promise<void> {
|
|
94
|
+
const state = await AgentPane.getState();
|
|
95
|
+
const d = state?.data ?? {};
|
|
96
|
+
ui.repo = typeof d.repo === 'string' ? d.repo : '';
|
|
97
|
+
ui.runtime = typeof d.runtime === 'string' && d.runtime ? d.runtime : 'repoburg';
|
|
98
|
+
ui.status = isReviewStatus(d.status) ? d.status : 'idle';
|
|
99
|
+
ui.blockers = typeof d.blockers === 'number' ? d.blockers : 0;
|
|
100
|
+
ui.summary = typeof d.summary === 'string' ? d.summary : '';
|
|
101
|
+
ui.sessionId = typeof d.sessionId === 'string' ? d.sessionId : null;
|
|
102
|
+
ui.sessionRepo = typeof d.sessionRepo === 'string' ? d.sessionRepo : null;
|
|
103
|
+
ui.sessionRuntime = typeof d.sessionRuntime === 'string' ? d.sessionRuntime : null;
|
|
104
|
+
ui.running = d.running === true;
|
|
105
|
+
render();
|
|
106
|
+
|
|
107
|
+
// Turn lifecycle pushed by the pane (host → app relay). Only turns of a
|
|
108
|
+
// review job started here (Run review) are parsed — a verdict submitted
|
|
109
|
+
// via the submit_review tool already set the state and must not be
|
|
110
|
+
// clobbered by the same turn's plain-text completion.
|
|
111
|
+
AgentPane.onTurnCompleted((event, payload) => {
|
|
112
|
+
if (!ui.running) return;
|
|
113
|
+
ui.running = false;
|
|
114
|
+
if (event === 'turn:failed') {
|
|
115
|
+
ui.status = 'error';
|
|
116
|
+
ui.summary = payload?.error ? String(payload.error) : 'turn failed';
|
|
117
|
+
} else {
|
|
118
|
+
const verdict = parseTurnVerdict(payload?.response ?? '');
|
|
119
|
+
if (verdict) {
|
|
120
|
+
applyVerdict(verdict);
|
|
121
|
+
} else {
|
|
122
|
+
ui.status = 'error';
|
|
123
|
+
ui.summary = 'The agent answer did not end with a JSON block.';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
save();
|
|
127
|
+
render();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Live streaming: show the agent's latest delta text while the review runs.
|
|
131
|
+
AgentPane.onTurnDelta((payload) => {
|
|
132
|
+
if (!ui.running) return;
|
|
133
|
+
const el = byId('progress');
|
|
134
|
+
if (el) el.textContent = String(payload.delta ?? '');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const runBtn = byId<HTMLButtonElement>('run');
|
|
139
|
+
if (runBtn) {
|
|
140
|
+
runBtn.addEventListener('click', async () => {
|
|
141
|
+
const repoInput = byId<HTMLInputElement>('repo');
|
|
142
|
+
const runtimeInput = byId<HTMLSelectElement>('runtime');
|
|
143
|
+
if (!repoInput || !runtimeInput) return;
|
|
144
|
+
ui.repo = repoInput.value.trim();
|
|
145
|
+
ui.runtime = runtimeInput.value;
|
|
146
|
+
if (!ui.repo) return;
|
|
147
|
+
ui.running = true;
|
|
148
|
+
ui.status = 'running';
|
|
149
|
+
render();
|
|
150
|
+
try {
|
|
151
|
+
// Reuse the session only when it was created for THIS repo + runtime;
|
|
152
|
+
// a changed repo/runtime needs a fresh session (cwd is fixed per session).
|
|
153
|
+
let sessionId: string;
|
|
154
|
+
if (ui.sessionId === null || ui.sessionRepo !== ui.repo || ui.sessionRuntime !== ui.runtime) {
|
|
155
|
+
const session = await AgentPane.createSession({
|
|
156
|
+
title: 'PR Review job',
|
|
157
|
+
runtime_type: ui.runtime,
|
|
158
|
+
cwd: ui.repo,
|
|
159
|
+
});
|
|
160
|
+
sessionId = session.id;
|
|
161
|
+
ui.sessionRepo = ui.repo;
|
|
162
|
+
ui.sessionRuntime = ui.runtime;
|
|
163
|
+
} else {
|
|
164
|
+
sessionId = ui.sessionId;
|
|
165
|
+
}
|
|
166
|
+
ui.sessionId = sessionId;
|
|
167
|
+
await save();
|
|
168
|
+
await AgentPane.submitTurn(sessionId, REVIEW_PROMPT);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
ui.running = false;
|
|
171
|
+
ui.status = 'error';
|
|
172
|
+
ui.summary = err instanceof Error ? err.message : String(err);
|
|
173
|
+
// A cached session id can go stale (session deleted, other pane DB) —
|
|
174
|
+
// drop it so the next run creates a fresh session instead of 404 loops.
|
|
175
|
+
ui.sessionId = null;
|
|
176
|
+
ui.sessionRepo = null;
|
|
177
|
+
ui.sessionRuntime = null;
|
|
178
|
+
save();
|
|
179
|
+
render();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
init();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: Inter, system-ui, sans-serif; margin: 24px; color: #3c3c3c; background: #fff; }
|
|
7
|
+
h1 { font-size: 16px; margin: 0 0 16px; }
|
|
8
|
+
.row { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
9
|
+
input { flex: 1; padding: 8px 10px; font-size: 13px; border: 1px solid #e2e0db; border-radius: 6px; }
|
|
10
|
+
select { padding: 8px; font-size: 13px; border: 1px solid #e2e0db; border-radius: 6px; background: #fff; }
|
|
11
|
+
button { padding: 8px 16px; font-size: 13px; background: #d97706; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
|
|
12
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
13
|
+
.verdict { display: none; align-items: center; gap: 8px; padding: 16px; border-radius: 8px; font-size: 14px; font-weight: 600; margin-bottom: 16px; }
|
|
14
|
+
.verdict.idle, .verdict.running, .verdict.error { display: flex; background: #f5f3ef; color: #8a857c; }
|
|
15
|
+
.verdict.green { display: flex; background: rgba(34,197,94,0.1); color: #15803d; }
|
|
16
|
+
.verdict.red { display: flex; background: rgba(239,68,68,0.1); color: #b91c1c; }
|
|
17
|
+
.count { font-size: 22px; }
|
|
18
|
+
.summary { font-size: 13px; color: #8a857c; margin-bottom: 8px; }
|
|
19
|
+
.progress { font-size: 11px; color: #b0aaa0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; margin-bottom: 8px; min-height: 14px; }
|
|
20
|
+
.note { font-size: 12px; color: #8a857c; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<h1>PR Review</h1>
|
|
25
|
+
<!-- Markup only — the pane injects the shim (window.AgentPane) into the
|
|
26
|
+
head and the compiled app logic (app.ts + its imported modules,
|
|
27
|
+
bundled) before the closing body tag. Do NOT reference the logic
|
|
28
|
+
files from here (a relative src cannot resolve in a srcdoc iframe). -->
|
|
29
|
+
<div id="verdict" class="verdict"><span id="count" class="count"></span><span id="verdict-text"></span></div>
|
|
30
|
+
<div class="row">
|
|
31
|
+
<input id="repo" placeholder="Repository path (cwd for the review session)" />
|
|
32
|
+
<select id="runtime" title="Agent runtime for the review session">
|
|
33
|
+
<option value="repoburg">Repoburg</option>
|
|
34
|
+
<option value="claude">Claude</option>
|
|
35
|
+
<option value="codex">Codex</option>
|
|
36
|
+
</select>
|
|
37
|
+
<button id="run">Run review</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="summary" class="summary"></div>
|
|
40
|
+
<div id="progress" class="progress"></div>
|
|
41
|
+
<p class="note">This is a sample mini app. It opens an agent session on the repository, runs a review job, and stores the verdict in its persistent state.</p>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "PR Review",
|
|
3
|
+
"description": "Sample mini app: run a repository review and show a green/red verdict with the blocker count.",
|
|
4
|
+
"icon": "🔍",
|
|
5
|
+
"tools": [
|
|
6
|
+
{
|
|
7
|
+
"name": "submit_review",
|
|
8
|
+
"description": "Submit the final verdict of the repository review. Call this exactly once, at the end of the review.",
|
|
9
|
+
"inputSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"status": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"enum": ["green", "red"],
|
|
15
|
+
"description": "green = review passed, red = blocking issues found"
|
|
16
|
+
},
|
|
17
|
+
"blockers": {
|
|
18
|
+
"type": "number",
|
|
19
|
+
"description": "Number of blocking issues found (0 for green)."
|
|
20
|
+
},
|
|
21
|
+
"summary": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "One-line verdict shown in the app."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": ["status", "blockers", "summary"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Review domain logic — the review job prompt and verdict parsing
|
|
3
|
+
* (both the fallback JSON-block contract and the submit_review tool args).
|
|
4
|
+
*/
|
|
5
|
+
import type { ReviewVerdict } from './types';
|
|
6
|
+
|
|
7
|
+
/** Review job sent to the agent: JSON result contract + tool call. */
|
|
8
|
+
export const REVIEW_PROMPT =
|
|
9
|
+
'Review the current repository as a senior engineer. Look at the recent ' +
|
|
10
|
+
'changes (git status / recent commits) and report blocking issues only. ' +
|
|
11
|
+
'When the review is done, call the MCP tool `pr-review__submit_review` ' +
|
|
12
|
+
'exactly once with the verdict: status ("green" or "red"), blockers ' +
|
|
13
|
+
'(number), and a one-line summary.';
|
|
14
|
+
|
|
15
|
+
/** Parse the LAST ```json fenced block from the agent's answer. */
|
|
16
|
+
export function parseJsonBlock(text: string): unknown {
|
|
17
|
+
const blocks = text.match(/```json\n([\s\S]*?)\n```/g) ?? [];
|
|
18
|
+
if (blocks.length === 0) return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(blocks[blocks.length - 1].replace(/```json\n|\n```/g, ''));
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Fallback verdict from the turn's plain-text answer (JSON block). */
|
|
27
|
+
export function parseTurnVerdict(text: string): ReviewVerdict | null {
|
|
28
|
+
const raw = parseJsonBlock(text);
|
|
29
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
30
|
+
const obj = raw as Record<string, unknown>;
|
|
31
|
+
return normalizeVerdict(obj);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Structured verdict from the submit_review tool call arguments. */
|
|
35
|
+
export function parseToolVerdict(args: Record<string, unknown>): ReviewVerdict | null {
|
|
36
|
+
return normalizeVerdict(args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Map one verdict-shaped object to ReviewVerdict; null when invalid. */
|
|
40
|
+
function normalizeVerdict(obj: Record<string, unknown>): ReviewVerdict | null {
|
|
41
|
+
if (obj.status !== 'green' && obj.status !== 'red') return null;
|
|
42
|
+
return {
|
|
43
|
+
status: obj.status,
|
|
44
|
+
blockers: typeof obj.blockers === 'number' ? obj.blockers : 0,
|
|
45
|
+
summary: typeof obj.summary === 'string' ? obj.summary : '',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the PR Review mini app.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Verdict/UI status machine of the app. */
|
|
6
|
+
export type ReviewStatus = 'idle' | 'running' | 'green' | 'red' | 'error';
|
|
7
|
+
|
|
8
|
+
/** A review outcome — from the submit_review tool or a JSON result block. */
|
|
9
|
+
export interface ReviewVerdict {
|
|
10
|
+
status: 'green' | 'red';
|
|
11
|
+
blockers: number;
|
|
12
|
+
summary: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Persistent UI state — survives pane reloads (app_instances row). */
|
|
16
|
+
export interface AppUi {
|
|
17
|
+
status: ReviewStatus;
|
|
18
|
+
blockers: number;
|
|
19
|
+
summary: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
runtime: string;
|
|
22
|
+
sessionId: string | null;
|
|
23
|
+
sessionRepo: string | null;
|
|
24
|
+
sessionRuntime: string | null;
|
|
25
|
+
running: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Narrow an unknown persisted field back to ReviewStatus. */
|
|
29
|
+
export function isReviewStatus(value: unknown): value is ReviewStatus {
|
|
30
|
+
return (
|
|
31
|
+
value === 'idle' ||
|
|
32
|
+
value === 'running' ||
|
|
33
|
+
value === 'green' ||
|
|
34
|
+
value === 'red' ||
|
|
35
|
+
value === 'error'
|
|
36
|
+
);
|
|
37
|
+
}
|