summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-22 11:36:00 -0600
committerAdam <[email protected]>2026-02-22 11:36:00 -0600
commitaaf8317c8290c838212042c96bd0f88c5a2540d9 (patch)
tree154bed995b8c2af47844c8c37506f9514fd5b273 /packages/ui/src
parente70d2b27de3aaed5a19b9ca2c6749ed7fce3ef93 (diff)
downloadopencode-aaf8317c8290c838212042c96bd0f88c5a2540d9.tar.gz
opencode-aaf8317c8290c838212042c96bd0f88c5a2540d9.zip
feat(app): feed customization options
Diffstat (limited to 'packages/ui/src')
-rw-r--r--packages/ui/src/components/message-part.css28
-rw-r--r--packages/ui/src/components/message-part.tsx322
-rw-r--r--packages/ui/src/components/session-turn.tsx4
3 files changed, 221 insertions, 133 deletions
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index 07a718141..ce76d8e18 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -332,14 +332,6 @@
}
}
-[data-slot="collapsible-content"]:has([data-component="edit-content"]),
-[data-slot="collapsible-content"]:has([data-component="write-content"]) {
- border: 1px solid var(--border-weak-base);
- border-radius: 6px;
- background: transparent;
- overflow: hidden;
-}
-
[data-component="bash-output"] {
width: 100%;
border: 1px solid var(--border-weak-base);
@@ -399,11 +391,6 @@
}
}
-[data-slot="collapsible-content"]:has([data-component="edit-content"]) [data-component="edit-content"],
-[data-slot="collapsible-content"]:has([data-component="write-content"]) [data-component="write-content"] {
- border-top: none;
-}
-
[data-component="edit-trigger"],
[data-component="write-trigger"] {
display: flex;
@@ -492,9 +479,8 @@
[data-component="edit-content"] {
border-radius: inherit;
border-top: 1px solid var(--border-weaker-base);
- max-height: 420px;
overflow-x: hidden;
- overflow-y: auto;
+ overflow-y: visible;
scrollbar-width: none;
-ms-overflow-style: none;
@@ -512,9 +498,8 @@
[data-component="write-content"] {
border-radius: inherit;
border-top: 1px solid var(--border-weaker-base);
- max-height: 240px;
overflow-x: hidden;
- overflow-y: auto;
+ overflow-y: visible;
[data-component="code"] {
padding-bottom: 0 !important;
@@ -1212,11 +1197,18 @@
}
}
+[data-component="edit-tool"],
+[data-component="write-tool"],
[data-component="apply-patch-tool"] {
> [data-component="collapsible"].tool-collapsible {
gap: 0px;
}
+ > [data-component="collapsible"] > [data-slot="collapsible-content"] {
+ border: none;
+ background: transparent;
+ }
+
> [data-component="collapsible"] > [data-slot="collapsible-trigger"][aria-expanded="true"] {
position: sticky;
top: var(--sticky-accordion-top, 0px);
@@ -1298,7 +1290,7 @@
[data-component="apply-patch-file-diff"] {
border-radius: inherit;
overflow-x: hidden;
- overflow-y: auto;
+ overflow-y: visible;
scrollbar-width: none;
-ms-overflow-style: none;
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 828ddbe87..adba42ce9 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -276,12 +276,24 @@ function renderable(part: PartType, showReasoningSummaries = true) {
return !!PART_MAPPING[part.type]
}
+function toolDefaultOpen(tool: string, shell = false, edit = false) {
+ if (tool === "bash") return shell
+ if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit
+}
+
+function partDefaultOpen(part: PartType, shell = false, edit = false) {
+ if (part.type !== "tool") return
+ return toolDefaultOpen(part.tool, shell, edit)
+}
+
export function AssistantParts(props: {
messages: AssistantMessage[]
showAssistantCopyPartID?: string | null
turnDurationMs?: number
working?: boolean
showReasoningSummaries?: boolean
+ shellToolDefaultOpen?: boolean
+ editToolDefaultOpen?: boolean
}) {
const data = useData()
const emptyParts: PartType[] = []
@@ -372,6 +384,7 @@ export function AssistantParts(props: {
message={entry().message}
showAssistantCopyPartID={props.showAssistantCopyPartID}
turnDurationMs={props.turnDurationMs}
+ defaultOpen={partDefaultOpen(entry().part, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
/>
)}
</Show>
@@ -900,6 +913,42 @@ export const ToolRegistry = {
render: getTool,
}
+function ToolFileAccordion(props: { path: string; actions?: JSX.Element; children: JSX.Element }) {
+ const value = createMemo(() => props.path || "tool-file")
+
+ return (
+ <Accordion
+ multiple
+ data-scope="apply-patch"
+ style={{ "--sticky-accordion-offset": "40px" }}
+ defaultValue={[value()]}
+ >
+ <Accordion.Item value={value()}>
+ <StickyAccordionHeader>
+ <Accordion.Trigger>
+ <div data-slot="apply-patch-trigger-content">
+ <div data-slot="apply-patch-file-info">
+ <FileIcon node={{ path: props.path, type: "file" }} />
+ <div data-slot="apply-patch-file-name-container">
+ <Show when={props.path.includes("/")}>
+ <span data-slot="apply-patch-directory">{`\u202A${getDirectory(props.path)}\u202C`}</span>
+ </Show>
+ <span data-slot="apply-patch-filename">{getFilename(props.path)}</span>
+ </div>
+ </div>
+ <div data-slot="apply-patch-trigger-actions">
+ {props.actions}
+ <Icon name="chevron-grabber-vertical" size="small" />
+ </div>
+ </div>
+ </Accordion.Trigger>
+ </StickyAccordionHeader>
+ <Accordion.Content>{props.children}</Accordion.Content>
+ </Accordion.Item>
+ </Accordion>
+ )
+}
+
PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const data = useData()
const i18n = useI18n()
@@ -1479,57 +1528,67 @@ ToolRegistry.register({
const i18n = useI18n()
const diffComponent = useDiffComponent()
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
+ const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "")
const filename = () => getFilename(props.input.filePath ?? "")
const pending = () => props.status === "pending" || props.status === "running"
return (
- <BasicTool
- {...props}
- icon="code-lines"
- defer
- trigger={
- <div data-component="edit-trigger">
- <div data-slot="message-part-title-area">
- <div data-slot="message-part-title">
- <span data-slot="message-part-title-text">
- <Show when={pending()} fallback={i18n.t("ui.messagePart.title.edit")}>
- <TextShimmer text={i18n.t("ui.messagePart.title.edit")} />
+ <div data-component="edit-tool">
+ <BasicTool
+ {...props}
+ icon="code-lines"
+ defer
+ trigger={
+ <div data-component="edit-trigger">
+ <div data-slot="message-part-title-area">
+ <div data-slot="message-part-title">
+ <span data-slot="message-part-title-text">
+ <Show when={pending()} fallback={i18n.t("ui.messagePart.title.edit")}>
+ <TextShimmer text={i18n.t("ui.messagePart.title.edit")} />
+ </Show>
+ </span>
+ <Show when={!pending()}>
+ <span data-slot="message-part-title-filename">{filename()}</span>
</Show>
- </span>
- <Show when={!pending()}>
- <span data-slot="message-part-title-filename">{filename()}</span>
+ </div>
+ <Show when={!pending() && props.input.filePath?.includes("/")}>
+ <div data-slot="message-part-path">
+ <span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
+ </div>
+ </Show>
+ </div>
+ <div data-slot="message-part-actions">
+ <Show when={!pending() && props.metadata.filediff}>
+ <DiffChanges changes={props.metadata.filediff} />
</Show>
</div>
- <Show when={!pending() && props.input.filePath?.includes("/")}>
- <div data-slot="message-part-path">
- <span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
- </div>
- </Show>
- </div>
- <div data-slot="message-part-actions">
- <Show when={!pending() && props.metadata.filediff}>
- <DiffChanges changes={props.metadata.filediff} />
- </Show>
</div>
- </div>
- }
- >
- <Show when={props.metadata.filediff?.path || props.input.filePath}>
- <div data-component="edit-content">
- <Dynamic
- component={diffComponent}
- before={{
- name: props.metadata?.filediff?.file || props.input.filePath,
- contents: props.metadata?.filediff?.before || props.input.oldString,
- }}
- after={{
- name: props.metadata?.filediff?.file || props.input.filePath,
- contents: props.metadata?.filediff?.after || props.input.newString,
- }}
- />
- </div>
- </Show>
- <DiagnosticsDisplay diagnostics={diagnostics()} />
- </BasicTool>
+ }
+ >
+ <Show when={path()}>
+ <ToolFileAccordion
+ path={path()}
+ actions={
+ <Show when={!pending() && props.metadata.filediff}>{(diff) => <DiffChanges changes={diff()} />}</Show>
+ }
+ >
+ <div data-component="edit-content">
+ <Dynamic
+ component={diffComponent}
+ before={{
+ name: props.metadata?.filediff?.file || props.input.filePath,
+ contents: props.metadata?.filediff?.before || props.input.oldString,
+ }}
+ after={{
+ name: props.metadata?.filediff?.file || props.input.filePath,
+ contents: props.metadata?.filediff?.after || props.input.newString,
+ }}
+ />
+ </div>
+ </ToolFileAccordion>
+ </Show>
+ <DiagnosticsDisplay diagnostics={diagnostics()} />
+ </BasicTool>
+ </div>
)
},
})
@@ -1540,51 +1599,56 @@ ToolRegistry.register({
const i18n = useI18n()
const codeComponent = useCodeComponent()
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
+ const path = createMemo(() => props.input.filePath || "")
const filename = () => getFilename(props.input.filePath ?? "")
const pending = () => props.status === "pending" || props.status === "running"
return (
- <BasicTool
- {...props}
- icon="code-lines"
- defer
- trigger={
- <div data-component="write-trigger">
- <div data-slot="message-part-title-area">
- <div data-slot="message-part-title">
- <span data-slot="message-part-title-text">
- <Show when={pending()} fallback={i18n.t("ui.messagePart.title.write")}>
- <TextShimmer text={i18n.t("ui.messagePart.title.write")} />
+ <div data-component="write-tool">
+ <BasicTool
+ {...props}
+ icon="code-lines"
+ defer
+ trigger={
+ <div data-component="write-trigger">
+ <div data-slot="message-part-title-area">
+ <div data-slot="message-part-title">
+ <span data-slot="message-part-title-text">
+ <Show when={pending()} fallback={i18n.t("ui.messagePart.title.write")}>
+ <TextShimmer text={i18n.t("ui.messagePart.title.write")} />
+ </Show>
+ </span>
+ <Show when={!pending()}>
+ <span data-slot="message-part-title-filename">{filename()}</span>
</Show>
- </span>
- <Show when={!pending()}>
- <span data-slot="message-part-title-filename">{filename()}</span>
+ </div>
+ <Show when={!pending() && props.input.filePath?.includes("/")}>
+ <div data-slot="message-part-path">
+ <span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
+ </div>
</Show>
</div>
- <Show when={!pending() && props.input.filePath?.includes("/")}>
- <div data-slot="message-part-path">
- <span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
- </div>
- </Show>
+ <div data-slot="message-part-actions">{/* <DiffChanges diff={diff} /> */}</div>
</div>
- <div data-slot="message-part-actions">{/* <DiffChanges diff={diff} /> */}</div>
- </div>
- }
- >
- <Show when={props.input.content}>
- <div data-component="write-content">
- <Dynamic
- component={codeComponent}
- file={{
- name: props.input.filePath,
- contents: props.input.content,
- cacheKey: checksum(props.input.content),
- }}
- overflow="scroll"
- />
- </div>
- </Show>
- <DiagnosticsDisplay diagnostics={diagnostics()} />
- </BasicTool>
+ }
+ >
+ <Show when={props.input.content && path()}>
+ <ToolFileAccordion path={path()}>
+ <div data-component="write-content">
+ <Dynamic
+ component={codeComponent}
+ file={{
+ name: props.input.filePath,
+ contents: props.input.content,
+ cacheKey: checksum(props.input.content),
+ }}
+ overflow="scroll"
+ />
+ </div>
+ </ToolFileAccordion>
+ </Show>
+ <DiagnosticsDisplay diagnostics={diagnostics()} />
+ </BasicTool>
+ </div>
)
},
})
@@ -1731,45 +1795,73 @@ ToolRegistry.register({
}
>
{(file) => (
- <BasicTool
- {...props}
- icon="code-lines"
- defer
- trigger={
- <div data-component="edit-trigger">
- <div data-slot="message-part-title-area">
- <div data-slot="message-part-title">
- <span data-slot="message-part-title-text">
- <Show when={pending()} fallback={i18n.t("ui.tool.patch")}>
- <TextShimmer text={i18n.t("ui.tool.patch")} />
+ <div data-component="apply-patch-tool">
+ <BasicTool
+ {...props}
+ icon="code-lines"
+ defer
+ trigger={
+ <div data-component="edit-trigger">
+ <div data-slot="message-part-title-area">
+ <div data-slot="message-part-title">
+ <span data-slot="message-part-title-text">
+ <Show when={pending()} fallback={i18n.t("ui.tool.patch")}>
+ <TextShimmer text={i18n.t("ui.tool.patch")} />
+ </Show>
+ </span>
+ <Show when={!pending()}>
+ <span data-slot="message-part-title-filename">{getFilename(file().relativePath)}</span>
</Show>
- </span>
+ </div>
+ <Show when={!pending() && file().relativePath.includes("/")}>
+ <div data-slot="message-part-path">
+ <span data-slot="message-part-directory">{getDirectory(file().relativePath)}</span>
+ </div>
+ </Show>
+ </div>
+ <div data-slot="message-part-actions">
<Show when={!pending()}>
- <span data-slot="message-part-title-filename">{getFilename(file().relativePath)}</span>
+ <DiffChanges changes={{ additions: file().additions, deletions: file().deletions }} />
</Show>
</div>
- <Show when={!pending() && file().relativePath.includes("/")}>
- <div data-slot="message-part-path">
- <span data-slot="message-part-directory">{getDirectory(file().relativePath)}</span>
- </div>
- </Show>
</div>
- <div data-slot="message-part-actions">
- <Show when={!pending()}>
- <DiffChanges changes={{ additions: file().additions, deletions: file().deletions }} />
- </Show>
+ }
+ >
+ <ToolFileAccordion
+ path={file().relativePath}
+ actions={
+ <Switch>
+ <Match when={file().type === "add"}>
+ <span data-slot="apply-patch-change" data-type="added">
+ {i18n.t("ui.patch.action.created")}
+ </span>
+ </Match>
+ <Match when={file().type === "delete"}>
+ <span data-slot="apply-patch-change" data-type="removed">
+ {i18n.t("ui.patch.action.deleted")}
+ </span>
+ </Match>
+ <Match when={file().type === "move"}>
+ <span data-slot="apply-patch-change" data-type="modified">
+ {i18n.t("ui.patch.action.moved")}
+ </span>
+ </Match>
+ <Match when={true}>
+ <DiffChanges changes={{ additions: file().additions, deletions: file().deletions }} />
+ </Match>
+ </Switch>
+ }
+ >
+ <div data-component="apply-patch-file-diff">
+ <Dynamic
+ component={diffComponent}
+ before={{ name: file().filePath, contents: file().before }}
+ after={{ name: file().movePath ?? file().filePath, contents: file().after }}
+ />
</div>
- </div>
- }
- >
- <div data-component="edit-content">
- <Dynamic
- component={diffComponent}
- before={{ name: file().filePath, contents: file().before }}
- after={{ name: file().movePath ?? file().filePath, contents: file().after }}
- />
- </div>
- </BasicTool>
+ </ToolFileAccordion>
+ </BasicTool>
+ </div>
)}
</Show>
)
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 8e8a3f387..0eceb754c 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -140,6 +140,8 @@ export function SessionTurn(
messageID: string
lastUserMessageID?: string
showReasoningSummaries?: boolean
+ shellToolDefaultOpen?: boolean
+ editToolDefaultOpen?: boolean
onUserInteracted?: () => void
classes?: {
root?: string
@@ -369,6 +371,8 @@ export function SessionTurn(
turnDurationMs={turnDurationMs()}
working={working()}
showReasoningSummaries={showReasoningSummaries()}
+ shellToolDefaultOpen={props.shellToolDefaultOpen}
+ editToolDefaultOpen={props.editToolDefaultOpen}
/>
</div>
</Show>