Skip to content

Commit 47c41e2

Browse files
authored
Merge pull request #136 from HexmosTech/v1-auto-fixing
Added SendToAgentButton (split button + dropdown) supporting Claude, Codex, and Gemini CLI,
2 parents 4453a65 + efd63da commit 47c41e2

10 files changed

Lines changed: 1046 additions & 32 deletions

File tree

internal/staticserve/static/app.js

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Fetches data from /api/review and updates reactively
33

44
import { waitForPreact, filePathToId, transformEvent, getBadgeClass, formatIssueForCopy, getCommentVisibilityKey } from './components/utils.js';
5-
import { buildIssueCategoryGroups, buildIssueFacetOptions, buildIssueFilterUniverse, countIssuesByFilters, createDefaultIssueFilters, getIssueFilterSummary, matchesIssueFilters, resetIssueFilters, toggleIssueFilterValue } from './components/issue_filter_state.mjs';
5+
import { buildIssueCategoryGroups, buildIssueFacetOptions, buildIssueFilterUniverse, countIssuesByFilters, createDefaultIssueFilters, DEFAULT_SEVERITIES, getCommentFilterValue, getIssueFilterSummary, matchesIssueFilters, resetIssueFilters, toggleIssueFilterValue } from './components/issue_filter_state.mjs';
66
import { appendStreamedCommentsToFiles, buildEventsURL, extractExternalCommentsFromEvents, extractNewEvents, inferReviewStatusFromEvents } from './components/review_stream_state.mjs';
77
import { getHeader } from './components/Header.js';
88
import { getSidebar } from './components/Sidebar.js';
@@ -12,6 +12,8 @@ import { getPrecommitBar } from './components/PrecommitBar.js';
1212
import { getFileBlock } from './components/FileBlock.js';
1313
import { getEventLog } from './components/EventLog.js';
1414
import { getIssueFilterBar } from './components/IssueFilterBar.js';
15+
import { getSendToAgentInfo } from './components/SendToAgentButton.js';
16+
import { renderHandoffConfetti } from './components/handoffConfetti.js';
1517
import { getToolbar } from './components/Toolbar.js';
1618
import { getCommentNav } from './components/CommentNav.js';
1719
import { UsageBanner } from './components/UsageBanner.js';
@@ -36,6 +38,30 @@ function countCommentsFromFiles(files) {
3638
}, 0);
3739
}
3840

41+
// Build a severity + type breakdown for the handoff modal so the user can see the
42+
// scope of what's being auto-fixed (e.g. "6 issues — 2 Critical, 3 Warning, 1 Info").
43+
function buildHandoffImpactSummary(files) {
44+
const bySeverity = Object.fromEntries(DEFAULT_SEVERITIES.map((severity) => [severity, 0]));
45+
const typeCounts = new Map();
46+
let total = 0;
47+
(files || []).forEach((file) => {
48+
(file.comments || file.Comments || []).forEach((comment) => {
49+
total += 1;
50+
const severity = getCommentFilterValue(comment, 'severity');
51+
bySeverity[severity] = (bySeverity[severity] || 0) + 1;
52+
53+
const type = getCommentFilterValue(comment, 'type');
54+
if (type) {
55+
typeCounts.set(type, (typeCounts.get(type) || 0) + 1);
56+
}
57+
});
58+
});
59+
const byType = [...typeCounts.entries()]
60+
.map(([label, count]) => ({ label, count }))
61+
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
62+
return { total, bySeverity, byType };
63+
}
64+
3965
function convertFilesToUIFormat(files) {
4066
if (!files) return [];
4167

@@ -267,7 +293,7 @@ async function initApp() {
267293
const [isTailing, setIsTailing] = useState(false);
268294
const [hiddenCommentKeys, setHiddenCommentKeys] = useState(new Set());
269295
const [copyFeedback, setCopyFeedback] = useState({ status: 'idle', message: '' });
270-
const [handoffModal, setHandoffModal] = useState({ isOpen: false, type: '', message: '' });
296+
const [handoffModal, setHandoffModal] = useState({ isOpen: false, type: '', message: '', agent: null, summary: null });
271297
const [slideShowOpen, setSlideShowOpen] = useState(false);
272298
const [embeddedSlideshowActive, setEmbeddedSlideshowActive] = useState(false);
273299
const [summarySlideIndex, setSummarySlideIndex] = useState(0);
@@ -679,7 +705,19 @@ async function initApp() {
679705
});
680706
}, []);
681707

682-
const handleSendToAgent = useCallback(async () => {
708+
const handleSendToAgent = useCallback(async (agentKey) => {
709+
const agent = getSendToAgentInfo(agentKey);
710+
711+
if (!agent.available) {
712+
setHandoffModal({
713+
isOpen: true,
714+
type: 'info',
715+
agent,
716+
message: `${agent.label} handoff is coming soon. Try Claude for now.`
717+
});
718+
return;
719+
}
720+
683721
const filteredFiles = (reviewData.files || reviewData.Files || []).map(file => {
684722
const filePath = file.file_path || file.filePath || file.FilePath;
685723
const newComments = (file.comments || file.Comments || []).filter(c => {
@@ -689,24 +727,36 @@ async function initApp() {
689727
});
690728
return { ...file, comments: newComments, Comments: newComments };
691729
}).filter(file => file.comments.length > 0);
692-
730+
693731
if (filteredFiles.length === 0) {
694-
setHandoffModal({
695-
isOpen: true,
696-
type: 'error',
697-
message: "No visible comments to send to the AI agent. Please show some comments first."
732+
setHandoffModal({
733+
isOpen: true,
734+
type: 'error',
735+
agent,
736+
message: "No visible comments to send to the AI agent. Please show some comments first."
698737
});
699738
return;
700739
}
701-
740+
741+
const impactSummary = buildHandoffImpactSummary(filteredFiles);
742+
743+
setHandoffModal({
744+
isOpen: true,
745+
type: 'starting',
746+
agent,
747+
summary: impactSummary,
748+
message: `${agent.label} started auto-fixing the issues…`
749+
});
750+
702751
const payload = {
703752
...reviewData,
704753
files: filteredFiles,
705754
Files: filteredFiles,
706755
summary: "AI Agent Handoff generated for visible issues.",
707-
status: "completed"
756+
status: "completed",
757+
agent: agent.key
708758
};
709-
759+
710760
try {
711761
const handoffURL = sessionReviewID ? `/handoff?r=${sessionReviewID}` : '/handoff';
712762
const response = await fetch(handoffURL, {
@@ -715,16 +765,19 @@ async function initApp() {
715765
body: JSON.stringify(payload)
716766
});
717767
if (!response.ok) throw new Error("Handoff failed");
718-
setHandoffModal({
719-
isOpen: true,
720-
type: 'success',
721-
message: "Claude Code is now starting in your terminal! You can safely close this browser window."
768+
setHandoffModal({
769+
isOpen: true,
770+
type: 'success',
771+
agent,
772+
summary: impactSummary,
773+
message: `Auto-fixing started for your issues — check the ${agent.label} Code terminal to follow along. You can safely close this browser window.`
722774
});
723775
} catch (e) {
724-
setHandoffModal({
725-
isOpen: true,
726-
type: 'error',
727-
message: "Failed to send to agent: " + e.message
776+
setHandoffModal({
777+
isOpen: true,
778+
type: 'error',
779+
agent,
780+
message: "Failed to send to agent: " + e.message
728781
});
729782
}
730783
}, [reviewData, issueFilters, hiddenCommentKeys]);
@@ -1131,25 +1184,71 @@ async function initApp() {
11311184
}
11321185
</div>
11331186
1187+
${handoffModal.isOpen && handoffModal.type === 'success' && renderHandoffConfetti(html)}
11341188
${handoffModal.isOpen && html`
1135-
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(4px);">
1136-
<div class="modal-content" style="background: var(--bg-card); padding: 32px; border-radius: 12px; max-width: 400px; width: 90%; border: 1px solid var(--border-color); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); text-align: center;">
1137-
${handoffModal.type === 'success'
1189+
<div class="modal-overlay handoff-modal-overlay">
1190+
<div class="modal-content handoff-modal-content">
1191+
${(handoffModal.type === 'starting' || handoffModal.type === 'success')
11381192
? html`
1139-
<div style="margin-bottom: 16px; color: #8b5cf6;">
1140-
${renderIcon(html, 'handoffSuccess', { size: 48 })}
1193+
<div class="handoff-agent-badge ${handoffModal.type === 'success' ? 'is-success' : ''}">
1194+
${handoffModal.type === 'starting' && html`<span class="handoff-agent-badge-ring"></span>`}
1195+
${(handoffModal.agent?.logoOnLight || handoffModal.agent?.logo)
1196+
? html`<img class="handoff-agent-badge-logo" src=${handoffModal.agent.logoOnLight || handoffModal.agent.logo} alt="${handoffModal.agent.label}" />`
1197+
: renderIcon(html, handoffModal.agent?.icon || 'sendToAgent', { size: 26 })}
1198+
${handoffModal.type === 'success' && html`
1199+
<span class="handoff-agent-badge-check">${renderIcon(html, 'check', { size: 12 })}</span>
1200+
`}
11411201
</div>
11421202
`
11431203
: html`<div style="margin-bottom: 16px;">${renderIcon(html, 'handoffNotice', { size: 48 })}</div>`
11441204
}
11451205
<h3 style="margin: 0 0 12px 0; font-size: 20px; color: var(--text-primary);">
1146-
${handoffModal.type === 'success' ? 'Check Your Terminal' : 'Notice'}
1206+
${handoffModal.type === 'starting'
1207+
? `${handoffModal.agent?.label || 'Agent'} is on it`
1208+
: handoffModal.type === 'success'
1209+
? `Auto-fixing Using ${handoffModal.agent?.label || 'Agent'} Code`
1210+
: handoffModal.type === 'info'
1211+
? 'Coming Soon'
1212+
: 'Notice'}
11471213
</h3>
1148-
<p style="margin: 0 0 24px 0; color: var(--text-secondary); line-height: 1.5;">
1214+
<p style="margin: 0 0 16px 0; color: var(--text-secondary); line-height: 1.5;">
11491215
${handoffModal.message}
11501216
</p>
1151-
<button
1152-
class="btn btn-primary"
1217+
${handoffModal.summary && handoffModal.summary.total > 0 && html`
1218+
<div class="handoff-impact-summary">
1219+
<div class="handoff-impact-total">
1220+
${handoffModal.summary.total} issue${handoffModal.summary.total === 1 ? '' : 's'} being fixed
1221+
</div>
1222+
<div class="handoff-impact-section">
1223+
<div class="handoff-impact-label">Severity</div>
1224+
<div class="handoff-impact-chips">
1225+
${['critical', 'warning', 'info']
1226+
.filter((severity) => handoffModal.summary.bySeverity[severity] > 0)
1227+
.map((severity) => html`
1228+
<span class="handoff-impact-chip severity-${severity}">
1229+
<span class="handoff-impact-chip-dot"></span>
1230+
${handoffModal.summary.bySeverity[severity]} ${severity.charAt(0).toUpperCase() + severity.slice(1)}
1231+
</span>
1232+
`)}
1233+
</div>
1234+
</div>
1235+
${handoffModal.summary.byType && handoffModal.summary.byType.length > 0 && html`
1236+
<div class="handoff-impact-section handoff-impact-section-divided">
1237+
<div class="handoff-impact-label">Type</div>
1238+
<div class="handoff-impact-types">
1239+
${handoffModal.summary.byType.map((entry) => html`
1240+
<span class="handoff-impact-type-chip">
1241+
${entry.label}
1242+
<span class="handoff-impact-type-count">${entry.count}</span>
1243+
</span>
1244+
`)}
1245+
</div>
1246+
</div>
1247+
`}
1248+
</div>
1249+
`}
1250+
<button
1251+
class="btn btn-primary"
11531252
onClick=${() => setHandoffModal({ ...handoffModal, isOpen: false })}
11541253
style="width: 100%; padding: 12px; font-size: 16px;"
11551254
>
Lines changed: 46 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)