summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/web/src/components/CodeBlock.tsx3
-rw-r--r--packages/web/src/components/Share.tsx119
-rw-r--r--packages/web/src/components/codeblock.module.css7
-rw-r--r--packages/web/src/components/diffview.module.css17
-rw-r--r--packages/web/src/components/share.module.css119
5 files changed, 241 insertions, 24 deletions
diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx
index f410ab070..b3a0d3f2e 100644
--- a/packages/web/src/components/CodeBlock.tsx
+++ b/packages/web/src/components/CodeBlock.tsx
@@ -6,6 +6,7 @@ import {
createResource,
} from "solid-js"
import { codeToHtml } from "shiki"
+import styles from "./codeblock.module.css"
import { transformerNotationDiff } from "@shikijs/transformers"
interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
@@ -37,7 +38,7 @@ function CodeBlock(props: CodeBlockProps) {
}
})
- return <div ref={containerRef} {...rest}></div>
+ return <div ref={containerRef} class={styles.codeblock} {...rest}></div>
}
export default CodeBlock
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index 6d418b82d..d572dcdd5 100644
--- a/packages/web/src/components/Share.tsx
+++ b/packages/web/src/components/Share.tsx
@@ -18,11 +18,13 @@ import {
IconSparkles,
IconUserCircle,
IconChevronDown,
+ IconCommandLine,
IconChevronRight,
IconPencilSquare,
IconWrenchScrewdriver,
} from "./icons"
import DiffView from "./DiffView"
+import CodeBlock from "./CodeBlock"
import styles from "./share.module.css"
import { type UIMessage } from "ai"
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 }) {
return (
<span
@@ -478,7 +544,7 @@ export default function Share(props: { api: string }) {
{(part) => (
<div data-section="part" data-part-type="user-text">
<div data-section="decoration">
- <div>
+ <div title="Message">
<IconUserCircle width={18} height={18} />
</div>
<div></div>
@@ -505,7 +571,7 @@ export default function Share(props: { api: string }) {
{(part) => (
<div data-section="part" data-part-type="ai-text">
<div data-section="decoration">
- <div>
+ <div title="AI response">
<IconSparkles width={18} height={18} />
</div>
<div></div>
@@ -570,7 +636,7 @@ export default function Share(props: { api: string }) {
data-part-type="system-text"
>
<div data-section="decoration">
- <div>
+ <div title="System message">
<IconCpuChip width={18} height={18} />
</div>
<div></div>
@@ -610,7 +676,7 @@ export default function Share(props: { api: string }) {
data-part-type="tool-edit"
>
<div data-section="decoration">
- <div>
+ <div title="Edit file">
<IconPencilSquare width={18} height={18} />
</div>
<div></div>
@@ -618,11 +684,12 @@ export default function Share(props: { api: string }) {
<div data-section="content">
<div data-part-tool-body>
<span data-part-title data-size="md">
- Edit {filePath}
+ <span data-element-label>Edit</span>
+ <b>{filePath}</b>
</span>
<div data-part-tool-edit>
<DiffView
- class={styles["code-block"]}
+ class={styles["diff-code-block"]}
changes={metadata()?.changes || []}
lang={getFileType(filePath)}
/>
@@ -634,6 +701,44 @@ export default function Share(props: { api: string }) {
)
}}
</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 */}
<Match
when={
@@ -648,7 +753,7 @@ export default function Share(props: { api: string }) {
data-part-type="tool-fallback"
>
<div data-section="decoration">
- <div>
+ <div title="Tool call">
<IconWrenchScrewdriver
width={18}
height={18}
diff --git a/packages/web/src/components/codeblock.module.css b/packages/web/src/components/codeblock.module.css
new file mode 100644
index 000000000..089490fb2
--- /dev/null
+++ b/packages/web/src/components/codeblock.module.css
@@ -0,0 +1,7 @@
+.codeblock {
+ pre {
+ --shiki-dark-bg: var(--sl-color-bg) !important;
+ background-color: var(--sl-color-bg) !important;
+ }
+}
+
diff --git a/packages/web/src/components/diffview.module.css b/packages/web/src/components/diffview.module.css
index e92bd61fc..1faef042e 100644
--- a/packages/web/src/components/diffview.module.css
+++ b/packages/web/src/components/diffview.module.css
@@ -9,9 +9,10 @@
.column {
display: flex;
flex-direction: column;
- overflow-x: auto;
+
+ overflow-x: visible;
min-width: 0;
- align-items: flex-start;
+ align-items: stretch;
&:first-child {
border-right: 1px solid var(--sl-color-divider);
@@ -28,13 +29,17 @@
[data-section="cell"] {
position: relative;
flex: none;
- width: max-content;
+
+ width: 100%;
padding: 0.1875rem 0.5rem 0.1875rem 1.8ch;
margin: 0;
pre {
+ --shiki-dark-bg: var(--sl-color-bg-surface) !important;
background-color: var(--sl-color-bg-surface) !important;
- white-space: pre;
+
+ white-space: pre-wrap;
+ word-break: break-word;
code > span:empty::before {
content: "\00a0";
@@ -47,9 +52,9 @@
[data-diff-type="removed"] {
background-color: var(--sl-color-red-low);
- min-width: 100%;
pre {
+ --shiki-dark-bg: var(--sl-color-red-low) !important;
background-color: var(--sl-color-red-low) !important;
}
@@ -64,9 +69,9 @@
[data-diff-type="added"] {
background-color: var(--sl-color-green-low);
- min-width: 100%;
pre {
+ --shiki-dark-bg: var(--sl-color-green-low) !important;
background-color: var(--sl-color-green-low) !important;
}
diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css
index 2f70cd12b..06273a114 100644
--- a/packages/web/src/components/share.module.css
+++ b/packages/web/src/components/share.module.css
@@ -4,6 +4,8 @@
flex-direction: column;
gap: 2.5rem;
line-height: 1;
+
+ --term-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2060%2016'%20preserveAspectRatio%3D'xMidYMid%20meet'%3E%3Ccircle%20cx%3D'8'%20cy%3D'8'%20r%3D'8'%2F%3E%3Ccircle%20cx%3D'30'%20cy%3D'8'%20r%3D'8'%2F%3E%3Ccircle%20cx%3D'52'%20cy%3D'8'%20r%3D'8'%2F%3E%3C%2Fsvg%3E");
}
[data-element-button-text] {
@@ -61,6 +63,14 @@
display: flex;
align-items: center;
justify-content: space-between;
+ gap: 1rem;
+
+ h1 {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ }
}
[data-section="row"] {
@@ -107,7 +117,8 @@
padding: 0;
margin: 0;
display: flex;
- gap: 1rem;
+ gap: 0.5rem 1rem;
+ flex-wrap: wrap;
li {
display: flex;
@@ -173,6 +184,7 @@
div:first-child {
flex: 0 0 auto;
width: 18px;
+ opacity: 0.65;
svg {
color: var(--sl-color-text-secondary);
display: block;
@@ -203,6 +215,10 @@
line-height: 18px;
font-size: 0.75rem;
+ b {
+ font-weight: 500;
+ }
+
&[data-size="md"] {
font-size: 0.875rem;
}
@@ -258,6 +274,21 @@
}
}
}
+
+ [data-part-type="tool-edit"] {
+ [data-part-tool-body] {
+ gap: 0.5rem;
+ }
+ [data-part-title] {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+
+ b {
+ color: var(--sl-color-text);
+ }
+ }
+ }
}
[data-element-message-text] {
@@ -269,14 +300,6 @@
align-items: flex-start;
gap: 1rem;
- pre {
- line-height: 1.5;
- font-size: 0.875rem;
- white-space: pre-wrap;
- overflow-wrap: anywhere;
- color: var(--sl-color-text);
- }
-
&[data-size="sm"] {
pre {
font-size: 0.75rem;
@@ -289,6 +312,14 @@
}
}
+ pre {
+ line-height: 1.5;
+ font-size: 0.875rem;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ color: var(--sl-color-text);
+ }
+
button {
flex: 0 0 auto;
padding: 2px 0;
@@ -327,7 +358,75 @@
}
}
-.code-block {
+[data-element-message-terminal] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+
+ [data-section="body"] {
+ border: 1px solid var(--sl-color-divider);
+ border-radius: 0.25rem;
+
+ [data-section="header"] {
+ position: relative;
+ border-bottom: 1px solid var(--sl-color-divider);
+ width: 100%;
+ height: 25px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ top: 8px;
+ left: 10px;
+ width: 2rem;
+ height: 0.5rem;
+ line-height: 0;
+ background-color: var(--sl-color-hairline);
+ mask-image: var(--term-icon);
+ mask-repeat: no-repeat;
+ }
+ }
+ }
+
+ [data-section="content"] {
+ padding: 0.5rem calc(0.5rem + 3px);
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+
+ pre {
+ line-height: 1.6;
+ font-size: 0.75rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+ }
+
+ &[data-expanded="true"] {
+ pre {
+ display: block;
+ }
+ }
+ &[data-expanded="false"] {
+ pre {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 7;
+ overflow: hidden;
+ }
+ }
+
+ button {
+ flex: 0 0 auto;
+ padding-left: 1px;
+ font-size: 0.75rem;
+ }
+}
+
+.diff-code-block {
pre {
line-height: 1.25;
font-size: 0.75rem;