summaryrefslogtreecommitdiffhomepage
path: root/packages/web
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-06-05 13:52:39 -0400
committerJay V <[email protected]>2025-06-05 13:52:44 -0400
commit142056e9afa6913a44e65bf109eac0c857b49b02 (patch)
treef893d98b071fa6650773f6fc59ee62bca52256d0 /packages/web
parent241c366164496f1961a86ae02d2b51e9a9453b12 (diff)
downloadopencode-142056e9afa6913a44e65bf109eac0c857b49b02.tar.gz
opencode-142056e9afa6913a44e65bf109eac0c857b49b02.zip
share page todos
Diffstat (limited to 'packages/web')
-rw-r--r--packages/web/src/components/Share.tsx120
-rw-r--r--packages/web/src/components/share.module.css71
2 files changed, 191 insertions, 0 deletions
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index b6bd7fd0d..9831504c8 100644
--- a/packages/web/src/components/Share.tsx
+++ b/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={
diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css
index ef4507340..895a38a95 100644
--- a/packages/web/src/components/share.module.css
+++ b/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%);
+ }
+ }
+ }
+}