mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
fix(ui): surface pending interruptions
Auto-open the permission/question center when an active instance receives pending interruptions so an earlier permission cannot remain hidden while a later question shows Waiting for earlier responses. Keep question message-store attachments deduplicated when a request moves from a global fallback entry to a resolved tool part, and order active question state by enqueue time to match the permission flow. Queued permission actions are disabled to preserve the same active-interruption ordering as questions. Validation: npm run typecheck --workspace @codenomad/ui; node --import tsx --test packages/ui/src/stores/message-v2/instance-store.test.ts
This commit is contained in:
parent
681755888f
commit
9e1e16feb2
5 changed files with 69 additions and 11 deletions
|
|
@ -340,10 +340,25 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
return t("instanceShell.connection.unknown")
|
||||
}
|
||||
|
||||
const hasPendingRequests = createMemo(() => {
|
||||
const pendingRequestCount = createMemo(() => {
|
||||
const permissions = getPermissionQueueLength(props.instance.id)
|
||||
const questions = getQuestionQueueLength(props.instance.id)
|
||||
return permissions + questions > 0
|
||||
return permissions + questions
|
||||
})
|
||||
const hasPendingRequests = createMemo(() => {
|
||||
return pendingRequestCount() > 0
|
||||
})
|
||||
|
||||
let previousPendingRequestCount = 0
|
||||
let previousIsActiveInstance = props.isActiveInstance !== false
|
||||
createEffect(() => {
|
||||
const count = pendingRequestCount()
|
||||
const isActiveInstance = props.isActiveInstance !== false
|
||||
if (isActiveInstance && count > 0 && (previousPendingRequestCount === 0 || !previousIsActiveInstance)) {
|
||||
setPermissionModalOpen(true)
|
||||
}
|
||||
previousPendingRequestCount = count
|
||||
previousIsActiveInstance = isActiveInstance
|
||||
})
|
||||
|
||||
const activePromptInputApi = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
disabled={!isActive() || permissionSubmitting().has(item.id)}
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "once")}
|
||||
>
|
||||
{t("permissionApproval.actions.allowOnce")}
|
||||
|
|
@ -383,7 +383,7 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
disabled={!isActive() || permissionSubmitting().has(item.id)}
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "always")}
|
||||
>
|
||||
{t("permissionApproval.actions.alwaysAllow")}
|
||||
|
|
@ -391,7 +391,7 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={permissionSubmitting().has(item.id)}
|
||||
disabled={!isActive() || permissionSubmitting().has(item.id)}
|
||||
onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "reject")}
|
||||
>
|
||||
{t("permissionApproval.actions.deny")}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export function PermissionToolBlock(props: PermissionToolBlockProps) {
|
|||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
onClick={() => respond("once")}
|
||||
>
|
||||
{t("toolCall.permission.actions.allowOnce")}
|
||||
|
|
@ -89,7 +89,7 @@ export function PermissionToolBlock(props: PermissionToolBlockProps) {
|
|||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
onClick={() => respond("always")}
|
||||
>
|
||||
{t("toolCall.permission.actions.alwaysAllow")}
|
||||
|
|
@ -97,7 +97,7 @@ export function PermissionToolBlock(props: PermissionToolBlockProps) {
|
|||
<button
|
||||
type="button"
|
||||
class="tool-call-permission-button"
|
||||
disabled={props.submitting()}
|
||||
disabled={!props.active() || props.submitting()}
|
||||
onClick={() => respond("reject")}
|
||||
>
|
||||
{t("toolCall.permission.actions.deny")}
|
||||
|
|
|
|||
|
|
@ -35,3 +35,36 @@ describe("message-v2 permission state", () => {
|
|||
assert.equal(store.getPermissionState(undefined, "permission-2")?.active, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("message-v2 question state", () => {
|
||||
it("keeps one question attachment when a duplicate moves from global to a tool part", () => {
|
||||
const store = createInstanceMessageStore("instance-1")
|
||||
|
||||
store.upsertQuestion({
|
||||
request: { id: "question-1", questions: [] } as any,
|
||||
enqueuedAt: 1_000,
|
||||
})
|
||||
store.upsertQuestion({
|
||||
request: { id: "question-1", questions: [] } as any,
|
||||
messageId: "message-1",
|
||||
partId: "part-1",
|
||||
enqueuedAt: 2_000,
|
||||
})
|
||||
|
||||
assert.equal(store.state.questions.queue.length, 1)
|
||||
assert.equal(store.getQuestionState(undefined, "question-1"), null)
|
||||
assert.equal(store.getQuestionState("message-1", "part-1")?.entry.request.id, "question-1")
|
||||
assert.equal(store.getQuestionState("message-1", "part-1")?.active, true)
|
||||
})
|
||||
|
||||
it("uses enqueue time when recalculating the active question", () => {
|
||||
const store = createInstanceMessageStore("instance-1")
|
||||
|
||||
store.upsertQuestion({ request: { id: "question-2", questions: [] } as any, enqueuedAt: 2_000 })
|
||||
store.upsertQuestion({ request: { id: "question-1", questions: [] } as any, enqueuedAt: 1_000 })
|
||||
|
||||
assert.equal(store.state.questions.active?.request.id, "question-1")
|
||||
assert.equal(store.getQuestionState(undefined, "question-1")?.active, true)
|
||||
assert.equal(store.getQuestionState(undefined, "question-2")?.active, false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -990,6 +990,17 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
|||
setState(
|
||||
"questions",
|
||||
produce((draft) => {
|
||||
Object.keys(draft.byMessage).forEach((existingMessageKey) => {
|
||||
const partEntries = draft.byMessage[existingMessageKey]
|
||||
Object.keys(partEntries).forEach((existingPartKey) => {
|
||||
if (partEntries[existingPartKey].request.id === entry.request.id) {
|
||||
delete partEntries[existingPartKey]
|
||||
}
|
||||
})
|
||||
if (Object.keys(partEntries).length === 0) {
|
||||
delete draft.byMessage[existingMessageKey]
|
||||
}
|
||||
})
|
||||
draft.byMessage[messageKey] = draft.byMessage[messageKey] ?? {}
|
||||
draft.byMessage[messageKey][partKey] = entry
|
||||
const existingIndex = draft.queue.findIndex((item) => item.request.id === entry.request.id)
|
||||
|
|
@ -998,9 +1009,8 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
|||
} else {
|
||||
draft.queue[existingIndex] = entry
|
||||
}
|
||||
if (!draft.active || draft.active.request.id === entry.request.id) {
|
||||
draft.active = entry
|
||||
}
|
||||
draft.queue.sort((left, right) => left.enqueuedAt - right.enqueuedAt)
|
||||
draft.active = draft.queue[0] ?? null
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue