Jay V 8 месяцев назад
Родитель
Сommit
142056e9af
2 измененных файлов с 191 добавлено и 0 удалено
  1. 120 0
      packages/web/src/components/Share.tsx
  2. 71 0
      packages/web/src/components/share.module.css

+ 120 - 0
packages/web/src/components/Share.tsx

@@ -16,6 +16,7 @@ import { IconOpenAI, IconGemini, IconAnthropic } from "./icons/custom"
 import {
   IconCpuChip,
   IconSparkles,
+  IconQueueList,
   IconUserCircle,
   IconChevronDown,
   IconCommandLine,
@@ -75,6 +76,27 @@ type SessionInfo = {
   cost?: number
 }
 
+type TodoStatus = "pending" | "in_progress" | "completed"
+
+interface Todo {
+  id: string
+  content: string
+  status: TodoStatus
+  priority: "low" | "medium" | "high"
+}
+
+function sortTodosByStatus(todos: Todo[]) {
+  const statusPriority: Record<TodoStatus, number> = {
+    in_progress: 0,
+    pending: 1,
+    completed: 2,
+  }
+
+  return todos
+    .slice()
+    .sort((a, b) => statusPriority[a.status] - statusPriority[b.status])
+}
+
 function getFileType(path: string) {
   return path.split(".").pop()
 }
@@ -1163,6 +1185,104 @@ export default function Share(props: { api: string }) {
                             )
                           }}
                         </Match>
+                        {/* Todo read */}
+                        <Match
+                          when={
+                            msg.role === "assistant" &&
+                            part.type === "tool-invocation" &&
+                            part.toolInvocation.toolName === "opencode_todoread" &&
+                            part
+                          }
+                        >
+                          {(part) => {
+                            const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
+
+                            const duration = createMemo(() =>
+                              DateTime.fromMillis(metadata()?.time.end || 0).diff(
+                                DateTime.fromMillis(metadata()?.time.start || 0),
+                              ).toMillis(),
+                            )
+
+                            return (
+                              <div
+                                data-section="part"
+                                data-part-type="tool-fallback"
+                              >
+                                <div data-section="decoration">
+                                  <div title="Plan">
+                                    <IconQueueList width={18} height={18} />
+                                  </div>
+                                  <div></div>
+                                </div>
+                                <div data-section="content">
+                                  <div data-part-tool-body>
+                                    <span data-part-title data-size="sm">
+                                      Checking plan&hellip;
+                                    </span>
+                                  </div>
+                                  <ToolFooter time={duration()} />
+                                </div>
+                              </div>
+                            )
+                          }}
+                        </Match>
+                        {/* Todo write */}
+                        <Match
+                          when={
+                            msg.role === "assistant" &&
+                            part.type === "tool-invocation" &&
+                            part.toolInvocation.toolName === "opencode_todowrite" &&
+                            part
+                          }
+                        >
+                          {(part) => {
+                            const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
+
+                            const todos = createMemo(() => sortTodosByStatus(
+                              part().toolInvocation.args.todos
+                            ))
+
+                            const duration = createMemo(() =>
+                              DateTime.fromMillis(metadata()?.time.end || 0).diff(
+                                DateTime.fromMillis(metadata()?.time.start || 0),
+                              ).toMillis(),
+                            )
+
+                            return (
+                              <div
+                                data-section="part"
+                                data-part-type="tool-fallback"
+                              >
+                                <div data-section="decoration">
+                                  <div title="Plan">
+                                    <IconQueueList width={18} height={18} />
+                                  </div>
+                                  <div></div>
+                                </div>
+                                <div data-section="content">
+                                  <div data-part-tool-body>
+                                    <span data-part-title data-size="sm">
+                                      Planning&hellip;
+                                    </span>
+                                    <Show when={todos().length > 0}>
+                                      <ul class={styles.todos}>
+                                        <For each={todos()}>
+                                          {({ status, content }) =>
+                                            <li data-status={status}>
+                                              <span></span>
+                                              {content}
+                                            </li>
+                                          }
+                                        </For>
+                                      </ul>
+                                    </Show>
+                                  </div>
+                                  <ToolFooter time={duration()} />
+                                </div>
+                              </div>
+                            )
+                          }}
+                        </Match>
                         {/* Tool call */}
                         <Match
                           when={

+ 71 - 0
packages/web/src/components/share.module.css

@@ -535,3 +535,74 @@
     font-size: 0.75rem;
   }
 }
+
+.todos {
+  list-style-type: none;
+  padding: 0;
+  margin: 0;
+  border: 1px solid var(--sl-color-divider);
+  border-radius: 0.25rem;
+
+  li {
+    margin: 0;
+    position: relative;
+    padding-left: 1.5rem;
+    font-size: 0.75rem;
+    padding: 0.375rem 0.625rem 0.375rem 1.75rem;
+    border-bottom: 1px solid var(--sl-color-divider);
+    line-height: 1.5;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    & > span {
+      position: absolute;
+      display: inline-block;
+      left: 0.5rem;
+      top: calc(0.5rem + 1px);
+      width: 0.75rem;
+      height: 0.75rem;
+      border: 1px solid var(--sl-color-divider);
+      border-radius: 0.15rem;
+
+      &::before {
+      }
+    }
+
+    &[data-status="pending"] {
+      color: var(--sl-color-text);
+    }
+    &[data-status="in_progress"] {
+      color: var(--sl-color-text);
+
+      & > span { border-color: var(--sl-color-orange); }
+      & > span::before {
+        content: "";
+        position: absolute;
+        top: 2px;
+        left: 2px;
+        width: calc(0.75rem - 2px - 4px);
+        height: calc(0.75rem - 2px - 4px);
+        box-shadow: inset 1rem 1rem var(--sl-color-orange-low);
+      }
+    }
+    &[data-status="completed"] {
+      color: var(--sl-color-text-dimmed);
+
+      & > span { border-color: var(--sl-color-hairline); }
+      & > span::before {
+        content: "";
+        position: absolute;
+        top: 2px;
+        left: 2px;
+        width: calc(0.75rem - 2px - 4px);
+        height: calc(0.75rem - 2px - 4px);
+        box-shadow: inset 1rem 1rem var(--sl-color-divider);
+
+        transform-origin: bottom left;
+        clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
+      }
+    }
+  }
+}