diff --git a/app-prefixable/src/components/telegram-settings.tsx b/app-prefixable/src/components/telegram-settings.tsx index 3f89c92c..b2e18e3f 100644 --- a/app-prefixable/src/components/telegram-settings.tsx +++ b/app-prefixable/src/components/telegram-settings.tsx @@ -274,7 +274,7 @@ export function TelegramSettings(props: Props) {

Config: {report().config.status === "ok" ? "loaded" : "invalid"}

Telegram API: {report().dependencies.telegramApi.status} - {report().dependencies.telegramApi.message}

OpenCode API: {report().dependencies.openCodeApi.status} - {report().dependencies.openCodeApi.message}

- 0}> + 1}>
{(report().dependencies.openCodeSources || []).map((item) => (

Source {item.sourceId}: {item.status} - {item.message}

diff --git a/app-prefixable/src/context/sync.tsx b/app-prefixable/src/context/sync.tsx index 0b303b00..b84641fc 100644 --- a/app-prefixable/src/context/sync.tsx +++ b/app-prefixable/src/context/sync.tsx @@ -58,6 +58,70 @@ function sortParts(parts: Part[]): Part[] { return [...withId, ...withoutId] } +function toolEnd(part: Extract): number { + const state = part.state + if (state.status === "completed") return state.time.end + if (state.status === "error") return state.time.end + return 0 +} + +function toolStart(part: Extract): number { + const state = part.state + if (state.status === "running") return state.time.start + if (state.status === "completed") return state.time.start + if (state.status === "error") return state.time.start + return 0 +} + +function toolRank(part: Extract): number { + const state = part.state + if (state.status === "pending") return 1 + if (state.status === "running") return 2 + return 3 +} + +function mergePart(existing: Part, synced: Part): Part { + if (existing.type !== "tool") return synced + if (synced.type !== "tool") return synced + + const existingEnd = toolEnd(existing) + const syncedEnd = toolEnd(synced) + if (existingEnd > syncedEnd) return existing + if (syncedEnd > existingEnd) return synced + + const existingStart = toolStart(existing) + const syncedStart = toolStart(synced) + if (existingStart > syncedStart) return existing + if (syncedStart > existingStart) return synced + + const existingRank = toolRank(existing) + const syncedRank = toolRank(synced) + if (existingRank > syncedRank) return existing + if (syncedRank > existingRank) return synced + + return synced +} + +function mergeMessage(existing: MessageWithParts, synced: MessageWithParts): MessageWithParts { + const map = new Map(existing.parts.map((part) => [part.id, part])) + const merged = synced.parts.map((part) => { + const current = map.get(part.id) + if (!current) return part + return mergePart(current, part) + }) + const ids = new Set(merged.map((part) => part.id)) + + for (const part of existing.parts) { + if (ids.has(part.id)) continue + merged.push(part) + } + + return { + info: synced.info, + parts: sortParts(merged), + } +} + function errorText(err: unknown) { if (err instanceof Error && err.message.trim()) return err.message return "Failed to bootstrap app state from API." @@ -405,19 +469,18 @@ export function SyncProvider(props: ParentProps) { setStore("message", sessionID, (existing: MessageWithParts[]) => { if (!existing || existing.length === 0) return synced - // Merge: use existing message if it has more recent parts - const merged = synced.map((s) => { - const e = existing.find((m) => m.info.id === s.info.id) - if (!e) return s - // Keep existing if it has more parts (SSE updates arrived) - return e.parts.length >= s.parts.length ? e : s + const map = new Map(existing.map((msg) => [msg.info.id, msg])) + const merged = synced.map((msg) => { + const current = map.get(msg.info.id) + if (!current) return msg + return mergeMessage(current, msg) }) + const ids = new Set(merged.map((msg) => msg.info.id)) // Add any messages from existing that aren't in synced (new SSE messages) - for (const e of existing) { - if (!merged.find((m) => m.info.id === e.info.id)) { - merged.push(e) - } + for (const msg of existing) { + if (ids.has(msg.info.id)) continue + merged.push(msg) } return merged.sort((a, b) => cmp(a.info.id, b.info.id)) diff --git a/app-prefixable/src/pages/session.tsx b/app-prefixable/src/pages/session.tsx index f6c5eca7..b1611a45 100644 --- a/app-prefixable/src/pages/session.tsx +++ b/app-prefixable/src/pages/session.tsx @@ -560,29 +560,13 @@ export function Session() { })(), ); const [notifyDenied, setNotifyDenied] = createSignal(false); - const [notifySourceId, setNotifySourceId] = createSignal(); - const [notifySourceReady, setNotifySourceReady] = createSignal(false); const notifyVersion = { value: 0 }; const deniedTimer = { id: null as ReturnType | null }; onCleanup(() => { if (deniedTimer.id !== null) clearTimeout(deniedTimer.id) }); - createEffect(() => { - const dir = directory || base64Decode(params.dir); - setNotifySourceReady(false); - let cancelled = false; - onCleanup(() => { cancelled = true }); - resolveTelegramSourceId(url, dir).then((sourceId) => { - if (cancelled) return; - setNotifySourceId(sourceId); - setNotifySourceReady(true); - }); - }); - // Re-read notification state when session changes, and seed from server alarm state createEffect(() => { const id = params.id; - const sourceReady = notifySourceReady(); - const sourceId = notifySourceId(); const dir = directory || base64Decode(params.dir); setNotifyDenied(false); if (!id) { @@ -595,40 +579,38 @@ export function Session() { if (migrated) writeNotifyMap(map); const local = notifyEnabledForSession(map, id, dir); setNotifyEnabled(local); - if (!sourceReady) return; // Cancellation flag for stale responses let cancelled = false; const version = notifyVersion.value; onCleanup(() => { cancelled = true }); // Then fetch server-side alarm state asynchronously - getSessionAlarm(url, id, sourceId).then((state) => { - // Skip if effect was cleaned up, session changed, or local state was updated - if (cancelled || !state || params.id !== id) return; - if (notifyVersion.value !== version) return; - setNotifyEnabled(state.enabled); - // Sync localStorage to match server truth - const map = readNotifyMap(); - writeNotifySessionEnabled(map, id, state.enabled, dir); - writeNotifyMap(map); - }); + resolveTelegramSourceId(url, dir) + .then((sourceId) => getSessionAlarm(url, id, sourceId)) + .then((state) => { + // Skip if effect was cleaned up, session changed, or local state was updated + if (cancelled || !state || params.id !== id) return; + if (notifyVersion.value !== version) return; + setNotifyEnabled(state.enabled); + // Sync localStorage to match server truth + const map = readNotifyMap(); + writeNotifySessionEnabled(map, id, state.enabled, dir); + writeNotifyMap(map); + }); }); /** Mirror bell toggle to server-side alarm state (fire-and-forget). */ function syncAlarmToServer(id: string, enabled: boolean) { - const sourceId = notifySourceId(); - if (sourceId) { - setSessionAlarm(url, id, enabled, sourceId); - return; - } - const dir = directory || base64Decode(params.dir); - resolveTelegramSourceId(url, dir).then((resolved) => { - if (resolved) { - setNotifySourceId(resolved); - setSessionAlarm(url, id, enabled, resolved); - return; - } - console.warn("[session] skip setSessionAlarm: telegram source id unresolved", { id, dir }); - }); + const dir = directory || base64Decode(params.dir) + console.log("[session] syncing telegram session alarm", { sessionId: id, enabled, directory: dir }) + resolveTelegramSourceId(url, dir) + .then((sourceId) => setSessionAlarm(url, id, enabled, sourceId, dir)) + .then((ok) => { + if (ok) { + console.log("[session] telegram session alarm synced", { sessionId: id, enabled, directory: dir }) + return + } + console.warn("[session] telegram session alarm sync failed", { sessionId: id, enabled, directory: dir }) + }) } function toggleNotify() { diff --git a/app-prefixable/src/pages/settings.tsx b/app-prefixable/src/pages/settings.tsx index 89552493..dc6f79ea 100644 --- a/app-prefixable/src/pages/settings.tsx +++ b/app-prefixable/src/pages/settings.tsx @@ -138,7 +138,6 @@ export function Settings() { async function toggleTelegramAlarmChannel() { if (telegramAlarmSaving()) return const current = alarmChannels().telegram - if (!telegramAlarmReady() && !current) return setTelegramAlarmSaving(true) const target = !current @@ -2458,7 +2457,7 @@ Add your project-specific instructions here.