Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
44f9a00
Scope pending attachments to committed sessions
lihongguang-0014 Sep 1, 2026
d967fba
Test pending attachment session retirement
lihongguang-0014 Sep 1, 2026
4200d32
Merge remote-tracking branch 'origin/main' into fix/session-scoped-pe…
lihongguang-0014 Sep 1, 2026
c9c8c95
Preserve attachments during draft materialization
lihongguang-0014 Sep 1, 2026
22b9e60
Harden attachment session lifecycle boundaries
lihongguang-0014 Sep 1, 2026
79ac8ea
Harden attachment recovery concurrency
lihongguang-0014 Sep 1, 2026
4b77094
Merge remote-tracking branch 'origin/main' into fix/session-scoped-pe…
lihongguang-0014 Sep 2, 2026
a946118
Update attachment E2E for session reads
lihongguang-0014 Sep 2, 2026
11a034e
Fence draft recovery across session changes
lihongguang-0014 Sep 2, 2026
cab6a0d
Fence pending draft recovery ownership
lihongguang-0014 Sep 2, 2026
9ff7357
Merge remote-tracking branch 'origin/main' into fix/session-scoped-pe…
lihongguang-0014 Sep 2, 2026
aa1f410
Close pending recovery race windows
lihongguang-0014 Sep 2, 2026
f412fc4
Preserve destructive queue ordering
lihongguang-0014 Sep 2, 2026
1a9d4ce
Preserve live queue CAS conflicts
lihongguang-0014 Sep 2, 2026
1a1ea71
Preserve mixed queue recovery order
lihongguang-0014 Sep 2, 2026
07cd11a
Acquire queue cancellation atomically
lihongguang-0014 Sep 2, 2026
ceb5aba
Fence queue cancellation continuations
lihongguang-0014 Sep 2, 2026
03cf221
Preserve queue cancellation across handoff
lihongguang-0014 Sep 2, 2026
caf83d4
Fence composer recovery during handoff
lihongguang-0014 Sep 2, 2026
5d1078e
Keep source queue fenced during handoff
lihongguang-0014 Sep 2, 2026
c9b33d3
Fence source queue before handoff persistence
lihongguang-0014 Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions opensquilla-webui/e2e/attachment-drag-upload.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { expect, test, type Download, type Page } from '@playwright/test'
import {
chatHistoryPayload,
sessionMessagesHydratePayload,
sessionMessagesSnapshotPayload,
sessionMessagesSubscribePayload,
} from './support/session-read-fixtures'

const CONTROL_URL = '/control/chat/new'
const HISTORY_IMAGE_DATA = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
Expand Down Expand Up @@ -42,10 +48,21 @@ async function mockRpc(page: Page, capturedSends: CapturedSend[], options: MockR
ws.onMessage(message => {
try {
const frame = JSON.parse(String(message))
if (frame?.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }))
return
}
if (frame?.type !== 'req') return
const method = String(frame.method || '')
const sessionKey = String(frame.params?.key || frame.params?.sessionKey || '')
if (method === 'connect') {
ws.send(JSON.stringify({ protocol: 3, policy: { tick_interval_ms: 30000 } }))
ws.send(JSON.stringify({
protocol: 3,
policy: {
tick_interval_ms: 30000,
concurrent_history_reads: true,
},
}))
return
}
if (method === 'chat.send') {
Expand Down Expand Up @@ -78,7 +95,7 @@ async function mockRpc(page: Page, capturedSends: CapturedSend[], options: MockR
}
if (method === 'chat.history') {
options.historyRequests?.push(frame.params || {})
ws.send(wsResponse(String(frame.id), { messages: historyMessages, has_more: false }))
ws.send(wsResponse(String(frame.id), chatHistoryPayload(historyMessages)))
return
}

Expand All @@ -91,12 +108,9 @@ async function mockRpc(page: Page, capturedSends: CapturedSend[], options: MockR
skills: {},
},
'sessions.list': { sessions: [], has_more: false },
'sessions.messages.subscribe': {
subscribed: true,
replay_complete: true,
current_stream_seq: 0,
run_status: 'idle',
},
'sessions.messages.snapshot': sessionMessagesSnapshotPayload(sessionKey),
'sessions.messages.subscribe': sessionMessagesSubscribePayload(sessionKey),
'sessions.messages.hydrate': sessionMessagesHydratePayload(sessionKey),
'usage.status': { sessions: [] },
}

Expand Down
42 changes: 41 additions & 1 deletion opensquilla-webui/src/components/chat/ChatComposer.i18n.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom

import { afterEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import { createApp, h, nextTick, ref } from 'vue'

import i18n, { loadLocaleMessages } from '@/i18n'
import type { Attachment } from '@/types/chat'
Expand Down Expand Up @@ -35,6 +35,46 @@ afterEach(() => {
})

describe('ChatComposer attachment localization', () => {
it('renders and removes the intended attachment when recovered and new IDs differ', async () => {
const attachments = ref<Attachment[]>([
{
kind: 'staged',
local_id: -1,
name: 'recovered.txt',
mime: 'text/plain',
file_uuid: 'recovered-upload',
},
{
kind: 'staged',
local_id: 1,
name: 'new.txt',
mime: 'text/plain',
file_uuid: 'new-upload',
},
])
const el = document.createElement('div')
document.body.appendChild(el)
const app = createApp({
render: () => h(ChatComposer, {
...BASE_PROPS,
attachments: attachments.value,
onRemoveAttachment: (index: number) => attachments.value.splice(index, 1),
} as any),
})
app.use(i18n)
app.mount(el)
await nextTick()

expect([...el.querySelectorAll('.attachment-chip__name')].map(node => node.textContent))
.toEqual(['recovered.txt', 'new.txt'])
el.querySelectorAll<HTMLButtonElement>('.attachment-remove')[0]?.click()
await nextTick()
expect([...el.querySelectorAll('.attachment-chip__name')].map(node => node.textContent))
.toEqual(['new.txt'])

app.unmount()
})

it('localizes failed status, fallback file label, and retry accessibility text', async () => {
await loadLocaleMessages('zh-Hans')
i18n.global.locale.value = 'zh-Hans'
Expand Down
Loading
Loading