Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 19 additions & 3 deletions packages/app/src/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -597,17 +597,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
},
more: createMemo(() => current()[0].session.length >= current()[0].limit),
archive: async (sessionID: string) => {
archive: (sessionID: string) => {
const directory = sdk.directory
const client = sdk.client
const [, setStore] = globalSync.child(directory)
await client.session.update({ sessionID, time: { archived: Date.now() } })
const [store, setStore] = globalSync.child(directory)
const session = (() => {
const match = Binary.search(store.session, sessionID, (s) => s.id)
return match.found ? store.session[match.index] : undefined
})()

// Optimistic: remove immediately
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)

client.session.update({ sessionID, time: { archived: Date.now() } }).catch(() => {
// Rollback: re-insert session
if (!session) return
setStore(
produce((draft) => {
const result = Binary.search(draft.session, sessionID, (s) => s.id)
if (!result.found) draft.session.splice(result.index, 0, session)
}),
)
})
},
},
absolute,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit

const sending = createMemo(() => replyMutation.isPending || rejectMutation.isPending)

const reply = async (answers: QuestionAnswer[]) => {
const reply = (answers: QuestionAnswer[]) => {
if (sending()) return
await replyMutation.mutateAsync(answers)
replyMutation.mutate(answers)
}

const reject = async () => {
const reject = () => {
if (sending()) return
await rejectMutation.mutateAsync()
rejectMutation.mutate()
}

const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
Expand Down
138 changes: 79 additions & 59 deletions packages/app/src/pages/session/message-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,16 +401,26 @@ export function MessageTimeline(props: {
const titleMutation = useMutation(() => ({
mutationFn: (input: { id: string; title: string }) =>
sdk.client.session.update({ sessionID: input.id, title: input.title }),
onSuccess: (_, input) => {
onMutate: (input) => {
const prev = sync.session.get(input.id)?.title
sync.set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === input.id)
if (index !== -1) draft.session[index].title = input.title
}),
)
setTitle("editing", false)
return { prev }
},
onError: (err) => {
onError: (err, input, context) => {
if (context?.prev !== undefined) {
sync.set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === input.id)
if (index !== -1) draft.session[index].title = context.prev!
}),
)
}
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err),
Expand Down Expand Up @@ -500,90 +510,100 @@ export function MessageTimeline(props: {
navigate(`/${params.dir}/session`)
}

const archiveSession = async (sessionID: string) => {
const archiveSession = (sessionID: string) => {
const session = sync.session.get(sessionID)
if (!session) return

const sessions = sync.data.session ?? []
const index = sessions.findIndex((s) => s.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])

await sdk.client.session
.update({ sessionID, time: { archived: Date.now() } })
.then(() => {
sync.set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === sessionID)
if (index !== -1) draft.session.splice(index, 1)
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
})
.catch((err) => {
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err),
})
// Optimistic: remove immediately and navigate
sync.set(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)

sdk.client.session.update({ sessionID, time: { archived: Date.now() } }).catch((err) => {
// Rollback: re-insert session
sync.set(
produce((draft) => {
const result = Binary.search(draft.session, sessionID, (s) => s.id)
if (!result.found) draft.session.splice(result.index, 0, session)
}),
)
showToast({
title: language.t("common.requestFailed"),
description: errorMessage(err),
})
})
}

const deleteSession = async (sessionID: string) => {
const deleteSession = (sessionID: string) => {
const session = sync.session.get(sessionID)
if (!session) return false

const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
const index = sessions.findIndex((s) => s.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])

const result = await sdk.client.session
// Collect sessions to remove (target + children) for rollback
const allSessions = sync.data.session ?? []
const removedSessions: typeof allSessions = []
const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>()
for (const item of allSessions) {
if (item.parentID) {
const existing = byParent.get(item.parentID)
if (existing) existing.push(item.id)
else byParent.set(item.parentID, [item.id])
}
}
const stack = [sessionID]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
for (const child of byParent.get(parentID) ?? []) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
for (const s of allSessions) {
if (removed.has(s.id)) removedSessions.push(s)
}

// Optimistic: remove immediately and navigate
sync.set(
produce((draft) => {
draft.session = draft.session.filter((s) => !removed.has(s.id))
}),
)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)

sdk.client.session
.delete({ sessionID })
.then((x) => x.data)
.catch((err) => {
// Rollback: re-insert all removed sessions
sync.set(
produce((draft) => {
for (const s of removedSessions) {
const result = Binary.search(draft.session, s.id, (x) => x.id)
if (!result.found) draft.session.splice(result.index, 0, s)
}
}),
)
showToast({
title: language.t("session.delete.failed.title"),
description: errorMessage(err),
})
return false
})

if (!result) return false

sync.set(
produce((draft) => {
const removed = new Set<string>([sessionID])

const byParent = new Map<string, string[]>()
for (const item of draft.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}

const stack = [sessionID]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue

const children = byParent.get(parentID)
if (!children) continue

for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}

draft.session = draft.session.filter((s) => !removed.has(s.id))
}),
)

navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
return true
}

Expand Down
Loading