mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 16:02:47 +00:00
fix: hold every turn during a move, and state the repoint's contract
- The chat manager refuses sends, retries and message edits while a move runs, from its first step, handing the text back. The composer was locked but a retry or an edit could still start a turn in the parent. - A move into an existing fork checks again for a running turn after saving that fork's pending edits. - repoint_unresolvable_draft_identities documents that it authorizes nothing and when its unscoped form may be used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d84a8f4551
commit
d7b38031af
@@ -8583,6 +8583,11 @@ fn principal_resolves_sql(principal: &str) -> String {
|
||||
/// The draft half of `repoint_unresolvable_cloned_identities`: an app policy or trigger in a draft
|
||||
/// copied into `target_workspace_id` that names nobody there is re-pointed at `authed`. `items`
|
||||
/// narrows it to `authed`'s own rows at those keys, for drafts copied into an existing workspace.
|
||||
///
|
||||
/// **Authorizes nothing itself.** The caller must have checked that `authed` (loaded in
|
||||
/// `target_workspace_id`) may write those drafts there. `items = None` rewrites every draft in the
|
||||
/// workspace, other users' included, so it is only for a fork being created inside the same
|
||||
/// transaction, before anyone else has drafts in it.
|
||||
pub async fn repoint_unresolvable_draft_identities(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
target_workspace_id: &str,
|
||||
|
||||
@@ -788,6 +788,9 @@ export class AIChatManager implements ChatViewHost {
|
||||
/** The session is moving into a fork. Its editors stay closed meanwhile: an edit would save a
|
||||
* draft in the workspace being left, after the move listed what it brings along. */
|
||||
moving = $state(false)
|
||||
/** A move into a fork has started: no turn starts until it ends, retries and edits included.
|
||||
* Set before `moving`, which waits for the editors to hand over their pending edits. */
|
||||
moveStarted = $state(false)
|
||||
onWorkModeChanged: ((mode: AIWorkMode) => void) | undefined = undefined
|
||||
workModesAvailable = $derived(this.isSessionChat && supportsPlanMode(this.mode))
|
||||
/** Use is the only mode: the workspace is protected, or the user's role builds nothing in it. */
|
||||
@@ -3646,7 +3649,9 @@ export class AIChatManager implements ChatViewHost {
|
||||
// false`), and a copy here would double it. Paste tokens are expanded
|
||||
// into the text, as the queue does, because the restore lanes carry no
|
||||
// pastes.
|
||||
if (this.runHeldElsewhere) {
|
||||
// A move into a fork holds turns the same way: one started now would act on the
|
||||
// workspace being left. The composer is locked then, but a retry or an edit is not.
|
||||
if (this.runHeldElsewhere || this.moveStarted) {
|
||||
if (options.synthetic) {
|
||||
// Client-authored prompt (a job auto-resume), not user input: nothing
|
||||
// to hand back and no toast. Releasing the staged text un-blocks the
|
||||
@@ -3664,7 +3669,12 @@ export class AIChatManager implements ChatViewHost {
|
||||
options.files
|
||||
)
|
||||
}
|
||||
sendUserToast('This session is running in another tab. Your message was kept.', true)
|
||||
sendUserToast(
|
||||
this.moveStarted
|
||||
? 'This session is moving to a fork. Your message was kept.'
|
||||
: 'This session is running in another tab. Your message was kept.',
|
||||
true
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -4833,7 +4843,7 @@ export class AIChatManager implements ChatViewHost {
|
||||
// nothing yet, so there is nothing to restore. Un-submitted context-chip
|
||||
// edits are the one loss — the chips re-seed from the untouched message
|
||||
// on the next edit.
|
||||
if (this.runHeldElsewhere) {
|
||||
if (this.runHeldElsewhere || this.moveStarted) {
|
||||
if (newContent !== undefined) {
|
||||
this.restoreToInput(
|
||||
expanded(chatDraft(newContent, pastes ?? [])),
|
||||
@@ -4842,7 +4852,12 @@ export class AIChatManager implements ChatViewHost {
|
||||
)
|
||||
}
|
||||
// "Text", not "message": chip edits are the part that does not survive.
|
||||
sendUserToast('This session is running in another tab. Your text was kept.', true)
|
||||
sendUserToast(
|
||||
this.moveStarted
|
||||
? 'This session is moving to a fork. Your text was kept.'
|
||||
: 'This session is running in another tab. Your text was kept.',
|
||||
true
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
return
|
||||
}
|
||||
moving = true
|
||||
if (runtime) runtime.manager.moveStarted = true
|
||||
const releaseAutoResume = runtime?.manager.holdAutoResume()
|
||||
// Bracketed like a turn, so another tab on this session cannot send while its drafts
|
||||
// move, then re-reads the session (its new workspace, the model note) once it ends.
|
||||
@@ -621,7 +622,10 @@
|
||||
sendUserToast(`Session moved to ${req.targetName}`)
|
||||
} finally {
|
||||
moving = false
|
||||
if (runtime) runtime.manager.moving = false
|
||||
if (runtime) {
|
||||
runtime.manager.moving = false
|
||||
runtime.manager.moveStarted = false
|
||||
}
|
||||
releaseAutoResume?.()
|
||||
if (chatId)
|
||||
localRunEnded(session.id, runtime?.manager.historyManager.getCurrentChatId() ?? chatId)
|
||||
|
||||
@@ -128,6 +128,7 @@ export async function moveSessionToFork(
|
||||
// conflict they are; still parked, they would land on top of the moved draft later.
|
||||
const unsavedThere = await flushPendingSaves(fork.id)
|
||||
if (unsavedThere) return { ok: false, error: unsavedThere, retry: fork }
|
||||
if (opts.isBusy?.()) return { ok: false, error: busy, retry: fork }
|
||||
}
|
||||
|
||||
let copied = 0
|
||||
|
||||
Reference in New Issue
Block a user