Просмотр исходного кода

feat(app): show which messages are queued (#15587)

Filip 6 месяцев назад
Родитель
Сommit
bf2cc3aa2f

+ 21 - 1
packages/ui/src/components/message-part.css

@@ -46,12 +46,18 @@
     overflow: hidden;
     overflow: hidden;
     background: var(--surface-weak);
     background: var(--surface-weak);
     border: 1px solid var(--border-weak-base);
     border: 1px solid var(--border-weak-base);
-    transition: border-color 0.15s ease;
+    transition:
+      border-color 0.15s ease,
+      opacity 0.3s ease;
 
 
     &:hover {
     &:hover {
       border-color: var(--border-strong-base);
       border-color: var(--border-strong-base);
     }
     }
 
 
+    &[data-queued] {
+      opacity: 0.6;
+    }
+
     &[data-type="image"] {
     &[data-type="image"] {
       width: 48px;
       width: 48px;
       height: 48px;
       height: 48px;
@@ -101,6 +107,11 @@
     border: 1px solid var(--border-weak-base);
     border: 1px solid var(--border-weak-base);
     padding: 8px 12px;
     padding: 8px 12px;
     border-radius: 6px;
     border-radius: 6px;
+    transition: opacity 0.3s ease;
+
+    &[data-queued] {
+      opacity: 0.6;
+    }
 
 
     [data-highlight="file"] {
     [data-highlight="file"] {
       color: var(--syntax-property);
       color: var(--syntax-property);
@@ -113,6 +124,14 @@
     max-width: 100%;
     max-width: 100%;
   }
   }
 
 
+  [data-slot="user-message-queued-indicator"] {
+    margin-top: 6px;
+    margin-right: 2px;
+    font-size: var(--font-size-small);
+    color: var(--text-weak);
+    user-select: none;
+  }
+
   [data-slot="user-message-copy-wrapper"] {
   [data-slot="user-message-copy-wrapper"] {
     min-height: 24px;
     min-height: 24px;
     margin-top: 4px;
     margin-top: 4px;
@@ -149,6 +168,7 @@
     align-items: center;
     align-items: center;
     justify-content: flex-end;
     justify-content: flex-end;
     overflow: hidden;
     overflow: hidden;
+    gap: 6px;
   }
   }
 
 
   [data-slot="user-message-meta-tail"] {
   [data-slot="user-message-meta-tail"] {

+ 15 - 2
packages/ui/src/components/message-part.tsx

@@ -92,6 +92,7 @@ export interface MessageProps {
   parts: PartType[]
   parts: PartType[]
   showAssistantCopyPartID?: string | null
   showAssistantCopyPartID?: string | null
   interrupted?: boolean
   interrupted?: boolean
+  queued?: boolean
   showReasoningSummaries?: boolean
   showReasoningSummaries?: boolean
 }
 }
 
 
@@ -500,6 +501,7 @@ export function Message(props: MessageProps) {
             message={userMessage() as UserMessage}
             message={userMessage() as UserMessage}
             parts={props.parts}
             parts={props.parts}
             interrupted={props.interrupted}
             interrupted={props.interrupted}
+            queued={props.queued}
           />
           />
         )}
         )}
       </Match>
       </Match>
@@ -679,7 +681,12 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
   )
   )
 }
 }
 
 
-export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[]; interrupted?: boolean }) {
+export function UserMessageDisplay(props: {
+  message: UserMessage
+  parts: PartType[]
+  interrupted?: boolean
+  queued?: boolean
+}) {
   const data = useData()
   const data = useData()
   const dialog = useDialog()
   const dialog = useDialog()
   const i18n = useI18n()
   const i18n = useI18n()
@@ -759,6 +766,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
               <div
               <div
                 data-slot="user-message-attachment"
                 data-slot="user-message-attachment"
                 data-type={file.mime.startsWith("image/") ? "image" : "file"}
                 data-type={file.mime.startsWith("image/") ? "image" : "file"}
+                data-queued={props.queued ? "" : undefined}
                 onClick={() => {
                 onClick={() => {
                   if (file.mime.startsWith("image/") && file.url) {
                   if (file.mime.startsWith("image/") && file.url) {
                     openImagePreview(file.url, file.filename)
                     openImagePreview(file.url, file.filename)
@@ -787,9 +795,14 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
       <Show when={text()}>
       <Show when={text()}>
         <>
         <>
           <div data-slot="user-message-body">
           <div data-slot="user-message-body">
-            <div data-slot="user-message-text">
+            <div data-slot="user-message-text" data-queued={props.queued ? "" : undefined}>
               <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
               <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
             </div>
             </div>
+            <Show when={props.queued}>
+              <div data-slot="user-message-queued-indicator">
+                <TextShimmer text={i18n.t("ui.message.queued")} />
+              </div>
+            </Show>
           </div>
           </div>
           <div data-slot="user-message-copy-wrapper" data-interrupted={props.interrupted ? "" : undefined}>
           <div data-slot="user-message-copy-wrapper" data-interrupted={props.interrupted ? "" : undefined}>
             <Show when={metaHead() || metaTail()}>
             <Show when={metaHead() || metaTail()}>

+ 24 - 3
packages/ui/src/components/session-turn.tsx

@@ -192,11 +192,31 @@ export function SessionTurn(
       (item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
       (item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
     )
     )
   })
   })
+
+  const pendingUser = createMemo(() => {
+    const item = pending()
+    if (!item?.parentID) return
+    const messages = allMessages() ?? emptyMessages
+    const result = Binary.search(messages, item.parentID, (m) => m.id)
+    const msg = result.found ? messages[result.index] : messages.find((m) => m.id === item.parentID)
+    if (!msg || msg.role !== "user") return
+    return msg
+  })
+
   const active = createMemo(() => {
   const active = createMemo(() => {
     const msg = message()
     const msg = message()
+    const parent = pendingUser()
+    if (!msg || !parent) return false
+    return parent.id === msg.id
+  })
+
+  const queued = createMemo(() => {
+    const id = message()?.id
+    if (!id) return false
+    if (!pendingUser()) return false
     const item = pending()
     const item = pending()
-    if (!msg || !item) return false
-    return item.parentID === msg.id
+    if (!item) return false
+    return id > item.id
   })
   })
 
 
   const parts = createMemo(() => {
   const parts = createMemo(() => {
@@ -334,6 +354,7 @@ export function SessionTurn(
   )
   )
   const showThinking = createMemo(() => {
   const showThinking = createMemo(() => {
     if (!working() || !!error()) return false
     if (!working() || !!error()) return false
+    if (queued()) return false
     if (status().type === "retry") return false
     if (status().type === "retry") return false
     if (showReasoningSummaries()) return assistantVisible() === 0
     if (showReasoningSummaries()) return assistantVisible() === 0
     if (assistantTailVisible() === "text") return false
     if (assistantTailVisible() === "text") return false
@@ -364,7 +385,7 @@ export function SessionTurn(
                 class={props.classes?.container}
                 class={props.classes?.container}
               >
               >
                 <div data-slot="session-turn-message-content" aria-live="off">
                 <div data-slot="session-turn-message-content" aria-live="off">
-                  <Message message={msg()} parts={parts()} interrupted={interrupted()} />
+                  <Message message={msg()} parts={parts()} interrupted={interrupted()} queued={queued()} />
                 </div>
                 </div>
                 <Show when={compaction()}>
                 <Show when={compaction()}>
                   {(part) => (
                   {(part) => (

+ 1 - 0
packages/ui/src/i18n/ar.ts

@@ -126,6 +126,7 @@ export const dict = {
   "ui.message.copyResponse": "نسخ الرد",
   "ui.message.copyResponse": "نسخ الرد",
   "ui.message.copied": "تم النسخ!",
   "ui.message.copied": "تم النسخ!",
   "ui.message.interrupted": "تمت المقاطعة",
   "ui.message.interrupted": "تمت المقاطعة",
+  "ui.message.queued": "في الانتظار",
   "ui.message.attachment.alt": "مرفق",
   "ui.message.attachment.alt": "مرفق",
 
 
   "ui.patch.action.deleted": "محذوف",
   "ui.patch.action.deleted": "محذوف",

+ 1 - 0
packages/ui/src/i18n/br.ts

@@ -126,6 +126,7 @@ export const dict = {
   "ui.message.copyResponse": "Copiar resposta",
   "ui.message.copyResponse": "Copiar resposta",
   "ui.message.copied": "Copiado!",
   "ui.message.copied": "Copiado!",
   "ui.message.interrupted": "Interrompido",
   "ui.message.interrupted": "Interrompido",
+  "ui.message.queued": "Na fila",
   "ui.message.attachment.alt": "anexo",
   "ui.message.attachment.alt": "anexo",
 
 
   "ui.patch.action.deleted": "Excluído",
   "ui.patch.action.deleted": "Excluído",

+ 1 - 0
packages/ui/src/i18n/bs.ts

@@ -130,6 +130,7 @@ export const dict = {
   "ui.message.copyResponse": "Kopiraj odgovor",
   "ui.message.copyResponse": "Kopiraj odgovor",
   "ui.message.copied": "Kopirano!",
   "ui.message.copied": "Kopirano!",
   "ui.message.interrupted": "Prekinuto",
   "ui.message.interrupted": "Prekinuto",
+  "ui.message.queued": "U redu",
   "ui.message.attachment.alt": "prilog",
   "ui.message.attachment.alt": "prilog",
 
 
   "ui.patch.action.deleted": "Obrisano",
   "ui.patch.action.deleted": "Obrisano",

+ 1 - 0
packages/ui/src/i18n/da.ts

@@ -125,6 +125,7 @@ export const dict = {
   "ui.message.copyResponse": "Kopier svar",
   "ui.message.copyResponse": "Kopier svar",
   "ui.message.copied": "Kopieret!",
   "ui.message.copied": "Kopieret!",
   "ui.message.interrupted": "Afbrudt",
   "ui.message.interrupted": "Afbrudt",
+  "ui.message.queued": "I kø",
   "ui.message.attachment.alt": "vedhæftning",
   "ui.message.attachment.alt": "vedhæftning",
 
 
   "ui.patch.action.deleted": "Slettet",
   "ui.patch.action.deleted": "Slettet",

+ 1 - 0
packages/ui/src/i18n/de.ts

@@ -131,6 +131,7 @@ export const dict = {
   "ui.message.copyResponse": "Antwort kopieren",
   "ui.message.copyResponse": "Antwort kopieren",
   "ui.message.copied": "Kopiert!",
   "ui.message.copied": "Kopiert!",
   "ui.message.interrupted": "Unterbrochen",
   "ui.message.interrupted": "Unterbrochen",
+  "ui.message.queued": "In Warteschlange",
   "ui.message.attachment.alt": "Anhang",
   "ui.message.attachment.alt": "Anhang",
 
 
   "ui.patch.action.deleted": "Gelöscht",
   "ui.patch.action.deleted": "Gelöscht",

+ 1 - 0
packages/ui/src/i18n/en.ts

@@ -127,6 +127,7 @@ export const dict: Record<string, string> = {
   "ui.message.copyResponse": "Copy response",
   "ui.message.copyResponse": "Copy response",
   "ui.message.copied": "Copied",
   "ui.message.copied": "Copied",
   "ui.message.interrupted": "Interrupted",
   "ui.message.interrupted": "Interrupted",
+  "ui.message.queued": "Queued",
   "ui.message.attachment.alt": "attachment",
   "ui.message.attachment.alt": "attachment",
 
 
   "ui.patch.action.deleted": "Deleted",
   "ui.patch.action.deleted": "Deleted",

+ 1 - 0
packages/ui/src/i18n/es.ts

@@ -126,6 +126,7 @@ export const dict = {
   "ui.message.copyResponse": "Copiar respuesta",
   "ui.message.copyResponse": "Copiar respuesta",
   "ui.message.copied": "¡Copiado!",
   "ui.message.copied": "¡Copiado!",
   "ui.message.interrupted": "Interrumpido",
   "ui.message.interrupted": "Interrumpido",
+  "ui.message.queued": "En cola",
   "ui.message.attachment.alt": "adjunto",
   "ui.message.attachment.alt": "adjunto",
 
 
   "ui.patch.action.deleted": "Eliminado",
   "ui.patch.action.deleted": "Eliminado",

+ 1 - 0
packages/ui/src/i18n/fr.ts

@@ -126,6 +126,7 @@ export const dict = {
   "ui.message.copyResponse": "Copier la réponse",
   "ui.message.copyResponse": "Copier la réponse",
   "ui.message.copied": "Copié !",
   "ui.message.copied": "Copié !",
   "ui.message.interrupted": "Interrompu",
   "ui.message.interrupted": "Interrompu",
+  "ui.message.queued": "En file",
   "ui.message.attachment.alt": "pièce jointe",
   "ui.message.attachment.alt": "pièce jointe",
 
 
   "ui.patch.action.deleted": "Supprimé",
   "ui.patch.action.deleted": "Supprimé",

+ 1 - 0
packages/ui/src/i18n/ja.ts

@@ -125,6 +125,7 @@ export const dict = {
   "ui.message.copyResponse": "応答をコピー",
   "ui.message.copyResponse": "応答をコピー",
   "ui.message.copied": "コピーしました!",
   "ui.message.copied": "コピーしました!",
   "ui.message.interrupted": "中断",
   "ui.message.interrupted": "中断",
+  "ui.message.queued": "待機中",
   "ui.message.attachment.alt": "添付ファイル",
   "ui.message.attachment.alt": "添付ファイル",
 
 
   "ui.patch.action.deleted": "削除済み",
   "ui.patch.action.deleted": "削除済み",

+ 1 - 0
packages/ui/src/i18n/ko.ts

@@ -126,6 +126,7 @@ export const dict = {
   "ui.message.copyResponse": "응답 복사",
   "ui.message.copyResponse": "응답 복사",
   "ui.message.copied": "복사됨!",
   "ui.message.copied": "복사됨!",
   "ui.message.interrupted": "중단됨",
   "ui.message.interrupted": "중단됨",
+  "ui.message.queued": "대기 중",
   "ui.message.attachment.alt": "첨부 파일",
   "ui.message.attachment.alt": "첨부 파일",
 
 
   "ui.patch.action.deleted": "삭제됨",
   "ui.patch.action.deleted": "삭제됨",

+ 1 - 0
packages/ui/src/i18n/no.ts

@@ -129,6 +129,7 @@ export const dict: Record<Keys, string> = {
   "ui.message.copyResponse": "Kopier svar",
   "ui.message.copyResponse": "Kopier svar",
   "ui.message.copied": "Kopiert!",
   "ui.message.copied": "Kopiert!",
   "ui.message.interrupted": "Avbrutt",
   "ui.message.interrupted": "Avbrutt",
+  "ui.message.queued": "I kø",
   "ui.message.attachment.alt": "vedlegg",
   "ui.message.attachment.alt": "vedlegg",
 
 
   "ui.patch.action.deleted": "Slettet",
   "ui.patch.action.deleted": "Slettet",

+ 1 - 0
packages/ui/src/i18n/pl.ts

@@ -125,6 +125,7 @@ export const dict = {
   "ui.message.copyResponse": "Kopiuj odpowiedź",
   "ui.message.copyResponse": "Kopiuj odpowiedź",
   "ui.message.copied": "Skopiowano!",
   "ui.message.copied": "Skopiowano!",
   "ui.message.interrupted": "Przerwano",
   "ui.message.interrupted": "Przerwano",
+  "ui.message.queued": "W kolejce",
   "ui.message.attachment.alt": "załącznik",
   "ui.message.attachment.alt": "załącznik",
 
 
   "ui.patch.action.deleted": "Usunięto",
   "ui.patch.action.deleted": "Usunięto",

+ 1 - 0
packages/ui/src/i18n/ru.ts

@@ -125,6 +125,7 @@ export const dict = {
   "ui.message.copyResponse": "Копировать ответ",
   "ui.message.copyResponse": "Копировать ответ",
   "ui.message.copied": "Скопировано!",
   "ui.message.copied": "Скопировано!",
   "ui.message.interrupted": "Прервано",
   "ui.message.interrupted": "Прервано",
+  "ui.message.queued": "В очереди",
   "ui.message.attachment.alt": "вложение",
   "ui.message.attachment.alt": "вложение",
 
 
   "ui.patch.action.deleted": "Удалено",
   "ui.patch.action.deleted": "Удалено",

+ 1 - 0
packages/ui/src/i18n/th.ts

@@ -127,6 +127,7 @@ export const dict = {
   "ui.message.copyResponse": "คัดลอกคำตอบ",
   "ui.message.copyResponse": "คัดลอกคำตอบ",
   "ui.message.copied": "คัดลอกแล้ว!",
   "ui.message.copied": "คัดลอกแล้ว!",
   "ui.message.interrupted": "ถูกขัดจังหวะ",
   "ui.message.interrupted": "ถูกขัดจังหวะ",
+  "ui.message.queued": "อยู่ในคิว",
   "ui.message.attachment.alt": "ไฟล์แนบ",
   "ui.message.attachment.alt": "ไฟล์แนบ",
 
 
   "ui.patch.action.deleted": "ลบ",
   "ui.patch.action.deleted": "ลบ",

+ 1 - 0
packages/ui/src/i18n/tr.ts

@@ -122,6 +122,7 @@ export const dict = {
   "ui.message.copyResponse": "Yanıtı kopyala",
   "ui.message.copyResponse": "Yanıtı kopyala",
   "ui.message.copied": "Kopyalandı",
   "ui.message.copied": "Kopyalandı",
   "ui.message.interrupted": "Kesildi",
   "ui.message.interrupted": "Kesildi",
+  "ui.message.queued": "Sırada",
   "ui.message.attachment.alt": "ek",
   "ui.message.attachment.alt": "ek",
 
 
   "ui.patch.action.deleted": "Silindi",
   "ui.patch.action.deleted": "Silindi",

+ 1 - 0
packages/ui/src/i18n/zh.ts

@@ -130,6 +130,7 @@ export const dict = {
   "ui.message.copyResponse": "复制回复",
   "ui.message.copyResponse": "复制回复",
   "ui.message.copied": "已复制!",
   "ui.message.copied": "已复制!",
   "ui.message.interrupted": "已中断",
   "ui.message.interrupted": "已中断",
+  "ui.message.queued": "排队中",
   "ui.message.attachment.alt": "附件",
   "ui.message.attachment.alt": "附件",
 
 
   "ui.patch.action.deleted": "已删除",
   "ui.patch.action.deleted": "已删除",

+ 1 - 0
packages/ui/src/i18n/zht.ts

@@ -130,6 +130,7 @@ export const dict = {
   "ui.message.copyResponse": "複製回覆",
   "ui.message.copyResponse": "複製回覆",
   "ui.message.copied": "已複製!",
   "ui.message.copied": "已複製!",
   "ui.message.interrupted": "已中斷",
   "ui.message.interrupted": "已中斷",
+  "ui.message.queued": "排隊中",
   "ui.message.attachment.alt": "附件",
   "ui.message.attachment.alt": "附件",
 
 
   "ui.patch.action.deleted": "已刪除",
   "ui.patch.action.deleted": "已刪除",