Adam 3 месяцев назад
Родитель
Сommit
3210df7428
2 измененных файлов с 114 добавлено и 28 удалено
  1. 94 28
      packages/desktop/src/pages/index.tsx
  2. 20 0
      packages/ui/src/styles/tailwind/index.css

+ 94 - 28
packages/desktop/src/pages/index.tsx

@@ -36,6 +36,7 @@ import { ProgressCircle } from "@/components/progress-circle"
 import { Message, Part } from "@/components/message"
 import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
 import { DiffChanges } from "@/components/diff-changes"
+import { Markdown } from "@/components/markdown"
 
 export default function Page() {
   const local = useLocal()
@@ -491,8 +492,8 @@ export default function Page() {
                   </Show>
                 </div>
               </div>
-              <Tabs.Content value="chat" class="select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
-                <div class="px-6 pt-12 max-w-[904px] w-full mx-auto flex flex-col flex-1 min-h-0">
+              <Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
+                <div class="relative px-6 pt-12 max-w-2xl w-full mx-auto flex flex-col flex-1 min-h-0">
                   <Show
                     when={local.session.active()}
                     fallback={
@@ -519,9 +520,12 @@ export default function Page() {
                   >
                     {(activeSession) => (
                       <div class="pt-3 flex flex-col flex-1 min-h-0">
-                        <div class="flex items-start gap-8 flex-1 min-h-0">
+                        <div class="flex-1 min-h-0">
                           <Show when={local.session.userMessages().length > 1}>
-                            <ul role="list" class="w-60 shrink-0 flex flex-col items-start gap-1">
+                            <ul
+                              role="list"
+                              class="absolute right-full mr-8 hidden w-60 shrink-0 @7xl:flex flex-col items-start gap-1"
+                            >
                               <For each={local.session.userMessages()}>
                                 {(message) => {
                                   const countLines = (text: string) => {
@@ -648,16 +652,12 @@ export default function Page() {
                                       (m) => m.role === "assistant" && m.parentID == message.id,
                                     ) as AssistantMessageType[]
                                   })
-                                  const working = createMemo(() => {
-                                    const last = assistantMessages()[assistantMessages().length - 1]
-                                    if (!last) return false
-                                    return !last.time.completed
-                                  })
+                                  const working = createMemo(() => !summary())
 
                                   return (
                                     <div
                                       data-message={message.id}
-                                      class="flex flex-col items-start self-stretch gap-8 min-h-[calc(100vh-15rem)]"
+                                      class="flex flex-col items-start self-stretch gap-8 min-h-screen"
                                     >
                                       {/* Title */}
                                       <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
@@ -675,7 +675,9 @@ export default function Page() {
                                         <div class="w-full flex flex-col gap-6 items-start self-stretch">
                                           <div class="flex flex-col items-start gap-1 self-stretch">
                                             <h2 class="text-12-medium text-text-weak">Summary</h2>
-                                            <div class="text-14-regular text-text-base self-stretch">{summary()}</div>
+                                            <Show when={summary()}>
+                                              <Markdown text={summary()!} />
+                                            </Show>
                                           </div>
                                           <Accordion class="w-full" multiple>
                                             <For each={message.summary?.diffs || []}>
@@ -729,14 +731,85 @@ export default function Page() {
                                       <div class="w-full">
                                         <Switch>
                                           <Match when={working()}>
-                                            <div class="w-full flex flex-col-reverse items-start self-stretch gap-6 max-h-30 overflow-y-auto no-scrollbar pointer-events-none mask-alpha mask-y-from-66% mask-y-from-background-base mask-y-to-transparent">
-                                              <For each={assistantMessages()?.toReversed()}>
-                                                {(assistantMessage) => {
-                                                  const parts = createMemo(() => sync.data.part[assistantMessage.id])
-                                                  return <Message message={assistantMessage} parts={parts()} />
-                                                }}
-                                              </For>
-                                            </div>
+                                            {(_) => {
+                                              const items = createMemo(() =>
+                                                assistantMessages().flatMap((m) => sync.data.part[m.id]),
+                                              )
+                                              const finishedItems = createMemo(() =>
+                                                items().filter(
+                                                  (p) =>
+                                                    (p?.type === "text" && p.time?.end) ||
+                                                    (p?.type === "reasoning" && p.time?.end) ||
+                                                    (p?.type === "tool" && p.state.status === "completed"),
+                                                ),
+                                              )
+
+                                              const MINIMUM_DELAY = 800
+                                              const [visibleCount, setVisibleCount] = createSignal(1)
+
+                                              createEffect(() => {
+                                                const total = finishedItems().length
+                                                if (total > visibleCount()) {
+                                                  const timer = setTimeout(() => {
+                                                    setVisibleCount((prev) => prev + 1)
+                                                  }, MINIMUM_DELAY)
+                                                  onCleanup(() => clearTimeout(timer))
+                                                } else if (total < visibleCount()) {
+                                                  setVisibleCount(total)
+                                                }
+                                              })
+
+                                              const translateY = createMemo(() => {
+                                                const total = visibleCount()
+                                                if (total < 2) return "0px"
+                                                return `-${(total - 2) * 48 - 8}px`
+                                              })
+
+                                              return (
+                                                <div class="flex flex-col gap-3">
+                                                  <div
+                                                    class="h-36 overflow-hidden pointer-events-none 
+                                                           mask-alpha mask-y-from-66% mask-y-from-background-base mask-y-to-transparent"
+                                                  >
+                                                    <div
+                                                      class="w-full flex flex-col items-start self-stretch gap-2 py-10
+                                                             transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
+                                                      style={{ transform: `translateY(${translateY()})` }}
+                                                    >
+                                                      <For each={finishedItems()}>
+                                                        {(part) => {
+                                                          const message = createMemo(() =>
+                                                            sync.data.message[part.sessionID].find(
+                                                              (m) => m.id === part.messageID,
+                                                            ),
+                                                          )
+                                                          return (
+                                                            <div class="h-10 flex items-center w-full">
+                                                              <Switch>
+                                                                <Match when={part.type === "text" && part}>
+                                                                  {(p) => (
+                                                                    <div
+                                                                      textContent={p().text}
+                                                                      class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
+                                                                    />
+                                                                  )}
+                                                                </Match>
+                                                                <Match when={part.type === "reasoning" && part}>
+                                                                  {(p) => <Part message={message()!} part={p()} />}
+                                                                </Match>
+                                                                <Match when={part.type === "tool" && part}>
+                                                                  {(p) => <Part message={message()!} part={p()} />}
+                                                                </Match>
+                                                              </Switch>
+                                                            </div>
+                                                          )
+                                                        }}
+                                                      </For>
+                                                    </div>
+                                                  </div>
+                                                </div>
+                                              )
+                                            }}
                                           </Match>
                                           <Match when={!working()}>
                                             <Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
@@ -752,7 +825,7 @@ export default function Page() {
                                                 </div>
                                               </Collapsible.Trigger>
                                               <Collapsible.Content>
-                                                <div class="w-full flex flex-col-reverse items-start self-stretch gap-8">
+                                                <div class="w-full flex flex-col items-start self-stretch gap-8">
                                                   <For each={assistantMessages()}>
                                                     {(assistantMessage) => {
                                                       const parts = createMemo(
@@ -807,14 +880,7 @@ export default function Page() {
               })()}
             </DragOverlay>
           </DragDropProvider>
-          <div
-            classList={{
-              "absolute inset-x-0 px-6 max-w-[904px] flex flex-col justify-center items-center z-50 mx-auto": true,
-              "bottom-8": true,
-              // "bottom-8": !!local.session.active(),
-              // "bottom-1/2 translate-y-1/2": !local.session.active(),
-            }}
-          >
+          <div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
             <PromptInput
               ref={(el) => {
                 inputRef = el

+ 20 - 0
packages/ui/src/styles/tailwind/index.css

@@ -11,6 +11,26 @@
   --spacing: 0.25rem;
   /* --spacing: var(--spacing); */
 
+  --breakpoint-sm: 40rem;
+  --breakpoint-md: 48rem;
+  --breakpoint-lg: 64rem;
+  --breakpoint-xl: 80rem;
+  --breakpoint-2xl: 96rem;
+
+  --container-3xs: 16rem;
+  --container-2xs: 18rem;
+  --container-xs: 20rem;
+  --container-sm: 24rem;
+  --container-md: 28rem;
+  --container-lg: 32rem;
+  --container-xl: 36rem;
+  --container-2xl: 42rem;
+  --container-3xl: 48rem;
+  --container-4xl: 56rem;
+  --container-5xl: 64rem;
+  --container-6xl: 72rem;
+  --container-7xl: 80rem;
+
   --font-sans: var(--font-family-sans);
   --font-sans--font-feature-settings: var(--font-family-sans--font-feature-settings);
   --font-mono: var(--font-family-mono);