Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
59cdcf6
perf(ui): bound expensive transcript rendering
pascalandr Aug 12, 2026
635228a
perf(ui): cap inactive transcript memory
pascalandr Aug 12, 2026
20dfdc2
perf(ui): cancel obsolete transcript work
pascalandr Aug 12, 2026
bb39dab
perf(ui): harden bounded transcript memory
pascalandr Aug 13, 2026
1ca5471
merge(DEV-v2): refresh bounded transcript memory
pascalandr Aug 18, 2026
68955d3
Merge branch 'DEV-v2' into refresh/pr648-20260818
pascalandr Aug 18, 2026
4e40628
Merge remote-tracking branch 'upstream/DEV-v2' into refresh/pr648-202…
pascalandr Aug 18, 2026
b8fd968
Merge remote-tracking branch 'upstream/DEV-v2' into refresh/pr648-202…
pascalandr Aug 19, 2026
1b6de75
Merge remote-tracking branch 'upstream/DEV-v2' into refresh/pr648-202…
pascalandr Aug 19, 2026
1e5f80c
Merge remote-tracking branch 'upstream/DEV-v2' into refresh/pr648-202…
pascalandr Aug 19, 2026
4700b23
Merge branch 'DEV-v2' into refresh/pr648-20260818
pascalandr Aug 19, 2026
a6b355a
fix(ui): close transcript memory gate findings
pascalandr Aug 19, 2026
5301ce5
fix(ui): close truncated diff approval bypasses
pascalandr Aug 19, 2026
360a224
fix(memory): isolate live reducers and cache replacement
pascalandr Aug 19, 2026
760a7c5
Merge branch 'DEV-v2' into refresh/pr648-20260818
pascalandr Aug 19, 2026
87bd430
Merge branch 'DEV-v2' into refresh/pr648-20260818
pascalandr Aug 19, 2026
bd57dc1
Merge branch 'DEV-v2' into refresh/pr648-20260818
pascalandr Aug 19, 2026
4a8dcd7
Merge branch 'DEV-v2' into refresh/pr648-20260818
pascalandr Aug 19, 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
8 changes: 7 additions & 1 deletion .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,25 @@ jobs:

- name: Test changed runnable UI behavior
run: >-
node --import tsx --test
node --import tsx --test --test-force-exit
packages/ui/src/components/message-timeline-v2.test.ts
packages/ui/src/components/provider-auth/provider-options.test.ts
packages/ui/src/components/session/session-bottom-pin-intent.test.ts
packages/ui/src/components/session-list-visibility.test.ts
packages/ui/src/components/tool-call/render-memory.test.ts
packages/ui/src/components/unified-picker-path.test.ts
packages/ui/src/components/virtual-follow-behavior.test.ts
packages/ui/src/lib/filesystem-events.test.ts
packages/ui/src/lib/global-cache.test.ts
packages/ui/src/lib/hooks/use-app-session-capture.test.ts
packages/ui/src/lib/hooks/use-instance-metadata.test.ts
packages/ui/src/lib/hooks/use-foreground-refresh.test.ts
packages/ui/src/lib/launch-errors.test.ts
packages/ui/src/lib/message-selection-position.test.ts
packages/ui/src/lib/model-visibility.test.ts
packages/ui/src/lib/retained-size.test.ts
packages/ui/src/lib/runtime-env.test.ts
packages/ui/src/lib/session-transcript-lru.test.ts
packages/ui/src/lib/trailing-resync.test.ts
packages/ui/src/stores/abort-created-workspace-cleanup.test.ts
packages/ui/src/stores/app-session-reconciliation.test.ts
Expand All @@ -129,6 +133,7 @@ jobs:
packages/ui/src/stores/client-state.test.ts
packages/ui/src/stores/message-v2/instance-store.test.ts
packages/ui/src/stores/message-v2/message-hydration-authority.test.ts
packages/ui/src/stores/message-v2/message-window.test.ts
packages/ui/src/stores/message-v2/message-status.test.ts
packages/ui/src/stores/message-v2/normalizers.test.ts
packages/ui/src/stores/shell-store.test.ts
Expand All @@ -144,6 +149,7 @@ jobs:
node --conditions=browser --import tsx --test --test-force-exit
packages/ui/src/components/form-request-tool-target.test.ts
packages/ui/src/components/form-request.test.ts
packages/ui/src/components/tool-call/renderer-copy.test.ts
packages/ui/src/lib/hooks/use-active-session-message-load.test.ts
packages/ui/src/stores/forms.test.ts
packages/ui/src/stores/instances-restore-ownership.test.ts
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/instance/instance-shell2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
instanceId: () => props.instance.id,
instanceSessions: allInstanceSessions,
activeSessionId: activeSessionIdForInstance,
isActiveInstance: () => Boolean(props.isActiveInstance),
})

const showEmbeddedSidebarToggle = createMemo(() => !leftPinned() && !leftOpen())
Expand Down
101 changes: 22 additions & 79 deletions packages/ui/src/components/instance/shell/useSessionCache.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,41 @@
import { createEffect, createSignal, type Accessor } from "solid-js"
import { messageStoreBus } from "../../../stores/message-v2/bus"
import { clearSessionRenderCache } from "../../message-block"
import { getLogger } from "../../../lib/logger"
import { invalidateSessionMessageLoad } from "../../../stores/session-state"

const log = getLogger("session")

const SESSION_CACHE_LIMIT = 5
import { createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
import {
reconcileSessionTranscriptBudget,
setSessionTranscriptVisible,
touchSessionTranscript,
} from "../../../stores/session-transcript-memory"

type SessionCacheOptions = {
instanceId: Accessor<string>
instanceSessions: Accessor<Map<string, unknown>>
activeSessionId: Accessor<string | null>
isActiveInstance: Accessor<boolean>
}

type SessionCacheState = {
cachedSessionIds: Accessor<string[]>
}

export function useSessionCache(options: SessionCacheOptions): SessionCacheState {
const [cachedSessionIds, setCachedSessionIds] = createSignal<string[]>([])
const [pendingEvictions, setPendingEvictions] = createSignal<string[]>([])

const evictSession = (sessionId: string) => {
if (!sessionId) return
const instanceId = options.instanceId()
log.info("Evicting cached session", { instanceId, sessionId })
const store = messageStoreBus.getInstance(instanceId)
invalidateSessionMessageLoad(instanceId, sessionId)
store?.clearSession(sessionId, { preserveScroll: true, notify: false })
clearSessionRenderCache(instanceId, sessionId)
}

const scheduleEvictions = (ids: string[]) => {
if (!ids.length) return
setPendingEvictions((current) => {
const existing = new Set(current)
const next = [...current]
ids.forEach((id) => {
if (!existing.has(id)) {
next.push(id)
existing.add(id)
}
})
return next
})
}

createEffect(() => {
const pending = pendingEvictions()
if (!pending.length) return
const cached = new Set(cachedSessionIds())
const remaining: string[] = []
pending.forEach((id) => {
if (cached.has(id)) {
remaining.push(id)
} else {
evictSession(id)
}
})
if (remaining.length !== pending.length) {
setPendingEvictions(remaining)
}
})

createEffect(() => {
const cachedSessionIds = createMemo(() => {
const instanceSessions = options.instanceSessions()
const activeId = options.activeSessionId()
if (!options.isActiveInstance() || !activeId || activeId === "info" || !instanceSessions.has(activeId)) return []
return [activeId]
})

setCachedSessionIds((current) => {
const next = current.filter((id) => id !== "info" && instanceSessions.has(id))

const touch = (id: string | null) => {
if (!id || id === "info") return
if (!instanceSessions.has(id)) return

const index = next.indexOf(id)
if (index !== -1) {
next.splice(index, 1)
}
next.unshift(id)
}

touch(activeId)

const trimmed = next.length > SESSION_CACHE_LIMIT ? next.slice(0, SESSION_CACHE_LIMIT) : next
createEffect(() => {
const instanceId = options.instanceId()
const [sessionId] = cachedSessionIds()
if (!sessionId) return
setSessionTranscriptVisible(instanceId, sessionId, true)
touchSessionTranscript(instanceId, sessionId)
reconcileSessionTranscriptBudget()
onCleanup(() => setSessionTranscriptVisible(instanceId, sessionId, false))
})

const trimmedSet = new Set(trimmed)
const removed = current.filter((id) => !trimmedSet.has(id))
if (removed.length) {
scheduleEvictions(removed)
}
return trimmed
})
onCleanup(() => {
reconcileSessionTranscriptBudget()
})

return {
Expand Down
45 changes: 33 additions & 12 deletions packages/ui/src/components/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { useGlobalCache } from "../lib/hooks/use-global-cache"
import type { TextPart, RenderCache } from "../types/message"
import { getLogger } from "../lib/logger"
import { copyToClipboard } from "../lib/clipboard"
import { useI18n } from "../lib/i18n"
import { limitToolOutputForRender, TOOL_OUTPUT_RENDER_CHARACTER_LIMIT } from "./tool-call/utils"

const log = getLogger("session")

Expand Down Expand Up @@ -89,6 +90,10 @@ function renderFallbackHtml(content: string): string {
return escapeHtml(content).replace(/\n/g, "<br />")
}

export function getMarkdownTextForRender(content: string): string {
return limitToolOutputForRender(content)
}

interface MarkdownProps {
part: TextPart
instanceId?: string
Expand Down Expand Up @@ -158,7 +163,7 @@ export function Markdown(props: MarkdownProps) {
const resolved = createMemo(() => {
const part = props.part
const rawText = typeof part.text === "string" ? part.text : ""
const text = decodeHtmlEntitiesLocally(rawText)
const text = decodeHtmlEntitiesLocally(getMarkdownTextForRender(rawText))
const themeKey = Boolean(props.isDark) ? "dark" : "light"
const highlightEnabled = !props.disableHighlight
const escapeRawHtml = Boolean(props.escapeRawHtml)
Expand Down Expand Up @@ -346,15 +351,31 @@ export function Markdown(props: MarkdownProps) {
})

return (
<div
ref={containerRef}
class="markdown-body"
dir="auto"
data-view="markdown"
data-part-id={resolved().partId}
data-markdown-theme={resolved().themeKey}
data-markdown-highlight={resolved().highlightEnabled ? "true" : "false"}
innerHTML={html()}
/>
<>
<div
ref={containerRef}
class="markdown-body"
dir="auto"
data-view="markdown"
data-part-id={resolved().partId}
data-markdown-theme={resolved().themeKey}
data-markdown-highlight={resolved().highlightEnabled ? "true" : "false"}
innerHTML={html()}
/>
<Show when={(typeof props.part.text === "string" ? props.part.text.length : 0) > TOOL_OUTPUT_RENDER_CHARACTER_LIMIT}>
<button
type="button"
class="message-action-button markdown-source-copy"
onClick={() => void copyToClipboard(typeof props.part.text === "string" ? props.part.text : "")}
aria-label={t("messageItem.actions.copyTitle")}
title={t("messageItem.actions.copyTitle")}
>
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
</Show>
</>
)
}
Loading
Loading