diff options
| author | Jay V <[email protected]> | 2025-06-03 18:08:46 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-06-03 18:08:46 -0400 |
| commit | 2fb59fee8ed30675ffb80a2cdf1b3e266b5b8bf7 (patch) | |
| tree | 15b5b25045c0265b527c5075651f979fdfd508f5 /packages | |
| parent | 8206da4d9ef5871ebf1cf56f9231eba5b7036fc3 (diff) | |
| download | opencode-2fb59fee8ed30675ffb80a2cdf1b3e266b5b8bf7.tar.gz opencode-2fb59fee8ed30675ffb80a2cdf1b3e266b5b8bf7.zip | |
share paage durations
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/web/src/components/Share.tsx | 214 | ||||
| -rw-r--r-- | packages/web/src/components/share.module.css | 11 |
2 files changed, 136 insertions, 89 deletions
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index e0a73edc7..e3efd6630 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -29,6 +29,8 @@ import styles from "./share.module.css" import { type UIMessage } from "ai" import { createStore, reconcile } from "solid-js/store" +const MIN_DURATION = 2 + type Status = | "disconnected" | "connecting" @@ -73,6 +75,23 @@ function getFileType(path: string) { return path.split(".").pop() } +function formatDuration(ms: number): string { + const ONE_SECOND = 1000 + const ONE_MINUTE = 60 * ONE_SECOND + + if (ms >= ONE_MINUTE) { + const minutes = Math.floor(ms / ONE_MINUTE) + return minutes === 1 ? `1min` : `${minutes}mins` + } + + if (ms >= ONE_SECOND) { + const seconds = Math.floor(ms / ONE_SECOND) + return `${seconds}s` + } + + return `${ms}ms` +} + // Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]` function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { const entries: Array<[string, any]> = [] @@ -260,18 +279,13 @@ function TerminalPart(props: TerminalPartProps) { ) } -function PartFooter(props: { time: number }) { +function ToolFooter(props: { time: number }) { return ( - <span - data-part-footer - title={DateTime.fromMillis(props.time).toLocaleString( - DateTime.DATETIME_FULL_WITH_SECONDS, - )} - > - {DateTime.fromMillis(props.time).toLocaleString( - DateTime.TIME_WITH_SECONDS, - )} - </span> + props.time > MIN_DURATION + ? <span data-part-footer title={`${props.time}ms`}> + {formatDuration(props.time)} + </span> + : <div data-part-footer="spacer"></div> ) } @@ -550,7 +564,6 @@ export default function Share(props: { api: string }) { text={part().text} expand={isLastPart()} /> - <PartFooter time={time} /> </div> </div> )} @@ -576,7 +589,6 @@ export default function Share(props: { api: string }) { text={part().text} expand={isLastPart()} /> - <PartFooter time={time} /> </div> </div> )} @@ -647,7 +659,6 @@ export default function Share(props: { api: string }) { data-color="dimmed" /> </div> - <PartFooter time={time} /> </div> </div> )} @@ -665,6 +676,13 @@ export default function Share(props: { api: string }) { const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId]) const args = part().toolInvocation.args const filePath = args.filePath + + const duration = createMemo(() => + DateTime.fromMillis(metadata()?.time.end || 0).diff( + DateTime.fromMillis(metadata()?.time.start || 0), + ).toMillis(), + ) + return ( <div data-section="part" @@ -690,7 +708,7 @@ export default function Share(props: { api: string }) { /> </div> </div> - <PartFooter time={time} /> + <ToolFooter time={duration()} /> </div> </div> ) @@ -706,14 +724,23 @@ export default function Share(props: { api: string }) { } > {(part) => { + const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId]) + const id = part().toolInvocation.toolCallId const command = part().toolInvocation.args.command - const stdout = msg.metadata?.tool[id]?.stdout + const stdout = metadata()?.stdout const result = stdout || (part().toolInvocation.state === "result" && part().toolInvocation.result) + + 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-edit" + data-part-type="tool-bash" > <div data-section="decoration"> <div title="Bash command"> @@ -728,7 +755,7 @@ export default function Share(props: { api: string }) { text={command + (result ? `\n${result}` : "")} /> </div> - <PartFooter time={time} /> + <ToolFooter time={duration()} /> </div> </div> ) @@ -742,80 +769,90 @@ export default function Share(props: { api: string }) { part } > - {(part) => ( - <div - data-section="part" - data-part-type="tool-fallback" - > - <div data-section="decoration"> - <div title="Tool call"> - <IconWrenchScrewdriver - width={18} - height={18} - /> - </div> - <div></div> - </div> - <div data-section="content"> - <div data-part-tool-body> - <span data-part-title data-size="md"> - {part().toolInvocation.toolName} - </span> - <div data-part-tool-args> - <For - each={flattenToolArgs( - part().toolInvocation.args, - )} - > - {([name, value]) => ( - <> - <div></div> - <div>{name}</div> - <div>{value}</div> - </> - )} - </For> + {(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="Tool call"> + <IconWrenchScrewdriver + width={18} + height={18} + /> </div> - <Switch> - <Match - when={ - part().toolInvocation.state === - "result" && - part().toolInvocation.result - } - > - <div data-part-tool-result> - <ResultsButton - results={results()} - onClick={() => showResults((e) => !e)} - /> - <Show when={results()}> - <TextPart - expand - data-size="sm" - data-color="dimmed" - text={part().toolInvocation.result} + <div></div> + </div> + <div data-section="content"> + <div data-part-tool-body> + <span data-part-title data-size="md"> + {part().toolInvocation.toolName} + </span> + <div data-part-tool-args> + <For + each={flattenToolArgs( + part().toolInvocation.args, + )} + > + {([name, value]) => ( + <> + <div></div> + <div>{name}</div> + <div>{value}</div> + </> + )} + </For> + </div> + <Switch> + <Match + when={ + part().toolInvocation.state === + "result" && + part().toolInvocation.result + } + > + <div data-part-tool-result> + <ResultsButton + results={results()} + onClick={() => showResults((e) => !e)} /> - </Show> - </div> - </Match> - <Match - when={ - part().toolInvocation.state === "call" - } - > - <TextPart - data-size="sm" - data-color="dimmed" - text="Calling..." - /> - </Match> - </Switch> + <Show when={results()}> + <TextPart + expand + data-size="sm" + data-color="dimmed" + text={part().toolInvocation.result} + /> + </Show> + </div> + </Match> + <Match + when={ + part().toolInvocation.state === "call" + } + > + <TextPart + data-size="sm" + data-color="dimmed" + text="Calling..." + /> + </Match> + </Switch> + </div> + <ToolFooter time={duration()} /> </div> - <PartFooter time={time} /> </div> - </div> - )} + ) + }} </Match> {/* Fallback */} <Match when={true}> @@ -857,7 +894,6 @@ export default function Share(props: { api: string }) { text={JSON.stringify(part, null, 2)} /> </div> - <PartFooter time={time} /> </div> </div> </Match> diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css index d8c0ef7da..a393de49b 100644 --- a/packages/web/src/components/share.module.css +++ b/packages/web/src/components/share.module.css @@ -272,6 +272,17 @@ } } + /* Part types */ + [data-part-type="user-text"], + [data-part-type="ai-text"], + [data-part-type="ai-model"], + [data-part-type="system-text"], + [data-part-type="fallback"] { + & > [data-section="content"] { + padding-bottom: 1rem; + } + } + [data-part-type="tool-edit"] { [data-part-tool-body] { gap: 0.5rem; |
