fix(ui): smart display for long convos, action buttons for isBlocking, no middle gap

This commit is contained in:
2026-03-08 01:24:14 +09:00
parent df46b91fc8
commit 8568985e7a
3 changed files with 147 additions and 37 deletions

View File

@@ -149,57 +149,104 @@
async function refreshTrajectory(sessionId, scrollToBottom = true) {
try {
// 1) trajectory (앞부분 ~336개)
// 2) cascades (최신 상태: latestNotifyUserStep + latestTaskBoundaryStep)
const [trajRes, cascRes] = await Promise.all([
fetch(`/api/bridge/trajectory/${sessionId}`),
fetch('/api/bridge/cascades'),
]);
let messages = [];
let isComplete = true; // trajectory가 전체를 포함하는지
// trajectory 파싱
if (trajRes.ok) {
const trajData = await trajRes.json();
if (trajData.trajectory?.steps) {
messages = parseTrajectoryToMessages(trajData.trajectory.steps);
// trajectory가 전체가 아닌 경우 (numTotalSteps > steps.length)
const total = trajData.numTotalSteps || 0;
const got = trajData.trajectory.steps.length;
if (total > got) {
messages.push({
type: 'status',
text: `${total - got}개 스텝 생략 ⋯`,
});
isComplete = (total <= got);
if (isComplete) {
// ≤336 스텝: 전체 표시 가능
messages = parseTrajectoryToMessages(trajData.trajectory.steps);
}
// > 336 스텝: trajectory 건너뛰고 cascades 중심으로 표시
}
}
// cascades에서 최신 상태 보강
// cascades에서 최신 상태
if (cascRes.ok) {
const cascData = await cascRes.json();
const cascade = (cascData.cascades || cascData)[sessionId];
if (cascade) {
// latestTaskBoundaryStep → 현재 작업 상태
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
// 긴 대화일 때: cascades 기반 표시
if (!isComplete) {
// 대화 요약
if (cascade.summary) {
messages.push({
type: 'status',
text: `💬 대화 요약: ${cascade.summary}`,
});
}
messages.push({
type: 'task',
title: tb.taskName || '',
summary: tb.taskSummary || '',
status: tb.taskStatus || '',
mode: tb.mode || '',
tools: [],
type: 'status',
text: `📊 총 ${cascade.stepCount || '?'}개 스텝 · 최종 입력: ${formatRelativeTime(cascade.lastModifiedTime)}`,
});
}
// latestNotifyUserStep → 마지막 AI 응답
if (cascade.latestNotifyUserStep?.step?.notifyUser?.notificationContent) {
const content = cascade.latestNotifyUserStep.step.notifyUser.notificationContent;
messages.push({
type: 'text',
html: simpleMarkdown(content),
});
// 현재 Task 상태 (항상 최신)
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
// 이미 trajectory에서 같은 task가 없는 경우만 추가
const lastTask = messages.filter(m => m.type === 'task').pop();
if (!lastTask || lastTask.title !== tb.taskName) {
messages.push({
type: 'task',
title: tb.taskName || '',
summary: tb.taskSummary || '',
status: tb.taskStatus || '',
mode: tb.mode || '',
tools: [],
});
}
}
// 최신 AI 응답 (항상 표시)
if (cascade.latestNotifyUserStep?.step?.notifyUser) {
const nu = cascade.latestNotifyUserStep.step.notifyUser;
const content = nu.notificationContent || '';
if (content) {
// 이미 trajectory에서 같은 내용이 없는 경우만 추가
const lastText = messages.filter(m => m.type === 'text').pop();
const contentSnip = content.substring(0, 50);
if (!lastText || !lastText.html?.includes(contentSnip.replace(/[<>&]/g, ''))) {
messages.push({
type: 'text',
html: simpleMarkdown(content),
});
}
}
// 🔴 사용자 행동 필요 (isBlocking)
if (nu.isBlocking) {
messages.push({
type: 'actions',
label: '⚠️ Antigravity에서 리뷰가 필요합니다',
buttons: [
{ label: '미러 탭에서 확인', action: 'switch_mirror' },
],
});
}
}
// 실행 중 상태 표시
if (cascade.status === 'CASCADE_RUN_STATUS_RUNNING') {
const lastMsg = messages[messages.length - 1];
if (!lastMsg || lastMsg.type !== 'actions') {
messages.push({
type: 'status',
text: '🔄 AI가 작업 중...',
});
}
}
}
}
@@ -214,6 +261,17 @@
}
}
function formatRelativeTime(isoStr) {
if (!isoStr) return '';
const diff = Date.now() - new Date(isoStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return '방금 전';
if (mins < 60) return `${mins}분 전`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}시간 전`;
return `${Math.floor(hrs / 24)}일 전`;
}
// 실시간 갱신 디바운스
let refreshTimer = null;
function scheduleRefresh() {