|
@@ -18,11 +18,13 @@ import {
|
|
|
IconSparkles,
|
|
IconSparkles,
|
|
|
IconUserCircle,
|
|
IconUserCircle,
|
|
|
IconChevronDown,
|
|
IconChevronDown,
|
|
|
|
|
+ IconCommandLine,
|
|
|
IconChevronRight,
|
|
IconChevronRight,
|
|
|
IconPencilSquare,
|
|
IconPencilSquare,
|
|
|
IconWrenchScrewdriver,
|
|
IconWrenchScrewdriver,
|
|
|
} from "./icons"
|
|
} from "./icons"
|
|
|
import DiffView from "./DiffView"
|
|
import DiffView from "./DiffView"
|
|
|
|
|
+import CodeBlock from "./CodeBlock"
|
|
|
import styles from "./share.module.css"
|
|
import styles from "./share.module.css"
|
|
|
import { type UIMessage } from "ai"
|
|
import { type UIMessage } from "ai"
|
|
|
import { createStore, reconcile } from "solid-js/store"
|
|
import { createStore, reconcile } from "solid-js/store"
|
|
@@ -199,6 +201,70 @@ function TextPart(props: TextPartProps) {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
+ text: string
|
|
|
|
|
+ expand?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+function TerminalPart(props: TerminalPartProps) {
|
|
|
|
|
+ const [local, rest] = splitProps(props, ["text", "expand"])
|
|
|
|
|
+ const [expanded, setExpanded] = createSignal(false)
|
|
|
|
|
+ const [overflowed, setOverflowed] = createSignal(false)
|
|
|
|
|
+ let preEl: HTMLElement | undefined
|
|
|
|
|
+
|
|
|
|
|
+ function checkOverflow() {
|
|
|
|
|
+ if (!preEl) return
|
|
|
|
|
+
|
|
|
|
|
+ const code = preEl.getElementsByTagName("code")[0]
|
|
|
|
|
+
|
|
|
|
|
+ if (code && !local.expand) {
|
|
|
|
|
+ console.log(preEl.clientHeight, code.offsetHeight)
|
|
|
|
|
+ setOverflowed(preEl.clientHeight < code.offsetHeight)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onMount(() => {
|
|
|
|
|
+ checkOverflow()
|
|
|
|
|
+ window.addEventListener("resize", checkOverflow)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ createEffect(() => {
|
|
|
|
|
+ local.text
|
|
|
|
|
+ setTimeout(checkOverflow, 0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ onCleanup(() => {
|
|
|
|
|
+ window.removeEventListener("resize", checkOverflow)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ data-element-message-terminal
|
|
|
|
|
+ data-expanded={expanded() || local.expand === true}
|
|
|
|
|
+ {...rest}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div data-section="body">
|
|
|
|
|
+ <div data-section="header"></div>
|
|
|
|
|
+ <div data-section="content">
|
|
|
|
|
+ <CodeBlock
|
|
|
|
|
+ lang="ansi"
|
|
|
|
|
+ ref={(el) => (preEl = el)}
|
|
|
|
|
+ code={`\x1b[90m>\x1b[0m ${local.text}`}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {overflowed() && (
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ data-element-button-text
|
|
|
|
|
+ onClick={() => setExpanded((e) => !e)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {expanded() ? "Show less" : "Show more"}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function PartFooter(props: { time: number }) {
|
|
function PartFooter(props: { time: number }) {
|
|
|
return (
|
|
return (
|
|
|
<span
|
|
<span
|
|
@@ -478,7 +544,7 @@ export default function Share(props: { api: string }) {
|
|
|
{(part) => (
|
|
{(part) => (
|
|
|
<div data-section="part" data-part-type="user-text">
|
|
<div data-section="part" data-part-type="user-text">
|
|
|
<div data-section="decoration">
|
|
<div data-section="decoration">
|
|
|
- <div>
|
|
|
|
|
|
|
+ <div title="Message">
|
|
|
<IconUserCircle width={18} height={18} />
|
|
<IconUserCircle width={18} height={18} />
|
|
|
</div>
|
|
</div>
|
|
|
<div></div>
|
|
<div></div>
|
|
@@ -505,7 +571,7 @@ export default function Share(props: { api: string }) {
|
|
|
{(part) => (
|
|
{(part) => (
|
|
|
<div data-section="part" data-part-type="ai-text">
|
|
<div data-section="part" data-part-type="ai-text">
|
|
|
<div data-section="decoration">
|
|
<div data-section="decoration">
|
|
|
- <div>
|
|
|
|
|
|
|
+ <div title="AI response">
|
|
|
<IconSparkles width={18} height={18} />
|
|
<IconSparkles width={18} height={18} />
|
|
|
</div>
|
|
</div>
|
|
|
<div></div>
|
|
<div></div>
|
|
@@ -570,7 +636,7 @@ export default function Share(props: { api: string }) {
|
|
|
data-part-type="system-text"
|
|
data-part-type="system-text"
|
|
|
>
|
|
>
|
|
|
<div data-section="decoration">
|
|
<div data-section="decoration">
|
|
|
- <div>
|
|
|
|
|
|
|
+ <div title="System message">
|
|
|
<IconCpuChip width={18} height={18} />
|
|
<IconCpuChip width={18} height={18} />
|
|
|
</div>
|
|
</div>
|
|
|
<div></div>
|
|
<div></div>
|
|
@@ -610,7 +676,7 @@ export default function Share(props: { api: string }) {
|
|
|
data-part-type="tool-edit"
|
|
data-part-type="tool-edit"
|
|
|
>
|
|
>
|
|
|
<div data-section="decoration">
|
|
<div data-section="decoration">
|
|
|
- <div>
|
|
|
|
|
|
|
+ <div title="Edit file">
|
|
|
<IconPencilSquare width={18} height={18} />
|
|
<IconPencilSquare width={18} height={18} />
|
|
|
</div>
|
|
</div>
|
|
|
<div></div>
|
|
<div></div>
|
|
@@ -618,11 +684,12 @@ export default function Share(props: { api: string }) {
|
|
|
<div data-section="content">
|
|
<div data-section="content">
|
|
|
<div data-part-tool-body>
|
|
<div data-part-tool-body>
|
|
|
<span data-part-title data-size="md">
|
|
<span data-part-title data-size="md">
|
|
|
- Edit {filePath}
|
|
|
|
|
|
|
+ <span data-element-label>Edit</span>
|
|
|
|
|
+ <b>{filePath}</b>
|
|
|
</span>
|
|
</span>
|
|
|
<div data-part-tool-edit>
|
|
<div data-part-tool-edit>
|
|
|
<DiffView
|
|
<DiffView
|
|
|
- class={styles["code-block"]}
|
|
|
|
|
|
|
+ class={styles["diff-code-block"]}
|
|
|
changes={metadata()?.changes || []}
|
|
changes={metadata()?.changes || []}
|
|
|
lang={getFileType(filePath)}
|
|
lang={getFileType(filePath)}
|
|
|
/>
|
|
/>
|
|
@@ -634,6 +701,44 @@ export default function Share(props: { api: string }) {
|
|
|
)
|
|
)
|
|
|
}}
|
|
}}
|
|
|
</Match>
|
|
</Match>
|
|
|
|
|
+ {/* Bash tool */}
|
|
|
|
|
+ <Match
|
|
|
|
|
+ when={
|
|
|
|
|
+ msg.role === "assistant" &&
|
|
|
|
|
+ part.type === "tool-invocation" &&
|
|
|
|
|
+ part.toolInvocation.toolName === "opencode_bash" &&
|
|
|
|
|
+ part
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ {(part) => {
|
|
|
|
|
+ const id = part().toolInvocation.toolCallId
|
|
|
|
|
+ const command = part().toolInvocation.args.command
|
|
|
|
|
+ const stdout = msg.metadata?.tool[id]?.stdout
|
|
|
|
|
+ const result = stdout || (part().toolInvocation.state === "result" && part().toolInvocation.result)
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ data-section="part"
|
|
|
|
|
+ data-part-type="tool-edit"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div data-section="decoration">
|
|
|
|
|
+ <div title="Bash command">
|
|
|
|
|
+ <IconCommandLine width={18} height={18} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div data-section="content">
|
|
|
|
|
+ <div data-part-tool-body>
|
|
|
|
|
+ <TerminalPart
|
|
|
|
|
+ data-size="sm"
|
|
|
|
|
+ text={command + (result ? `\n${result}` : "")}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <PartFooter time={time} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ }}
|
|
|
|
|
+ </Match>
|
|
|
{/* Tool call */}
|
|
{/* Tool call */}
|
|
|
<Match
|
|
<Match
|
|
|
when={
|
|
when={
|
|
@@ -648,7 +753,7 @@ export default function Share(props: { api: string }) {
|
|
|
data-part-type="tool-fallback"
|
|
data-part-type="tool-fallback"
|
|
|
>
|
|
>
|
|
|
<div data-section="decoration">
|
|
<div data-section="decoration">
|
|
|
- <div>
|
|
|
|
|
|
|
+ <div title="Tool call">
|
|
|
<IconWrenchScrewdriver
|
|
<IconWrenchScrewdriver
|
|
|
width={18}
|
|
width={18}
|
|
|
height={18}
|
|
height={18}
|