Fix session/tool-question UX regressions and stabilize streaming activity rendering (#451)
* fix: stale model variants in chat controls in draft session * fix: attention indicator for project tabs * fix: use part index and type for consistent IDs * fix question card navigation for unanswered questions
This commit is contained in:
committed by
GitHub
parent
50a66bb6f1
commit
22255cf2c0
@@ -45,6 +45,7 @@ declare global {
|
||||
const ENABLE_EMPTY_RESPONSE_DETECTION = false;
|
||||
const TEXT_SHRINK_TOLERANCE = 50;
|
||||
const RESYNC_DEBOUNCE_MS = 750;
|
||||
const QUESTION_RECONCILE_COOLDOWN_MS = 1500;
|
||||
|
||||
const textLengthCache = new WeakMap<Part[], number>();
|
||||
const computeTextLength = (parts: Part[] | undefined | null): number => {
|
||||
@@ -153,6 +154,38 @@ export const useEventStream = () => {
|
||||
return undefined;
|
||||
}, [activeSessionDirectory, fallbackDirectory]);
|
||||
|
||||
const bootstrapPendingQuestions = React.useCallback(async () => {
|
||||
try {
|
||||
const projects = useProjectsStore.getState().projects;
|
||||
const projectDirs = projects.map((project) => project.path);
|
||||
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||
const currentSessions = useSessionStore.getState().sessions;
|
||||
const sessionDirs = currentSessions.map((session) => (session as { directory?: string | null }).directory);
|
||||
|
||||
const directories = [effectiveDirectory, ...projectDirs, ...sessionDirs];
|
||||
const pending = await opencodeClient.listPendingQuestions({ directories });
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const request of pending) {
|
||||
addQuestion(request as unknown as QuestionRequest);
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}, [addQuestion, effectiveDirectory]);
|
||||
|
||||
const lastQuestionRefreshAtRef = React.useRef(0);
|
||||
const requestPendingQuestionsRefresh = React.useCallback((force = false) => {
|
||||
const now = Date.now();
|
||||
if (!force && now - lastQuestionRefreshAtRef.current < QUESTION_RECONCILE_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
lastQuestionRefreshAtRef.current = now;
|
||||
void bootstrapPendingQuestions();
|
||||
}, [bootstrapPendingQuestions]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -171,36 +204,13 @@ export const useEventStream = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const bootstrapPendingQuestions = async () => {
|
||||
try {
|
||||
const projects = useProjectsStore.getState().projects;
|
||||
const projectDirs = projects.map((project) => project.path);
|
||||
// Use getState() to avoid sessions dependency which causes cascading updates
|
||||
const currentSessions = useSessionStore.getState().sessions;
|
||||
const sessionDirs = currentSessions.map((session) => (session as { directory?: string | null }).directory);
|
||||
|
||||
const directories = [effectiveDirectory, ...projectDirs, ...sessionDirs];
|
||||
|
||||
const pending = await opencodeClient.listPendingQuestions({ directories });
|
||||
if (cancelled || pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const request of pending) {
|
||||
addQuestion(request as unknown as QuestionRequest);
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
};
|
||||
|
||||
void bootstrapPendingPermissions();
|
||||
void bootstrapPendingQuestions();
|
||||
requestPendingQuestionsRefresh(true);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [addPermission, addQuestion, effectiveDirectory]);
|
||||
}, [addPermission, requestPendingQuestionsRefresh]);
|
||||
|
||||
const normalizeDirectory = React.useCallback((value: string | null | undefined): string | null => {
|
||||
if (typeof value !== 'string') return null;
|
||||
@@ -852,8 +862,15 @@ export const useEventStream = () => {
|
||||
const partTime = (messagePart as { time?: { end?: unknown } }).time;
|
||||
const partHasEnded = typeof partTime?.end === 'number';
|
||||
const toolState = (messagePart as { state?: { status?: unknown } }).state?.status;
|
||||
const toolName = typeof (messagePart as { tool?: unknown }).tool === 'string'
|
||||
? (messagePart as { tool: string }).tool.toLowerCase()
|
||||
: null;
|
||||
const textContent = (messagePart as { text?: unknown }).text;
|
||||
|
||||
if (partType === 'tool' && toolName === 'question') {
|
||||
requestPendingQuestionsRefresh();
|
||||
}
|
||||
|
||||
const isStreamingPart = (() => {
|
||||
if (partType === 'tool') {
|
||||
return toolState === 'running' || toolState === 'pending';
|
||||
@@ -1229,6 +1246,15 @@ export const useEventStream = () => {
|
||||
if (!hasParts && !completedFromServer && !hasCompletedStatus && !eventHasStopFinish) break;
|
||||
|
||||
if ((messageExt as { role?: unknown }).role === 'assistant' && hasParts) {
|
||||
const hasQuestionTool = partsArray.some((part) => (
|
||||
part?.type === 'tool'
|
||||
&& typeof (part as { tool?: unknown }).tool === 'string'
|
||||
&& (part as { tool: string }).tool.toLowerCase() === 'question'
|
||||
));
|
||||
if (hasQuestionTool) {
|
||||
requestPendingQuestionsRefresh();
|
||||
}
|
||||
|
||||
const incomingLen = computeTextLength(partsArray);
|
||||
const wouldShrink = existingLen > 0 && incomingLen + TEXT_SHRINK_TOLERANCE < existingLen;
|
||||
|
||||
@@ -1662,6 +1688,7 @@ export const useEventStream = () => {
|
||||
applySessionMetadata,
|
||||
trackMessage,
|
||||
reportMessage,
|
||||
requestPendingQuestionsRefresh,
|
||||
|
||||
updateSession,
|
||||
removeSessionFromStore,
|
||||
|
||||
Reference in New Issue
Block a user