Explorar o código

feat(app): add compact ui (#15578)

Filip hai 6 meses
pai
achega
b15fb21191

+ 0 - 1
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>

+ 0 - 2
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()}

+ 27 - 0
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);

+ 15 - 0
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()

+ 6 - 0
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;

+ 22 - 14
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">

+ 1 - 0
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}} بحث",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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}} 件の検索",

+ 1 - 0
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}}개 검색",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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}} поиск",

+ 1 - 0
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}} รายการ",

+ 1 - 0
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",

+ 1 - 0
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}} 次搜索",

+ 1 - 0
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}} 次搜尋",