mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix(ai-sessions): keep the last-chance checkpoint and the live record fields
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bed7d9a478
commit
e8816ac9f9
@@ -26,7 +26,12 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import { pendingUserAction, pendingUserActionDetail, type DisplayMessage } from './shared'
|
||||
import {
|
||||
pendingUserAction,
|
||||
pendingUserActionDetail,
|
||||
RUN_PROMPT_ECHO_MAX,
|
||||
type DisplayMessage
|
||||
} from './shared'
|
||||
import { PLAN_MODE_TEXT_COLOR, PLAN_MODE_TRIGGER_CLASS } from './planMode'
|
||||
import { PLAN_MODE_MESSAGES } from './planModeMessages'
|
||||
import type { ContextElement } from './context'
|
||||
@@ -554,15 +559,21 @@
|
||||
//
|
||||
// Suppressed once the transcript already carries the message: a tab that
|
||||
// mounted after the driver's opening save reads it from the store, and the
|
||||
// echo beside it would be the same text drawn twice. The echo is a prefix of
|
||||
// what the driver holds (it is truncated at the source), which is what makes
|
||||
// `startsWith` the right test.
|
||||
// echo beside it would be the same text drawn twice.
|
||||
//
|
||||
// Compared whole, and by prefix only where the echo was cut at the source's
|
||||
// ceiling and a prefix is all there is to compare. Matching on prefix
|
||||
// unconditionally would hide the echo whenever a new prompt opens with the
|
||||
// previous turn's text ("Fix" after "Fix the bug") — which is the case the
|
||||
// sender pins by position precisely to keep.
|
||||
const remoteUserEcho = $derived.by(() => {
|
||||
const echo = aiChatManager.remoteUserMessage
|
||||
if (!echo || !aiChatManager.runHeldElsewhere) return undefined
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role !== 'user') continue
|
||||
return messages[i].content.trim().startsWith(echo) ? undefined : echo
|
||||
const held = messages[i].content.trim()
|
||||
const already = echo.length >= RUN_PROMPT_ECHO_MAX ? held.startsWith(echo) : held === echo
|
||||
return already ? undefined : echo
|
||||
}
|
||||
return echo
|
||||
})
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
backgroundJobCompletionNote,
|
||||
deriveChatJobStatus,
|
||||
pendingToolImagesMessage,
|
||||
pendingUserAction,
|
||||
trimJob
|
||||
} from './shared'
|
||||
import type {
|
||||
@@ -3274,14 +3273,6 @@ export class AIChatManager {
|
||||
// the poll exactly when nobody is watching. `pending` reads it without
|
||||
// disturbing the animation.
|
||||
const streaming = this.currentReply + this.replyReveal.pending
|
||||
// Parked on the user, so nothing is advancing to preserve — and a
|
||||
// snapshot taken here would store the question closed as interrupted,
|
||||
// which is what it means only if this tab died. Another tab that opens
|
||||
// the session meanwhile reads that as a failed call while the run is in
|
||||
// fact waiting. The checkpoint before this one still holds everything
|
||||
// the turn did up to the question, and the question itself does not
|
||||
// survive a reload either way: its resolver goes with the page.
|
||||
if (pendingUserAction(this.displayMessages)) return
|
||||
// Write only when the turn advanced, so a parked confirmation costs
|
||||
// nothing and the rate follows steps taken rather than time.
|
||||
const shape = `${collectedMessages.length}:${this.displayMessages.length}:${streaming.length}`
|
||||
|
||||
@@ -659,6 +659,13 @@ export type DisplayMessage =
|
||||
// is paused on the user. Drives the question card's interactivity, the
|
||||
// "waiting for user" indicator, and routing a composer send to the answer —
|
||||
// keep those in sync by going through this single predicate.
|
||||
/** Longest prompt a driving tab echoes to the tabs watching its run, and the
|
||||
* ceiling that keeps the run-status message bounded — it is the one field whose
|
||||
* length a user sets. Lives here rather than beside the sender so the receiver
|
||||
* can tell a truncated echo from a whole one without importing from `sessions`,
|
||||
* which only imports in the other direction. */
|
||||
export const RUN_PROMPT_ECHO_MAX = 2000
|
||||
|
||||
export function isActiveUserQuestion(message: DisplayMessage | undefined): boolean {
|
||||
return Boolean(
|
||||
message &&
|
||||
|
||||
@@ -91,7 +91,11 @@ import type {
|
||||
} from '$lib/components/raw_apps/rawAppDom'
|
||||
import { getNonStreamingMetadataCompletion } from '$lib/components/copilot/lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { pendingUserAction, type DisplayMessage } from '$lib/components/copilot/chat/shared'
|
||||
import {
|
||||
pendingUserAction,
|
||||
RUN_PROMPT_ECHO_MAX,
|
||||
type DisplayMessage
|
||||
} from '$lib/components/copilot/chat/shared'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
|
||||
import {
|
||||
broadcastRunStatus,
|
||||
@@ -1011,11 +1015,6 @@ async function initRuntime(runtime: SessionRuntime, session: Session) {
|
||||
// ticks and is not reaped as a closed tab.
|
||||
const statusTimers = new Map<string, ReturnType<typeof setInterval>>()
|
||||
|
||||
/** Longest prompt echoed to the other tabs. Enough for the prompts people
|
||||
* actually type, and a hard ceiling on the one field of the status message
|
||||
* whose length a user controls. */
|
||||
const RUN_PROMPT_ECHO_MAX = 2000
|
||||
|
||||
/** The prompt this run is working on: the last thing the user said, searching no
|
||||
* further back than `from`. Read off the driver's rendered transcript rather
|
||||
* than the request, so it is the same text the driving tab has on screen. */
|
||||
|
||||
@@ -511,6 +511,36 @@ let remoteReadSeq = 0
|
||||
// would roll the record back on the next write. Reading instead always lands on
|
||||
// what the shared store actually holds. In-memory only — re-persisting here
|
||||
// would echo straight back out through the row funnel.
|
||||
/** Take on what another tab stored, into the record object this tab already
|
||||
* holds rather than in place of it.
|
||||
*
|
||||
* Identity matters: the debounced writers capture the object they were called
|
||||
* on (`setSessionDraftPrompt` flushes 400ms later through `persistTouched(s)`),
|
||||
* so swapping the array slot leaves that flush writing a record this list no
|
||||
* longer contains — putting back the state it held before the other tab wrote.
|
||||
*
|
||||
* Preview tabs keep their live overrides. `friendlyLabel` / `friendlyPath` /
|
||||
* `editorNamed` are stamped by the open editor and deliberately not persisted,
|
||||
* so they are absent from every stored row; taking the row's tabs wholesale
|
||||
* would drop a watching tab's breadcrumbs back to raw paths on any write from
|
||||
* the other tab, a seen-watermark bump included. */
|
||||
function adoptRemoteRow(held: Session, row: Session): void {
|
||||
const stamped = new Map((held.previewTabs ?? []).map((t) => [t.id, t]))
|
||||
Object.assign(held, row)
|
||||
if (row.previewTabs) {
|
||||
held.previewTabs = row.previewTabs.map((t) => {
|
||||
const live = stamped.get(t.id)
|
||||
if (!live) return t
|
||||
return {
|
||||
...t,
|
||||
friendlyLabel: live.friendlyLabel,
|
||||
friendlyPath: live.friendlyPath,
|
||||
editorNamed: live.editorNamed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function applyRemoteSessionPut(id: string): Promise<void> {
|
||||
if (deletedSessionIds.has(id)) return
|
||||
const token = ++remoteReadSeq
|
||||
@@ -531,7 +561,7 @@ async function applyRemoteSessionPut(id: string): Promise<void> {
|
||||
if (!row || deletedSessionIds.has(id)) return
|
||||
const i = sessionState.sessions.findIndex((s) => s.id === id)
|
||||
if (i >= 0) {
|
||||
sessionState.sessions[i] = row
|
||||
adoptRemoteRow(sessionState.sessions[i], row)
|
||||
return
|
||||
}
|
||||
// New elsewhere: slot it in by createdAt, after any local transient drafts,
|
||||
|
||||
Reference in New Issue
Block a user