fix: what the batch C review found

- A redirect followed for a custom base URL (quota on) no longer carries
  the user's key or cookies to another origin, as fetch itself does, and
  a private address may redirect to another private one (already counted).
- The admin Test of an Ollama or Vertex AI entry without a URL goes where
  chat sends that entry's key: the environment's own URL variable, for
  Ollama else the local default. The Test of an Ollama Cloud key without
  a URL went to the cloud while chat went to local Ollama.
- Chat saves: each save notes the chat on screen and the order of the
  reads before reading its data. A save read before switching chats no
  longer writes into the chat switched to, and a copy that waited for its
  thumbnail no longer replaces a newer one.
- "Continue without saving" keeps its button when a later auto-save fails,
  and goes away when a new message is sent.
- A screenshot check that was waiting for its image when the user pressed
  Stop stays skipped after the next message.
- MCP History: draw.io's own copy of a diagram (after get_diagram) no
  longer adds an entry without a picture; a change of background is still
  its own version. The tab ignores an edit's answer that arrives after a
  newer AI write loaded.
- Desktop: a deleted preset is not brought back by a failed switch, and a
  request naming no preset does not stop a rollback. An origin keeping
  an access code counts as having settings.
- .env: a quoted value ending in a backslash ("C:\dir\") is read as dotenv
  reads it.
- The tool card no longer crashes on an id that does not turn into text;
  an older Test's success timer no longer ends a newer Test's spinner.
- Tests that passed without their fix now check it.
This commit is contained in:
dayuan.jiang
2026-10-05 20:09:42 +09:00
parent c0fa997186
commit 69c7896002
23 changed files with 461 additions and 63 deletions
+45 -10
View File
@@ -277,6 +277,9 @@ export default function ChatPanel({
// Set by Stop until the user sends the next message
const stoppedRef = useRef(false)
// Presses of Stop: a check that began before one still knows of it after
// the next message clears stoppedRef
const stopCountRef = useRef(0)
// Store original XML for display_diagram and edit_diagram streaming -
// shared between streaming preview and tool handler
@@ -347,7 +350,11 @@ export default function ChatPanel({
validateDiagram: validateWithFallback,
enableVlmValidation: vlmValidationEnabled,
sessionId,
isStopped: () => stoppedRef.current,
watchStop: () => {
const stopsBefore = stopCountRef.current
return () =>
stoppedRef.current || stopCountRef.current !== stopsBefore
},
onValidationStateChange: handleValidationStateChange,
})
@@ -672,6 +679,7 @@ export default function ChatPanel({
currentSessionId,
saveCurrentSession,
getChatGeneration,
getSaveTicket,
} = sessionManager
// Use ref for saveCurrentSession to avoid infinite loop
@@ -709,14 +717,17 @@ export default function ChatPanel({
localStorageDebounceRef.current = setTimeout(async () => {
try {
if (messages.length > 0 || hasDiagramNow) {
// Taken before the data is read, for the chat it was
// scheduled for
const ticket = {
...getSaveTicket(),
generation: scheduledForChat,
}
const sessionData = await buildSessionData({
// Only capture thumbnail if there was a diagram AND this isn't a no-diagram session
withThumbnail: hasDiagramNow && !isNodiagramSession,
})
await saveCurrentSessionRef.current(
sessionData,
scheduledForChat,
)
await saveCurrentSessionRef.current(sessionData, ticket)
}
} catch (error) {
console.error("Failed to save session:", error)
@@ -736,6 +747,7 @@ export default function ChatPanel({
sessionIsAvailable,
currentSessionId,
getChatGeneration,
getSaveTicket,
buildSessionData,
])
@@ -768,10 +780,11 @@ export default function ChatPanel({
try {
// Attempt to save session - browser may not wait for completion
// Skip thumbnail capture as it may not complete in time
const ticket = sessionManager.getSaveTicket()
const sessionData = await buildSessionData({
withThumbnail: false,
})
await sessionManager.saveCurrentSession(sessionData)
await sessionManager.saveCurrentSession(sessionData, ticket)
} catch (error) {
console.error(
"Failed to save session on visibility change:",
@@ -922,11 +935,13 @@ export default function ChatPanel({
// The current chat could not be saved (storage full). The list where
// old chats can be deleted shows only in an empty chat, so let the user
// go on without saving (same toast id: it replaces the plain message)
// go on without saving. It replaces the plain message, and has its own
// id so a later failed auto-save does not take its button away.
const offerToContinueUnsaved = useCallback(
(proceed: () => void) => {
toast.dismiss("session-save-failed")
toast.error(dict.errors.sessionSaveFailedLeave, {
id: "session-save-failed",
id: "session-save-leave",
duration: 15000,
action: {
label: dict.errors.continueWithoutSaving,
@@ -937,6 +952,14 @@ export default function ChatPanel({
[dict],
)
// A new turn makes the offer stale: going on would clear the chat while
// the answer streams in
useEffect(() => {
if (status === "submitted" || status === "streaming") {
toast.dismiss("session-save-leave")
}
}, [status])
// Handle session switching from history dropdown
const handleSelectSession = useCallback(
async (sessionId: string) => {
@@ -969,10 +992,18 @@ export default function ChatPanel({
// without messages); if that failed (storage full), stay on it
// unless the user goes on without saving it
if (messages.length > 0 || isRealDiagram(chartXMLRef.current)) {
// Of the chat on screen now, also if another one comes on
// screen while the thumbnail is taken
const ticket = sessionManager.getSaveTicket()
const sessionData = await buildSessionData({
withThumbnail: true,
})
if (!(await sessionManager.saveCurrentSession(sessionData))) {
if (
!(await sessionManager.saveCurrentSession(
sessionData,
ticket,
))
) {
offerToContinueUnsaved(open)
return
}
@@ -1049,10 +1080,13 @@ export default function ChatPanel({
sessionManager.isAvailable &&
(messages.length > 0 || isRealDiagram(chartXMLRef.current))
) {
const ticket = sessionManager.getSaveTicket()
const sessionData = await buildSessionData({ withThumbnail: true })
// Not saved (storage full): keep the chat on screen, unless the
// user goes on without saving it
if (!(await sessionManager.saveCurrentSession(sessionData))) {
if (
!(await sessionManager.saveCurrentSession(sessionData, ticket))
) {
offerToContinueUnsaved(startNewChat)
return
}
@@ -1128,6 +1162,7 @@ export default function ChatPanel({
// Handle stop button click
const handleStop = useCallback(() => {
stoppedRef.current = true
stopCountRef.current++
// A running screenshot check holds up the chat (the SDK waits for
// the tool handler): end it, so the call gets its result now
cancelValidation()
+1 -1
View File
@@ -30,7 +30,7 @@ function OperationsDisplay({ operations }: { operations: DiagramOperation[] }) {
<div className="space-y-3">
{shown.map((op, index) => (
<div
key={`${op.operation}-${op.cell_id}-${index}`}
key={`${op.operation}-${text(op.cell_id)}-${index}`}
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
>
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
+2 -1
View File
@@ -559,8 +559,9 @@ export function ModelConfigDialog({
clearTimeout(validationResetTimeoutRef.current)
}
validationResetTimeoutRef.current = setTimeout(() => {
setValidationStatus("idle")
validationResetTimeoutRef.current = null
if (run !== validationRunRef.current) return
setValidationStatus("idle")
}, 1500)
} else {
setValidationStatus("error")