prompts/code_review_prompts.txt ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are a veteran principal engineer. Review the following code changes :
2
+ identify concrete issues, explain impact, and propose minimal, correct patches.
3
+ Prioritize security, maintainability, and extensibility over cleverness.
4
+ When in doubt, choose the fix with the lowest blast radius and clearest long-term path.
5
+
6
+ Context
7
+
8
+ Repository/Service: {repo_name}
9
+
10
+ Change Scope: {brief_change_summary}
11
+
12
+ Diff/Patch (unified):
13
+
14
+ {diff_or_code_block}
15
+
16
+ Extra context based on Change Scope
17
+
18
+ {tool_outputs}
19
+
20
+ ---
21
+ Architectural/Domain notes (if any):
22
+ None
23
+ Non-negotiable guidelines (style, lint, frameworks, security baselines):
24
+ None
25
+ Threat model highlights (if known):
26
+ None
27
+ Performance/SLOs:
28
+ None
29
+ ---
30
+ What to Deliver (strict structure)
31
+ Executive Summary (≤10 bullets)
32
+
33
+ 3–5 top risks (by user impact and exploitability)
34
+
35
+ Overall maintainability & extensibility posture
36
+ Overall security posture
37
+ Must-fix before merge (list)
38
+ Findings (one object per issue; exhaustive but de-duplicated)
39
+
40
+ ```
41
+ title: "Short, specific title"
42
+ severity: "critical|high|medium|low|info"
43
+ category: "security|maintainability|extensibility|performance|reliability|testing|compliance"
44
+ ai_generated_smell: true
45
+ file: "relative/path.ext"
46
+ lines: "start-end"
47
+ code_snippet: "short excerpt that shows the problem"
48
+ why_it_matters: "Impact in plain language; include user impact and ops risk"
49
+ root_cause: "What design/assumption caused this"
50
+ cwe: "CWE-### (if security) or 'N/A'"
51
+ owasp_top10: "A# (if web) or 'N/A'"
52
+ fix:
53
+ strategy: "Minimal change strategy that fits current design"
54
+ patch: "unified diff applying the minimal safe fix"
55
+ follow_up:
56
+ - "refactor debts that can be deferred safely"
57
+ tests:
58
+ new_or_changed:
59
+ - "test names to add/update"
60
+ cases:
61
+ - "happy path"
62
+ - "edge case 1"
63
+ - "edge case 2"
64
+ - "negative case"
65
+ - "property-based (if relevant)"
66
+ breaking_change_risk: "none|low|medium|high"
67
+ migration_notes: "If any contract/DB/index/config migration is needed"
68
+ references:
69
+ - "links to official docs or standards (if applicable)"
70
+ ```
71
+
72
+ Scorecard (0–5; justify each score in 1–2 sentences)
73
+
74
+ Security:
75
+ Maintainability:
76
+ Extensibility:
77
+ Reliability (errors, retries, idempotency, timeouts):
78
+ Test Quality & Coverage:
79
+ Performance/Complexity:
80
+ Documentation & Naming:
protos/agent.proto ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ syntax = "proto3";
2
+
3
+ package agent;
4
+
5
+ service CodeReviewAgent {
6
+ rpc ReviewPR (ReviewRequest) returns (stream ReviewResponse) {}
7
+ }
8
+
9
+ message ReviewRequest {
10
+ string repo_url = 1;
11
+ int32 pr_number = 2;
12
+ }
13
+
14
+ message ReviewResponse {
15
+ string status = 1;
16
+ string review_comment = 2;
17
+ string file_path = 3;
18
+ }
protos/agent_pb2.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: protos/agent.proto
5
+ # Protobuf Python Version: 5.29.0
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 5,
15
+ 29,
16
+ 0,
17
+ '',
18
+ 'protos/agent.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12protos/agent.proto\x12\x05\x61gent\"4\n\rReviewRequest\x12\x10\n\x08repo_url\x18\x01 \x01(\t\x12\x11\n\tpr_number\x18\x02 \x01(\x05\"K\n\x0eReviewResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x16\n\x0ereview_comment\x18\x02 \x01(\t\x12\x11\n\tfile_path\x18\x03 \x01(\t2N\n\x0f\x43odeReviewAgent\x12;\n\x08ReviewPR\x12\x14.agent.ReviewRequest\x1a\x15.agent.ReviewResponse\"\x00\x30\x01\x62\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.agent_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_REVIEWREQUEST']._serialized_start=29
35
+ _globals['_REVIEWREQUEST']._serialized_end=81
36
+ _globals['_REVIEWRESPONSE']._serialized_start=83
37
+ _globals['_REVIEWRESPONSE']._serialized_end=158
38
+ _globals['_CODEREVIEWAGENT']._serialized_start=160
39
+ _globals['_CODEREVIEWAGENT']._serialized_end=238
40
+ # @@protoc_insertion_point(module_scope)
protos/agent_pb2_grpc.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+ from protos import agent_pb2 as protos_dot_agent__pb2
7
+
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
+ GRPC_VERSION = grpc.__version__
10
+ _version_not_supported = False
11
+
12
+ try:
13
+ from grpc._utilities import first_version_is_lower
14
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
15
+ except ImportError:
16
+ _version_not_supported = True
17
+
18
+ if _version_not_supported:
19
+ raise RuntimeError(
20
+ f'The grpc package installed is at version {GRPC_VERSION},'
21
+ + f' but the generated code in protos/agent_pb2_grpc.py depends on'
22
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
23
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
24
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
25
+ )
26
+
27
+
28
+ class CodeReviewAgentStub(object):
29
+ """Missing associated documentation comment in .proto file."""
30
+
31
+ def __init__(self, channel):
32
+ """Constructor.
33
+
34
+ Args:
35
+ channel: A grpc.Channel.
36
+ """
37
+ self.ReviewPR = channel.unary_stream(
38
+ '/agent.CodeReviewAgent/ReviewPR',
39
+ request_serializer=protos_dot_agent__pb2.ReviewRequest.SerializeToString,
40
+ response_deserializer=protos_dot_agent__pb2.ReviewResponse.FromString,
41
+ _registered_method=True)
42
+
43
+
44
+ class CodeReviewAgentServicer(object):
45
+ """Missing associated documentation comment in .proto file."""
46
+
47
+ def ReviewPR(self, request, context):
48
+ """Missing associated documentation comment in .proto file."""
49
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
50
+ context.set_details('Method not implemented!')
51
+ raise NotImplementedError('Method not implemented!')
52
+
53
+
54
+ def add_CodeReviewAgentServicer_to_server(servicer, server):
55
+ rpc_method_handlers = {
56
+ 'ReviewPR': grpc.unary_stream_rpc_method_handler(
57
+ servicer.ReviewPR,
58
+ request_deserializer=protos_dot_agent__pb2.ReviewRequest.FromString,
59
+ response_serializer=protos_dot_agent__pb2.ReviewResponse.SerializeToString,
60
+ ),
61
+ }
62
+ generic_handler = grpc.method_handlers_generic_handler(
63
+ 'agent.CodeReviewAgent', rpc_method_handlers)
64
+ server.add_generic_rpc_handlers((generic_handler,))
65
+ server.add_registered_method_handlers('agent.CodeReviewAgent', rpc_method_handlers)
66
+
67
+
68
+ # This class is part of an EXPERIMENTAL API.
69
+ class CodeReviewAgent(object):
70
+ """Missing associated documentation comment in .proto file."""
71
+
72
+ @staticmethod
73
+ def ReviewPR(request,
74
+ target,
75
+ options=(),
76
+ channel_credentials=None,
77
+ call_credentials=None,
78
+ insecure=False,
79
+ compression=None,
80
+ wait_for_ready=None,
81
+ timeout=None,
82
+ metadata=None):
83
+ return grpc.experimental.unary_stream(
84
+ request,
85
+ target,
86
+ '/agent.CodeReviewAgent/ReviewPR',
87
+ protos_dot_agent__pb2.ReviewRequest.SerializeToString,
88
+ protos_dot_agent__pb2.ReviewResponse.FromString,
89
+ options,
90
+ channel_credentials,
91
+ insecure,
92
+ call_credentials,
93
+ compression,
94
+ wait_for_ready,
95
+ timeout,
96
+ metadata,
97
+ _registered_method=True)
schemas/steps_schema.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "summary": "Brief summary of the steps and what you are trying to achieve.",
4
+ "steps": [
5
+ {
6
+ "name": "step1",
7
+ "description": "Describe the first step in detail. If any tool is used, mention the tool name and parameters. If more context is needed for the code to be reviewed, like caller or callee or function references, mention the tool to be used to get that context as given in tools schema YAML. Example below.",
8
+ "tools": [
9
+ {
10
+ "name": "ast_mcp_tools",
11
+ "function": "get_function_context_for_project_mcp",
12
+ "parameters": {
13
+ "github_repo": "https://github.com/huggingface/accelerate",
14
+ "function_name": "example_function"
15
+ }
16
+ }
17
+ ],
18
+ "tool_results" : "Results from the tool execution can be captured here to be used in subsequent steps.Will be filled in by the agent during execution."
19
+ },
20
+ {
21
+ "name": "step2",
22
+ "description": "Second step description. If no tool is used, just describe the step. Continue for all steps needed for the code review."
23
+ }
24
+ ]
25
+ }
templates/index.html ADDED
@@ -0,0 +1,714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Code Review Agent</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ .step-content {
11
+ display: none;
12
+ }
13
+
14
+ .step-content.open {
15
+ display: block;
16
+ }
17
+
18
+ /* Custom scrollbar for sidebar */
19
+ .sidebar-scroll::-webkit-scrollbar {
20
+ width: 6px;
21
+ }
22
+
23
+ .sidebar-scroll::-webkit-scrollbar-track {
24
+ background: #f1f1f1;
25
+ }
26
+
27
+ .sidebar-scroll::-webkit-scrollbar-thumb {
28
+ background: #888;
29
+ border-radius: 3px;
30
+ }
31
+
32
+ .sidebar-scroll::-webkit-scrollbar-thumb:hover {
33
+ background: #555;
34
+ }
35
+
36
+ /* JSON Renderer Styles */
37
+ .json-key {
38
+ color: #4b5563;
39
+ font-weight: 600;
40
+ }
41
+
42
+ .json-string {
43
+ color: #059669;
44
+ }
45
+
46
+ .json-number {
47
+ color: #d97706;
48
+ }
49
+
50
+ .json-boolean {
51
+ color: #7c3aed;
52
+ }
53
+
54
+ .json-null {
55
+ color: #9ca3af;
56
+ }
57
+
58
+ .json-block {
59
+ margin-left: 1.5rem;
60
+ border-left: 2px solid #e5e7eb;
61
+ padding-left: 0.5rem;
62
+ margin-top: 0.25rem;
63
+ margin-bottom: 0.25rem;
64
+ }
65
+ </style>
66
+ </head>
67
+
68
+ <body class="bg-gray-100 h-screen flex overflow-hidden">
69
+
70
+ <!-- Sidebar -->
71
+ <div class="w-64 bg-white shadow-lg flex flex-col h-full">
72
+ <div class="p-4 border-b">
73
+ <h1 class="text-xl font-bold text-gray-800">Agentic AI Code Review</h1>
74
+ </div>
75
+
76
+ <div class="p-4 border-b">
77
+ <div id="settingsPanel" class="space-y-3">
78
+ <div>
79
+ <label for="mcpUrl" class="block text-xs font-medium text-gray-700">MCP Server URL</label>
80
+ <div class="flex space-x-2 mt-1">
81
+ <input type="text" id="mcpUrl" name="mcpUrl"
82
+ class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-xs p-2 border"
83
+ placeholder="https://alexcpn-treesitter-mcp.hf.space/mcp/"
84
+ value="https://alexcpn-treesitter-mcp.hf.space/mcp/">
85
+ <button type="button" id="connectMcpBtn"
86
+ class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-xs font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
87
+ Connect
88
+ </button>
89
+ </div>
90
+ <div id="toolsList" class="mt-2 text-xs text-gray-600 hidden">
91
+ <p class="font-semibold mb-1">Available Tools:</p>
92
+ <ul class="list-disc pl-4 space-y-1" id="toolsListContent"></ul>
93
+ </div>
94
+ </div>
95
+ <div>
96
+ <label for="apiKey" class="block text-xs font-medium text-gray-700">OpenAI API Key
97
+ (Optional)</label>
98
+ <input type="password" id="apiKey" name="apiKey"
99
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-xs p-2 border"
100
+ placeholder="sk-...">
101
+ </div>
102
+ <div class="flex space-x-2">
103
+ <button type="button" id="fetchHistoryBtn"
104
+ class="flex-1 justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
105
+ Fetch History
106
+ </button>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="flex-1 overflow-y-auto sidebar-scroll p-4">
112
+ <h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">History</h2>
113
+ <div id="runsList" class="space-y-2">
114
+ <!-- Runs will be listed here -->
115
+ <div class="text-xs text-gray-400 text-center italic">No runs found</div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Main Content -->
121
+ <div class="flex-1 flex flex-col h-full overflow-hidden">
122
+ <header class="bg-white shadow-sm p-4 flex justify-between items-center z-10">
123
+ <h2 id="currentRunTitle" class="text-lg font-medium text-gray-900">Select a run or trigger a new one</h2>
124
+ <div class="flex items-center space-x-3">
125
+ <button id="refreshBtn" onclick="refreshCurrentRun()"
126
+ class="hidden px-3 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
127
+ Refresh
128
+ </button>
129
+ <span id="connectionStatus"
130
+ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
131
+ Disconnected
132
+ </span>
133
+ </div>
134
+ </header>
135
+
136
+ <main class="flex-1 overflow-y-auto p-6 space-y-6" id="mainContent">
137
+ <!-- New Review Section -->
138
+ <div id="newReviewSection" class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
139
+ <label for="prUrl" class="block text-sm font-medium text-gray-700 mb-2">Start a New Review</label>
140
+ <div class="flex space-x-3">
141
+ <input type="text" id="prUrl" name="prUrl"
142
+ class="flex-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm p-2 border"
143
+ placeholder="https://github.com/owner/repo/pull/123">
144
+ <button type="button" id="triggerBtn"
145
+ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
146
+ Trigger Review
147
+ </button>
148
+ </div>
149
+ </div>
150
+ <!-- Plan Section -->
151
+ <!-- Summary Section -->
152
+ <div id="summaryContainer" class="hidden mb-8">
153
+ <div class="bg-white rounded-lg shadow border-l-4 border-purple-500 overflow-hidden">
154
+ <div class="p-4 border-b border-gray-100">
155
+ <h3 class="text-lg font-semibold text-purple-700" id="summaryHeader">Executive Summary</h3>
156
+ </div>
157
+ <div id="summaryContent" class="p-6 prose prose-sm max-w-none text-gray-700 bg-gray-50">
158
+ <!-- Summary will be injected here -->
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- Files Container -->
164
+ <div id="filesContainer" class="space-y-8">
165
+ <!-- File reviews will be appended here -->
166
+ </div>
167
+
168
+ <!-- Debug Log -->
169
+ <div class="mt-8 p-4 bg-gray-800 text-green-400 rounded text-xs font-mono hidden" id="debugLog">
170
+ <h3 class="font-bold border-b border-gray-700 mb-2">Debug Log</h3>
171
+ <div id="debugContent" class="h-32 overflow-y-auto"></div>
172
+ </div>
173
+ </main>
174
+ </div>
175
+
176
+ <script>
177
+ const runsList = document.getElementById('runsList');
178
+ const filesContainer = document.getElementById('filesContainer');
179
+ const currentRunTitle = document.getElementById('currentRunTitle');
180
+ const connectionStatus = document.getElementById('connectionStatus');
181
+
182
+ let eventSource = null;
183
+ let currentRepo = '';
184
+ let currentPr = '';
185
+
186
+ // Load runs on input blur or submit
187
+ async function loadRuns(repoUrl, prNumber) {
188
+ if (!repoUrl || !prNumber) return;
189
+
190
+ const repoName = repoUrl.replace(/\/$/, '').split('/').pop();
191
+ currentRepo = repoName;
192
+ currentPr = prNumber;
193
+
194
+ try {
195
+ const response = await fetch(`/runs/${repoName}/${prNumber}`);
196
+ const data = await response.json();
197
+ renderRuns(data.runs);
198
+ } catch (error) {
199
+ console.error('Failed to load runs:', error);
200
+ }
201
+ }
202
+
203
+ function renderRuns(runs) {
204
+ runsList.innerHTML = '';
205
+ if (runs.length === 0) {
206
+ runsList.innerHTML = '<div class="text-xs text-gray-400 text-center italic">No runs found</div>';
207
+ return;
208
+ }
209
+
210
+ runs.forEach(run => {
211
+ const div = document.createElement('div');
212
+
213
+ let timeHash, repoName, prNumber;
214
+
215
+ if (typeof run === 'object') {
216
+ timeHash = run.time_hash;
217
+ repoName = run.repo_name;
218
+ prNumber = run.pr_number;
219
+ } else {
220
+ timeHash = run;
221
+ repoName = currentRepo;
222
+ prNumber = currentPr;
223
+ }
224
+
225
+ // Format timestamp: YYYYMMDDHHMMSS -> Local Date Time
226
+ const year = timeHash.substring(0, 4);
227
+ const month = timeHash.substring(4, 6);
228
+ const day = timeHash.substring(6, 8);
229
+ const hour = timeHash.substring(8, 10);
230
+ const minute = timeHash.substring(10, 12);
231
+ const second = timeHash.substring(12, 14);
232
+ const date = new Date(year, month - 1, day, hour, minute, second);
233
+
234
+ div.className = 'p-3 bg-white rounded border border-gray-200 hover:border-indigo-500 cursor-pointer transition-colors shadow-sm';
235
+
236
+ let content = `<div class="text-sm font-medium text-gray-900">Run ${timeHash}</div>`;
237
+ if (typeof run === 'object') {
238
+ content += `<div class="text-xs text-indigo-600 font-semibold mb-1">${repoName} #${prNumber}</div>`;
239
+ }
240
+ content += `<div class="text-xs text-gray-500">${date.toLocaleString()}</div>`;
241
+
242
+ div.innerHTML = content;
243
+ div.onclick = () => {
244
+ // Update context if switching runs
245
+ if (repoName && prNumber) {
246
+ currentRepo = repoName;
247
+ currentPr = prNumber;
248
+ // Only update input if we have a valid URL structure, otherwise keep it or clear it?
249
+ // Since we don't have the full owner/repo from the run history (only repo name),
250
+ // we can't reconstruct the full URL accurately.
251
+ // Better to NOT update the input with a broken URL.
252
+ // Or try to use the saved one if it matches.
253
+
254
+ const savedPrUrl = localStorage.getItem('lastPrUrl');
255
+ if (savedPrUrl && savedPrUrl.includes(repoName) && savedPrUrl.includes(prNumber)) {
256
+ document.getElementById('prUrl').value = savedPrUrl;
257
+ } else {
258
+ // If we can't reconstruct it, maybe just show a placeholder or don't touch it
259
+ // document.getElementById('prUrl').value = `https://github.com/.../${repoName}/pull/${prNumber}`;
260
+ }
261
+ }
262
+ loadRun(timeHash);
263
+ };
264
+ runsList.appendChild(div);
265
+ });
266
+ }
267
+
268
+ function loadRun(timeHash) {
269
+ log(`Loading run: ${timeHash}`);
270
+ currentRunHash = timeHash;
271
+ document.getElementById('refreshBtn').classList.remove('hidden');
272
+
273
+ // Update UI
274
+ currentRunTitle.textContent = `Viewing Run: ${timeHash} for Task: Code Review of PR ${currentRepo} #${currentPr}`;
275
+
276
+ // Clear previous content
277
+ filesContainer.innerHTML = '';
278
+ document.getElementById('summaryContainer').classList.add('hidden');
279
+ document.getElementById('summaryContent').innerHTML = '';
280
+
281
+ // Close existing stream
282
+ if (eventSource) {
283
+ eventSource.close();
284
+ }
285
+
286
+ // Start new stream
287
+ const streamUrl = `/stream/${currentRepo}/${currentPr}/${timeHash}`;
288
+ log(`Connecting to stream: ${streamUrl}`);
289
+ connectStream(streamUrl);
290
+ }
291
+
292
+ function connectStream(url) {
293
+ connectionStatus.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800';
294
+ connectionStatus.textContent = 'Connecting...';
295
+
296
+ eventSource = new EventSource(url);
297
+
298
+ eventSource.onopen = () => {
299
+ log('Stream connected');
300
+ connectionStatus.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
301
+ connectionStatus.textContent = 'Live';
302
+ };
303
+
304
+ eventSource.onmessage = (event) => {
305
+ // log(`Received event: ${event.data.substring(0, 50)}...`);
306
+ try {
307
+ const data = JSON.parse(event.data);
308
+ renderEvent(data);
309
+ } catch (e) {
310
+ log(`Error parsing event: ${e}`);
311
+ }
312
+ };
313
+
314
+ eventSource.onerror = (error) => {
315
+ log('Stream error');
316
+ console.error('EventSource failed:', error);
317
+ connectionStatus.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800';
318
+ connectionStatus.textContent = 'Disconnected';
319
+ // eventSource.close(); // Don't close immediately, it might reconnect
320
+ };
321
+ }
322
+
323
+ function renderEvent(data) {
324
+ const type = data.type;
325
+ const filePath = data.file_path;
326
+
327
+ log(`Rendering event: ${type} for ${filePath}`);
328
+
329
+ if (!filePath) {
330
+ log('Missing filePath, skipping');
331
+ return;
332
+ }
333
+
334
+ let content = {};
335
+ try {
336
+ content = JSON.parse(data.content || '{}');
337
+ } catch (e) {
338
+ content = { raw: data.content };
339
+ }
340
+
341
+ // Get or create file section (skip for summary)
342
+ let fileSection = null;
343
+ if (type !== 'summary') {
344
+ fileSection = document.getElementById(`file-${btoa(filePath)}`);
345
+ if (!fileSection) {
346
+ log(`Creating new section for ${filePath}`);
347
+ fileSection = createFileSection(filePath);
348
+ filesContainer.appendChild(fileSection);
349
+ }
350
+ }
351
+
352
+ const planContent = fileSection ? fileSection.querySelector('.plan-content') : null;
353
+ const planSection = fileSection ? fileSection.querySelector('.plan-section') : null;
354
+ const stepsSection = fileSection ? fileSection.querySelector('.steps-section') : null;
355
+
356
+ if (type === 'plan') {
357
+ planSection.classList.remove('hidden');
358
+ planContent.innerHTML = renderContent(content);
359
+
360
+ // Update header with timestamp if available (optional, or just keep static)
361
+ // const header = fileSection.querySelector('.plan-header-text');
362
+ // header.textContent = `Review Plan for ${filePath}`;
363
+
364
+ } else if (type === 'step') {
365
+ const stepName = data.step_name;
366
+ const stepId = `step-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
367
+
368
+ const div = document.createElement('div');
369
+ div.className = 'bg-white rounded-lg shadow border-l-4 border-green-500 overflow-hidden';
370
+ div.innerHTML = `
371
+ <div class="p-4 flex justify-between items-center cursor-pointer hover:bg-gray-50 transition-colors" onclick="toggleStep('${stepId}')">
372
+ <div class="flex items-center space-x-3">
373
+ <span class="flex-shrink-0 h-6 w-6 rounded-full bg-green-100 flex items-center justify-center text-green-600">✓</span>
374
+ <div>
375
+ <h3 class="text-md font-medium text-gray-900">${stepName}</h3>
376
+ </div>
377
+ </div>
378
+ <span class="text-gray-400 transform transition-transform duration-200" id="icon-${stepId}">▼</span>
379
+ </div>
380
+ <div id="${stepId}" class="step-content border-t border-gray-100 bg-gray-50">
381
+ <div class="p-4 text-sm font-mono text-gray-700 overflow-x-auto">
382
+ ${renderContent(content)}
383
+ </div>
384
+ </div>
385
+ `;
386
+ stepsSection.appendChild(div);
387
+ } else if (type === 'summary') {
388
+ const summaryContainer = document.getElementById('summaryContainer');
389
+ const summaryContent = document.getElementById('summaryContent');
390
+ const summaryHeader = document.getElementById('summaryHeader');
391
+
392
+ summaryContainer.classList.remove('hidden');
393
+
394
+ // Update header with full URL if available
395
+ if (data.repo_url && data.pr_number) {
396
+ const repoUrl = data.repo_url.replace(/\/$/, ""); // Remove trailing slash
397
+ const prUrl = `${repoUrl}/pull/${data.pr_number}`;
398
+ summaryHeader.innerHTML = `Executive Summary of <a href="${prUrl}" target="_blank" class="underline hover:text-purple-900">PR #${data.pr_number}</a>`;
399
+ } else {
400
+ summaryHeader.textContent = "Executive Summary";
401
+ }
402
+
403
+ // Simple markdown rendering (bold, headers, lists)
404
+ let html = data.content
405
+ .replace(/^# (.*$)/gim, '<h1 class="text-2xl font-bold mb-4">$1</h1>')
406
+ .replace(/^## (.*$)/gim, '<h2 class="text-xl font-bold mb-3 mt-4">$1</h2>')
407
+ .replace(/^### (.*$)/gim, '<h3 class="text-lg font-bold mb-2 mt-3">$1</h3>')
408
+ .replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
409
+ .replace(/^\- (.*$)/gim, '<li class="ml-4">$1</li>')
410
+ .replace(/\n/gim, '<br>');
411
+
412
+ summaryContent.innerHTML = html;
413
+ }
414
+ }
415
+
416
+ function createFileSection(filePath) {
417
+ const safeId = btoa(filePath);
418
+ const div = document.createElement('div');
419
+ div.id = `file-${safeId}`;
420
+ div.className = 'space-y-4';
421
+
422
+ div.innerHTML = `
423
+ <div class="flex items-center space-x-2 mb-4">
424
+ <svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
425
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
426
+ </svg>
427
+ <h2 class="text-xl font-bold text-gray-800">Review Plan for File: ${filePath}</h2>
428
+ </div>
429
+
430
+ <div class="plan-section hidden">
431
+ <div class="bg-white rounded-lg shadow border-l-4 border-blue-500 overflow-hidden">
432
+ <div class="p-4 border-b border-gray-100 flex justify-between items-center cursor-pointer hover:bg-gray-50 transition-colors"
433
+ onclick="togglePlan('${safeId}')">
434
+ <h3 class="plan-header-text text-lg font-semibold text-blue-700">Review Plan</h3>
435
+ <span class="text-gray-400 transform transition-transform duration-200" id="planIcon-${safeId}">▼</span>
436
+ </div>
437
+ <div id="planBody-${safeId}" class="p-4 transition-all duration-200">
438
+ <pre class="plan-content bg-gray-50 p-4 rounded text-sm overflow-x-auto font-mono text-gray-700 mb-6"></pre>
439
+
440
+ <h4 class="text-sm font-semibold text-gray-600 mb-3 uppercase tracking-wider">Execution Steps</h4>
441
+ <div class="steps-section space-y-4">
442
+ <!-- Steps will be appended here -->
443
+ </div>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ `;
448
+ return div;
449
+ }
450
+
451
+ function renderContent(data) {
452
+ if (typeof data === 'object' && data !== null) {
453
+ if (Array.isArray(data)) {
454
+ if (data.length === 0) return '<span class="text-gray-400">[]</span>';
455
+ let html = '<div class="json-block">';
456
+ data.forEach(item => {
457
+ html += `<div>${renderContent(item)}</div>`;
458
+ });
459
+ html += '</div>';
460
+ return html;
461
+ } else {
462
+ if (Object.keys(data).length === 0) return '<span class="text-gray-400">{}</span>';
463
+ let html = '<div class="json-block">';
464
+ for (const [key, value] of Object.entries(data)) {
465
+ html += `<div class="mb-1"><span class="json-key">${key}:</span> ${renderContent(value)}</div>`;
466
+ }
467
+ html += '</div>';
468
+ return html;
469
+ }
470
+ } else if (typeof data === 'string') {
471
+ // Check if it looks like a code block or long text
472
+ if (data.includes('\n') || data.length > 50) {
473
+ return `<div class="mt-1 p-2 bg-gray-100 rounded text-xs whitespace-pre-wrap border border-gray-200 text-gray-800">${escapeHtml(data)}</div>`;
474
+ }
475
+ return `<span class="json-string">"${escapeHtml(data)}"</span>`;
476
+ } else if (typeof data === 'number') {
477
+ return `<span class="json-number">${data}</span>`;
478
+ } else if (typeof data === 'boolean') {
479
+ return `<span class="json-boolean">${data}</span>`;
480
+ } else if (data === null) {
481
+ return `<span class="json-null">null</span>`;
482
+ }
483
+ return String(data);
484
+ }
485
+
486
+ function escapeHtml(text) {
487
+ const map = {
488
+ '&': '&amp;',
489
+ '<': '&lt;',
490
+ '>': '&gt;',
491
+ '"': '&quot;',
492
+ "'": '&#039;'
493
+ };
494
+ return text.replace(/[&<>"']/g, function (m) { return map[m]; });
495
+ }
496
+
497
+ window.toggleStep = function (id) {
498
+ const el = document.getElementById(id);
499
+ const icon = document.getElementById(`icon-${id}`);
500
+ el.classList.toggle('open');
501
+ if (el.classList.contains('open')) {
502
+ icon.style.transform = 'rotate(180deg)';
503
+ } else {
504
+ icon.style.transform = 'rotate(0deg)';
505
+ }
506
+ }
507
+
508
+ window.togglePlan = function (id) {
509
+ const el = document.getElementById(`planBody-${id}`);
510
+ const icon = document.getElementById(`planIcon-${id}`);
511
+ if (el.style.display === 'none') {
512
+ el.style.display = 'block';
513
+ icon.style.transform = 'rotate(0deg)';
514
+ } else {
515
+ el.style.display = 'none';
516
+ icon.style.transform = 'rotate(-90deg)';
517
+ }
518
+ }
519
+
520
+ function parsePrUrl(url) {
521
+ try {
522
+ // Expected format: https://github.com/owner/repo/pull/123
523
+ const regex = /github\.com\/([^\/]+\/[^\/]+)\/pull\/(\d+)/;
524
+ const match = url.match(regex);
525
+ if (match) {
526
+ return {
527
+ repoUrl: `https://github.com/${match[1]}`,
528
+ prNumber: match[2]
529
+ };
530
+ }
531
+ } catch (e) {
532
+ console.error("Error parsing URL", e);
533
+ }
534
+ return null;
535
+ }
536
+
537
+ // MCP Connection Handler
538
+ document.getElementById('connectMcpBtn').addEventListener('click', async () => {
539
+ const mcpUrl = document.getElementById('mcpUrl').value;
540
+ const toolsList = document.getElementById('toolsList');
541
+ const toolsListContent = document.getElementById('toolsListContent');
542
+ const btn = document.getElementById('connectMcpBtn');
543
+
544
+ if (!mcpUrl) return;
545
+
546
+ btn.disabled = true;
547
+ btn.textContent = 'Connecting...';
548
+ toolsList.classList.add('hidden');
549
+
550
+ try {
551
+ const response = await fetch('/list-tools', {
552
+ method: 'POST',
553
+ headers: { 'Content-Type': 'application/json' },
554
+ body: JSON.stringify({ mcp_server_url: mcpUrl })
555
+ });
556
+
557
+ const data = await response.json();
558
+
559
+ if (data.status === 'success') {
560
+ let content = '';
561
+ if (Array.isArray(data.tools)) {
562
+ // Format array of tool objects
563
+ content = data.tools.map(tool => {
564
+ const name = tool.name || tool.inputSchema?.title || 'Unknown';
565
+ const desc = tool.description || 'No description';
566
+ return `<div class="mb-2">
567
+ <span class="font-bold text-indigo-700">${name}</span>
568
+ <p class="text-gray-600 ml-2">${desc}</p>
569
+ </div>`;
570
+ }).join('');
571
+ } else if (typeof data.tools === 'object') {
572
+ content = `<pre class="whitespace-pre-wrap font-mono bg-gray-50 p-2 rounded border">${JSON.stringify(data.tools, null, 2)}</pre>`;
573
+ } else {
574
+ content = `<pre class="whitespace-pre-wrap font-mono bg-gray-50 p-2 rounded border">${data.tools}</pre>`;
575
+ }
576
+
577
+ toolsListContent.innerHTML = content;
578
+ toolsList.classList.remove('hidden');
579
+ localStorage.setItem('mcpServerUrl', mcpUrl);
580
+ } else {
581
+ alert('Failed to connect: ' + data.message);
582
+ }
583
+ } catch (e) {
584
+ alert('Error connecting to MCP server: ' + e.message);
585
+ } finally {
586
+ btn.disabled = false;
587
+ btn.textContent = 'Connect';
588
+ }
589
+ });
590
+
591
+ document.getElementById('triggerBtn').addEventListener('click', async (e) => {
592
+ e.preventDefault();
593
+
594
+ const prUrl = document.getElementById('prUrl').value;
595
+ const apiKey = document.getElementById('apiKey').value;
596
+ const mcpUrl = document.getElementById('mcpUrl').value;
597
+ const parsed = parsePrUrl(prUrl);
598
+
599
+ if (!parsed) {
600
+ alert('Invalid GitHub PR URL. Expected format: https://github.com/owner/repo/pull/123');
601
+ return;
602
+ }
603
+
604
+ const { repoUrl, prNumber } = parsed;
605
+
606
+ // Save to localStorage
607
+ localStorage.setItem('lastPrUrl', prUrl);
608
+ if (apiKey) {
609
+ localStorage.setItem('openaiApiKey', apiKey);
610
+ }
611
+ if (mcpUrl) {
612
+ localStorage.setItem('mcpServerUrl', mcpUrl);
613
+ }
614
+
615
+ // Refresh runs list just in case
616
+ await loadRuns(repoUrl, prNumber);
617
+
618
+ try {
619
+ const btn = document.getElementById('triggerBtn');
620
+ const originalText = btn.textContent;
621
+ btn.disabled = true;
622
+ btn.textContent = 'Starting...';
623
+
624
+ const response = await fetch('/review', {
625
+ method: 'POST',
626
+ headers: {
627
+ 'Content-Type': 'application/json',
628
+ },
629
+ body: JSON.stringify({
630
+ repo_url: repoUrl,
631
+ pr_number: parseInt(prNumber),
632
+ openai_api_key: apiKey || null,
633
+ mcp_server_url: mcpUrl || null
634
+ }),
635
+ });
636
+
637
+ const data = await response.json();
638
+
639
+ // Refresh runs list to show the new one
640
+ await loadRuns(repoUrl, prNumber);
641
+
642
+ // Automatically select the new run
643
+ loadRun(data.time_hash);
644
+
645
+ btn.disabled = false;
646
+ btn.textContent = originalText;
647
+
648
+ } catch (error) {
649
+ console.error('Error:', error);
650
+ alert('Failed to trigger review');
651
+ document.getElementById('triggerBtn').disabled = false;
652
+ document.getElementById('triggerBtn').textContent = 'Trigger Review';
653
+ }
654
+ });
655
+
656
+ // Helper to fetch all runs
657
+ async function fetchAllRuns() {
658
+ try {
659
+ const response = await fetch('/runs');
660
+ const data = await response.json();
661
+ renderRuns(data.runs);
662
+ } catch (error) {
663
+ console.error('Failed to load all runs:', error);
664
+ }
665
+ }
666
+
667
+ document.getElementById('fetchHistoryBtn').addEventListener('click', fetchAllRuns);
668
+
669
+ // Load saved values on startup
670
+ window.addEventListener('DOMContentLoaded', () => {
671
+ const savedPrUrl = localStorage.getItem('lastPrUrl');
672
+ const savedApiKey = localStorage.getItem('openaiApiKey');
673
+ const savedMcpUrl = localStorage.getItem('mcpServerUrl');
674
+
675
+ const prUrlInput = document.getElementById('prUrl');
676
+ const apiKeyInput = document.getElementById('apiKey');
677
+ const mcpUrlInput = document.getElementById('mcpUrl');
678
+
679
+ if (savedPrUrl) {
680
+ prUrlInput.value = savedPrUrl;
681
+ }
682
+ if (savedApiKey) {
683
+ apiKeyInput.value = savedApiKey;
684
+ }
685
+ if (savedMcpUrl) {
686
+ mcpUrlInput.value = savedMcpUrl;
687
+ }
688
+
689
+ // Always fetch all runs on startup
690
+ fetchAllRuns();
691
+ });
692
+
693
+ let currentRunHash = '';
694
+
695
+ function refreshCurrentRun() {
696
+ if (currentRunHash) {
697
+ loadRun(currentRunHash);
698
+ }
699
+ }
700
+
701
+ // ... (rest of script) ...
702
+ function log(msg) {
703
+ const debugLog = document.getElementById('debugLog');
704
+ const debugContent = document.getElementById('debugContent');
705
+ debugLog.classList.remove('hidden');
706
+ const div = document.createElement('div');
707
+ div.textContent = `${new Date().toISOString().split('T')[1]} - ${msg}`;
708
+ debugContent.appendChild(div);
709
+ debugContent.scrollTop = debugContent.scrollHeight;
710
+ }
711
+ </script>
712
+ </body>
713
+
714
+ </html>