summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-03-01 15:41:47 +0100
committerGitHub <[email protected]>2026-03-01 08:41:47 -0600
commitb15fb211917de83b39d9ec3a1d66ae4353d1e6e0 (patch)
tree451d5004a7ac2966572189df5e11a70e2b1b22b2
parentc8866e60ba0b626962d7aaf81379cd96ec6c857a (diff)
downloadopencode-b15fb211917de83b39d9ec3a1d66ae4353d1e6e0.tar.gz
opencode-b15fb211917de83b39d9ec3a1d66ae4353d1e6e0.zip
feat(app): add compact ui (#15578)
-rw-r--r--packages/app/src/pages/session.tsx1
-rw-r--r--packages/app/src/pages/session/message-timeline.tsx2
-rw-r--r--packages/ui/src/components/message-part.css27
-rw-r--r--packages/ui/src/components/message-part.tsx15
-rw-r--r--packages/ui/src/components/session-turn.css6
-rw-r--r--packages/ui/src/components/session-turn.tsx36
-rw-r--r--packages/ui/src/i18n/ar.ts1
-rw-r--r--packages/ui/src/i18n/br.ts1
-rw-r--r--packages/ui/src/i18n/bs.ts1
-rw-r--r--packages/ui/src/i18n/da.ts1
-rw-r--r--packages/ui/src/i18n/de.ts1
-rw-r--r--packages/ui/src/i18n/en.ts1
-rw-r--r--packages/ui/src/i18n/es.ts1
-rw-r--r--packages/ui/src/i18n/fr.ts1
-rw-r--r--packages/ui/src/i18n/ja.ts1
-rw-r--r--packages/ui/src/i18n/ko.ts1
-rw-r--r--packages/ui/src/i18n/no.ts1
-rw-r--r--packages/ui/src/i18n/pl.ts1
-rw-r--r--packages/ui/src/i18n/ru.ts1
-rw-r--r--packages/ui/src/i18n/th.ts1
-rw-r--r--packages/ui/src/i18n/tr.ts1
-rw-r--r--packages/ui/src/i18n/zh.ts1
-rw-r--r--packages/ui/src/i18n/zht.ts1
23 files changed, 87 insertions, 17 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 0d2718efb..5ef68cc5c 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1099,7 +1099,6 @@ export default function Page() {
anchor={anchor}
onRegisterMessage={scrollSpy.register}
onUnregisterMessage={scrollSpy.unregister}
- lastUserMessageID={lastUserMessage()?.id}
/>
</Show>
</Match>
diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx
index 8215f31ba..d2de720a3 100644
--- a/packages/app/src/pages/session/message-timeline.tsx
+++ b/packages/app/src/pages/session/message-timeline.tsx
@@ -105,7 +105,6 @@ export function MessageTimeline(props: {
anchor: (id: string) => string
onRegisterMessage: (el: HTMLDivElement, id: string) => void
onUnregisterMessage: (id: string) => void
- lastUserMessageID?: string
}) {
let touchGesture: number | undefined
@@ -601,7 +600,6 @@ export function MessageTimeline(props: {
<SessionTurn
sessionID={sessionID() ?? ""}
messageID={message.id}
- lastUserMessageID={props.lastUserMessageID}
showReasoningSummaries={settings.general.showReasoningSummaries()}
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index 58227f625..6727bb22f 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -225,6 +225,33 @@
}
}
+[data-component="compaction-part"] {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+
+ [data-slot="compaction-part-divider"] {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 0;
+ width: 100%;
+ }
+
+ [data-slot="compaction-part-line"] {
+ flex: 1 1 auto;
+ height: 1px;
+ background: var(--border-weak-base);
+ }
+
+ [data-slot="compaction-part-label"] {
+ flex: 0 0 auto;
+ white-space: nowrap;
+ text-align: center;
+ }
+}
+
[data-component="reasoning-part"] {
width: 100%;
color: var(--text-base);
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 6b6dfe2e5..02a99f9dd 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -1037,6 +1037,21 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
)
}
+PART_MAPPING["compaction"] = function CompactionPartDisplay() {
+ const i18n = useI18n()
+ return (
+ <div data-component="compaction-part">
+ <div data-slot="compaction-part-divider">
+ <span data-slot="compaction-part-line" />
+ <span data-slot="compaction-part-label" class="text-12-regular text-text-weak">
+ {i18n.t("ui.messagePart.compaction")}
+ </span>
+ <span data-slot="compaction-part-line" />
+ </div>
+ </div>
+ )
+}
+
PART_MAPPING["text"] = function TextPartDisplay(props) {
const data = useData()
const i18n = useI18n()
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css
index 9639e6635..4af87b361 100644
--- a/packages/ui/src/components/session-turn.css
+++ b/packages/ui/src/components/session-turn.css
@@ -37,6 +37,12 @@
max-width: 100%;
}
+ [data-slot="session-turn-compaction"] {
+ width: 100%;
+ min-width: 0;
+ align-self: stretch;
+ }
+
[data-slot="session-turn-thinking"] {
display: flex;
align-items: center;
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index bd4f2843a..e329b1170 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { Dynamic } from "solid-js/web"
-import { AssistantParts, Message, PART_MAPPING } from "./message-part"
+import { AssistantParts, Message, Part, PART_MAPPING } from "./message-part"
import { Card } from "./card"
import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
@@ -139,7 +139,6 @@ export function SessionTurn(
props: ParentProps<{
sessionID: string
messageID: string
- lastUserMessageID?: string
showReasoningSummaries?: boolean
shellToolDefaultOpen?: boolean
editToolDefaultOpen?: boolean
@@ -187,18 +186,18 @@ export function SessionTurn(
return msg
})
- const lastUserMessageID = createMemo(() => {
- if (props.lastUserMessageID) return props.lastUserMessageID
-
+ const pending = createMemo(() => {
const messages = allMessages() ?? emptyMessages
- for (let i = messages.length - 1; i >= 0; i--) {
- const msg = messages[i]
- if (msg?.role === "user") return msg.id
- }
- return undefined
+ return messages.findLast(
+ (item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
+ )
+ })
+ const active = createMemo(() => {
+ const msg = message()
+ const item = pending()
+ if (!msg || !item) return false
+ return item.parentID === msg.id
})
-
- const isLastUserMessage = createMemo(() => props.messageID === lastUserMessageID())
const parts = createMemo(() => {
const msg = message()
@@ -206,6 +205,8 @@ export function SessionTurn(
return list(data.store.part?.[msg.id], emptyParts)
})
+ const compaction = createMemo(() => parts().find((part) => part.type === "compaction"))
+
const diffs = createMemo(() => {
const files = message()?.summary?.diffs
if (!files?.length) return emptyDiffs
@@ -285,7 +286,7 @@ export function SessionTurn(
})
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
- const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
+ const working = createMemo(() => status().type !== "idle" && active())
const showReasoningSummaries = createMemo(() => props.showReasoningSummaries ?? true)
const assistantCopyPartID = createMemo(() => {
@@ -365,6 +366,13 @@ export function SessionTurn(
<div data-slot="session-turn-message-content" aria-live="off">
<Message message={msg()} parts={parts()} interrupted={interrupted()} />
</div>
+ <Show when={compaction()}>
+ {(part) => (
+ <div data-slot="session-turn-compaction">
+ <Part part={part()} message={msg()} hideDetails />
+ </div>
+ )}
+ </Show>
<Show when={assistantMessages().length > 0}>
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
<AssistantParts
@@ -386,7 +394,7 @@ export function SessionTurn(
</Show>
</div>
</Show>
- <SessionRetry status={status()} show={isLastUserMessage()} />
+ <SessionRetry status={status()} show={active()} />
<Show when={edited() > 0 && !working()}>
<div data-slot="session-turn-diffs">
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts
index 1f4b30aa7..afd046d7e 100644
--- a/packages/ui/src/i18n/ar.ts
+++ b/packages/ui/src/i18n/ar.ts
@@ -60,6 +60,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "النظر في الخطوات التالية",
"ui.messagePart.questions.dismissed": "تم رفض الأسئلة",
+ "ui.messagePart.compaction": "تم ضغط السجل",
"ui.messagePart.context.read.one": "{{count}} قراءة",
"ui.messagePart.context.read.other": "{{count}} قراءات",
"ui.messagePart.context.search.one": "{{count}} بحث",
diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts
index b08a7f57f..9b7a1d1d4 100644
--- a/packages/ui/src/i18n/br.ts
+++ b/packages/ui/src/i18n/br.ts
@@ -60,6 +60,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Considerando próximos passos",
"ui.messagePart.questions.dismissed": "Perguntas descartadas",
+ "ui.messagePart.compaction": "Histórico compactado",
"ui.messagePart.context.read.one": "{{count}} leitura",
"ui.messagePart.context.read.other": "{{count}} leituras",
"ui.messagePart.context.search.one": "{{count}} pesquisa",
diff --git a/packages/ui/src/i18n/bs.ts b/packages/ui/src/i18n/bs.ts
index 9f2eb7cd2..7d31289e1 100644
--- a/packages/ui/src/i18n/bs.ts
+++ b/packages/ui/src/i18n/bs.ts
@@ -64,6 +64,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Razmatranje sljedećih koraka",
"ui.messagePart.questions.dismissed": "Pitanja odbačena",
+ "ui.messagePart.compaction": "Historija sažeta",
"ui.messagePart.context.read.one": "{{count}} čitanje",
"ui.messagePart.context.read.other": "{{count}} čitanja",
"ui.messagePart.context.search.one": "{{count}} pretraga",
diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts
index 26c7db0c5..3cd0328a1 100644
--- a/packages/ui/src/i18n/da.ts
+++ b/packages/ui/src/i18n/da.ts
@@ -59,6 +59,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Overvejer næste skridt",
"ui.messagePart.questions.dismissed": "Spørgsmål afvist",
+ "ui.messagePart.compaction": "Historik komprimeret",
"ui.messagePart.context.read.one": "{{count}} læsning",
"ui.messagePart.context.read.other": "{{count}} læsninger",
"ui.messagePart.context.search.one": "{{count}} søgning",
diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts
index 467fa4e2e..384ebd338 100644
--- a/packages/ui/src/i18n/de.ts
+++ b/packages/ui/src/i18n/de.ts
@@ -65,6 +65,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Nächste Schritte erwägen",
"ui.messagePart.questions.dismissed": "Fragen verworfen",
+ "ui.messagePart.compaction": "Verlauf komprimiert",
"ui.messagePart.context.read.one": "{{count}} Lesevorgang",
"ui.messagePart.context.read.other": "{{count}} Lesevorgänge",
"ui.messagePart.context.search.one": "{{count}} Suche",
diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts
index 60e169dfa..a78474daf 100644
--- a/packages/ui/src/i18n/en.ts
+++ b/packages/ui/src/i18n/en.ts
@@ -66,6 +66,7 @@ export const dict: Record<string, string> = {
"ui.messagePart.option.typeOwnAnswer": "Type your own answer",
"ui.messagePart.review.title": "Review your answers",
"ui.messagePart.questions.dismissed": "Questions dismissed",
+ "ui.messagePart.compaction": "History compacted",
"ui.messagePart.context.read.one": "{{count}} read",
"ui.messagePart.context.read.other": "{{count}} reads",
"ui.messagePart.context.search.one": "{{count}} search",
diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts
index 0a06f0d9d..c5b7d60ce 100644
--- a/packages/ui/src/i18n/es.ts
+++ b/packages/ui/src/i18n/es.ts
@@ -60,6 +60,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Considerando siguientes pasos",
"ui.messagePart.questions.dismissed": "Preguntas descartadas",
+ "ui.messagePart.compaction": "Historial compactado",
"ui.messagePart.context.read.one": "{{count}} lectura",
"ui.messagePart.context.read.other": "{{count}} lecturas",
"ui.messagePart.context.search.one": "{{count}} búsqueda",
diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts
index 86d277308..de1005ec3 100644
--- a/packages/ui/src/i18n/fr.ts
+++ b/packages/ui/src/i18n/fr.ts
@@ -60,6 +60,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Examen des prochaines étapes",
"ui.messagePart.questions.dismissed": "Questions ignorées",
+ "ui.messagePart.compaction": "Historique compacté",
"ui.messagePart.context.read.one": "{{count}} lecture",
"ui.messagePart.context.read.other": "{{count}} lectures",
"ui.messagePart.context.search.one": "{{count}} recherche",
diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts
index ca8c48efe..e9e1fff2f 100644
--- a/packages/ui/src/i18n/ja.ts
+++ b/packages/ui/src/i18n/ja.ts
@@ -59,6 +59,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "次のステップを検討中",
"ui.messagePart.questions.dismissed": "質問をスキップしました",
+ "ui.messagePart.compaction": "履歴を圧縮しました",
"ui.messagePart.context.read.one": "{{count}} 件の読み取り",
"ui.messagePart.context.read.other": "{{count}} 件の読み取り",
"ui.messagePart.context.search.one": "{{count}} 件の検索",
diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts
index 226de0d22..0280cc24e 100644
--- a/packages/ui/src/i18n/ko.ts
+++ b/packages/ui/src/i18n/ko.ts
@@ -60,6 +60,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "다음 단계 고려 중",
"ui.messagePart.questions.dismissed": "질문 무시됨",
+ "ui.messagePart.compaction": "기록이 압축됨",
"ui.messagePart.context.read.one": "{{count}}개 읽음",
"ui.messagePart.context.read.other": "{{count}}개 읽음",
"ui.messagePart.context.search.one": "{{count}}개 검색",
diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts
index 5243a61b8..ca7db5d75 100644
--- a/packages/ui/src/i18n/no.ts
+++ b/packages/ui/src/i18n/no.ts
@@ -63,6 +63,7 @@ export const dict: Record<Keys, string> = {
"ui.sessionTurn.status.consideringNextSteps": "Vurderer neste trinn",
"ui.messagePart.questions.dismissed": "Spørsmål avvist",
+ "ui.messagePart.compaction": "Historikk komprimert",
"ui.messagePart.context.read.one": "{{count}} lest",
"ui.messagePart.context.read.other": "{{count}} lest",
"ui.messagePart.context.search.one": "{{count}} søk",
diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts
index 339694c26..ccc46a7f1 100644
--- a/packages/ui/src/i18n/pl.ts
+++ b/packages/ui/src/i18n/pl.ts
@@ -59,6 +59,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Rozważanie kolejnych kroków",
"ui.messagePart.questions.dismissed": "Pytania odrzucone",
+ "ui.messagePart.compaction": "Historia skompaktowana",
"ui.messagePart.context.read.one": "{{count}} odczyt",
"ui.messagePart.context.read.other": "{{count}} odczyty",
"ui.messagePart.context.search.one": "{{count}} wyszukiwanie",
diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts
index ca3ce8e05..9e9d6722f 100644
--- a/packages/ui/src/i18n/ru.ts
+++ b/packages/ui/src/i18n/ru.ts
@@ -59,6 +59,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Рассмотрение следующих шагов",
"ui.messagePart.questions.dismissed": "Вопросы отклонены",
+ "ui.messagePart.compaction": "История сжата",
"ui.messagePart.context.read.one": "{{count}} чтение",
"ui.messagePart.context.read.other": "{{count}} чтений",
"ui.messagePart.context.search.one": "{{count}} поиск",
diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts
index 2043bbdc8..c82acd225 100644
--- a/packages/ui/src/i18n/th.ts
+++ b/packages/ui/src/i18n/th.ts
@@ -61,6 +61,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "พิจารณาขั้นตอนถัดไป",
"ui.messagePart.questions.dismissed": "ละทิ้งคำถามแล้ว",
+ "ui.messagePart.compaction": "ประวัติถูกบีบอัด",
"ui.messagePart.context.read.one": "อ่าน {{count}} รายการ",
"ui.messagePart.context.read.other": "อ่าน {{count}} รายการ",
"ui.messagePart.context.search.one": "ค้นหา {{count}} รายการ",
diff --git a/packages/ui/src/i18n/tr.ts b/packages/ui/src/i18n/tr.ts
index da660034b..766dcb852 100644
--- a/packages/ui/src/i18n/tr.ts
+++ b/packages/ui/src/i18n/tr.ts
@@ -56,6 +56,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "Sonraki adımlar değerlendiriliyor",
"ui.messagePart.questions.dismissed": "Sorular reddedildi",
+ "ui.messagePart.compaction": "Geçmiş sıkıştırıldı",
"ui.messagePart.context.read.one": "{{count}} okuma",
"ui.messagePart.context.read.other": "{{count}} okuma",
"ui.messagePart.context.search.one": "{{count}} arama",
diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts
index 78006482a..98f1f0377 100644
--- a/packages/ui/src/i18n/zh.ts
+++ b/packages/ui/src/i18n/zh.ts
@@ -64,6 +64,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步",
"ui.messagePart.questions.dismissed": "问题已忽略",
+ "ui.messagePart.compaction": "历史已压缩",
"ui.messagePart.context.read.one": "{{count}} 次读取",
"ui.messagePart.context.read.other": "{{count}} 次读取",
"ui.messagePart.context.search.one": "{{count}} 次搜索",
diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts
index 044697bf6..6f1b1d380 100644
--- a/packages/ui/src/i18n/zht.ts
+++ b/packages/ui/src/i18n/zht.ts
@@ -64,6 +64,7 @@ export const dict = {
"ui.sessionTurn.status.consideringNextSteps": "正在考慮下一步",
"ui.messagePart.questions.dismissed": "問題已略過",
+ "ui.messagePart.compaction": "歷史已壓縮",
"ui.messagePart.context.read.one": "{{count}} 次讀取",
"ui.messagePart.context.read.other": "{{count}} 次讀取",
"ui.messagePart.context.search.one": "{{count}} 次搜尋",