mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 22:33:37 +00:00
feat(app): add bottom terminal panel (#44175)
This commit is contained in:
parent
784042202e
commit
2ae3b82c24
9 changed files with 194 additions and 67 deletions
|
|
@ -18,7 +18,7 @@ const branchDiffs = [
|
|||
),
|
||||
]
|
||||
|
||||
test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => {
|
||||
test("uses side placement by default and supports the terminal across the bottom", async ({ page }) => {
|
||||
test.setTimeout(120_000)
|
||||
await page.setViewportSize({ width: 1400, height: 900 })
|
||||
await mockOpenCodeServer(page, {
|
||||
|
|
@ -27,7 +27,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||
id: projectID,
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "review-terminal-stacked",
|
||||
name: "review-terminal-bottom",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
},
|
||||
|
|
@ -45,7 +45,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||
sessions: [
|
||||
{
|
||||
id: sessionID,
|
||||
slug: "review-terminal-stacked",
|
||||
slug: "review-terminal-bottom",
|
||||
projectID,
|
||||
directory,
|
||||
title,
|
||||
|
|
@ -138,7 +138,22 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(page.locator("#terminal-panel")).toBeVisible()
|
||||
await expectTree(page, 2_773, "action.yml")
|
||||
await expectStackGeometry(page)
|
||||
await expectSideGeometry(page)
|
||||
|
||||
await page.evaluate(() => {
|
||||
const settings = JSON.parse(localStorage.getItem("settings.v3") ?? "{}")
|
||||
localStorage.setItem(
|
||||
"settings.v3",
|
||||
JSON.stringify({ ...settings, general: { ...settings.general, terminalPlacement: "bottom" } }),
|
||||
)
|
||||
})
|
||||
await page.reload()
|
||||
await expectSessionReady(page, { server, sessionID, title })
|
||||
await expect(page.locator("#review-panel")).toBeVisible()
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(page.locator("#terminal-panel")).toBeVisible()
|
||||
await expectTree(page, 2_773, "action.yml")
|
||||
await expectBottomGeometry(page)
|
||||
})
|
||||
|
||||
async function expectTree(page: Page, total: number, file: string) {
|
||||
|
|
@ -163,23 +178,51 @@ async function expectMountedTree(page: Page, total: number) {
|
|||
expect(state.rows).toBeLessThanOrEqual(60)
|
||||
}
|
||||
|
||||
async function expectStackGeometry(page: Page) {
|
||||
async function expectSideGeometry(page: Page) {
|
||||
const geometry = await page.evaluate(() => {
|
||||
const review = document.querySelector<HTMLElement>("#review-panel")!.getBoundingClientRect()
|
||||
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!.getBoundingClientRect()
|
||||
return {
|
||||
reviewLeft: review.left,
|
||||
reviewRight: review.right,
|
||||
terminalLeft: terminal.left,
|
||||
terminalRight: terminal.right,
|
||||
terminalTop: terminal.top,
|
||||
reviewTop: review.top,
|
||||
}
|
||||
})
|
||||
expect(Math.abs(geometry.terminalLeft - geometry.reviewLeft)).toBeLessThanOrEqual(1)
|
||||
expect(Math.abs(geometry.terminalRight - geometry.reviewRight)).toBeLessThanOrEqual(1)
|
||||
expect(geometry.terminalTop).toBeGreaterThan(geometry.reviewTop)
|
||||
}
|
||||
|
||||
async function expectBottomGeometry(page: Page) {
|
||||
const geometry = await page.evaluate(() => {
|
||||
const review = document.querySelector<HTMLElement>("#review-panel")!
|
||||
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!
|
||||
const terminalRect = terminal.getBoundingClientRect()
|
||||
const reviewParent = review.parentElement!.getBoundingClientRect()
|
||||
const terminalParent = terminal.parentElement!.getBoundingClientRect()
|
||||
const sidebar = review.querySelector<HTMLElement>('[data-slot="session-review-v2-sidebar"]')!
|
||||
return {
|
||||
review: review.getBoundingClientRect().height,
|
||||
reviewBottom: review.getBoundingClientRect().bottom,
|
||||
reviewParent: reviewParent.height,
|
||||
terminal: terminal.getBoundingClientRect().height,
|
||||
terminal: terminalRect.height,
|
||||
terminalLeft: terminalRect.left,
|
||||
terminalRight: terminalRect.right,
|
||||
terminalTop: terminalRect.top,
|
||||
terminalParent: terminalParent.height,
|
||||
sidebar: sidebar.getBoundingClientRect().width,
|
||||
viewport: window.innerWidth,
|
||||
}
|
||||
})
|
||||
expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1)
|
||||
expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1)
|
||||
expect(geometry.terminalTop - geometry.reviewBottom).toBeGreaterThanOrEqual(7)
|
||||
expect(geometry.terminalTop - geometry.reviewBottom).toBeLessThanOrEqual(9)
|
||||
expect(geometry.terminalLeft).toBeLessThanOrEqual(9)
|
||||
expect(geometry.terminalRight).toBeGreaterThanOrEqual(geometry.viewport - 9)
|
||||
expect(geometry.sidebar).toBeGreaterThanOrEqual(240)
|
||||
}
|
||||
|
||||
|
|
@ -84,11 +84,11 @@ test("transitions a streaming shell from writing through command execution", asy
|
|||
executed: true,
|
||||
}),
|
||||
)
|
||||
await expect(titleShimmer).toHaveAttribute("data-active", "false")
|
||||
await expect(titleShimmer).toHaveAttribute("data-active", "true")
|
||||
await expect(subtitle).toHaveText(command)
|
||||
})
|
||||
|
||||
test("shows and expands a running shell command without shimmering it", async ({ page }) => {
|
||||
test("shimmers and expands a running shell command", async ({ page }) => {
|
||||
const id = "prt_shell_running_command"
|
||||
const command = "sleep 10 && echo done"
|
||||
await setupTimeline(page, {
|
||||
|
|
@ -97,7 +97,7 @@ test("shows and expands a running shell command without shimmering it", async ({
|
|||
})
|
||||
|
||||
const tool = page.locator(`[data-timeline-part-id="${id}"]`)
|
||||
await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "false")
|
||||
await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
|
||||
await expect(tool).not.toContainText("Writing command...")
|
||||
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command)
|
||||
await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0)
|
||||
|
|
|
|||
|
|
@ -922,6 +922,10 @@ export const dict = {
|
|||
"settings.general.row.shell.description": "Shell used by the terminal and agent tools",
|
||||
"settings.general.row.shell.autoDefault": "Auto (Default)",
|
||||
"settings.general.row.shell.terminalOnly": "terminal only",
|
||||
"settings.general.row.terminalPlacement.title": "Terminal placement",
|
||||
"settings.general.row.terminalPlacement.description": "Choose where the terminal opens in sessions",
|
||||
"settings.general.row.terminalPlacement.side": "Side",
|
||||
"settings.general.row.terminalPlacement.bottom": "Bottom",
|
||||
"settings.general.row.appearance.title": "Appearance",
|
||||
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
||||
"settings.general.row.colorScheme.title": "Color scheme",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
|
|||
const reviewPanelOpen = createMemo(() => reviewOpen() && !!session.identity.params.id)
|
||||
const terminalOpen = createMemo(() => session.layout.view().terminal.opened())
|
||||
const desktopTerminalOpen = createMemo(() => session.isDesktop() && terminalOpen())
|
||||
const sideTerminalOpen = createMemo(() => desktopTerminalOpen() && settings.general.terminalPlacement() === "side")
|
||||
const bottomTerminalOpen = createMemo(
|
||||
() => desktopTerminalOpen() && settings.general.terminalPlacement() === "bottom",
|
||||
)
|
||||
const fileTreeOpen = createMemo(
|
||||
() =>
|
||||
session.isDesktop() &&
|
||||
|
|
@ -23,7 +27,7 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
|
|||
opened: layout.fileTree.opened(),
|
||||
}),
|
||||
)
|
||||
const resizable = createMemo(() => reviewPanelOpen() || desktopTerminalOpen())
|
||||
const resizable = createMemo(() => reviewPanelOpen() || sideTerminalOpen())
|
||||
const sidePanelOpen = createMemo(() => resizable() || fileTreeOpen())
|
||||
const [rowWidth, setRowWidth] = createSignal<number>()
|
||||
let row: HTMLDivElement | undefined
|
||||
|
|
@ -57,7 +61,7 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
|
|||
const panelLayout = createMemo(() =>
|
||||
sessionPanelLayout({
|
||||
review: reviewPanelOpen(),
|
||||
terminal: desktopTerminalOpen(),
|
||||
terminal: sideTerminalOpen(),
|
||||
files: fileTreeOpen(),
|
||||
}),
|
||||
)
|
||||
|
|
@ -97,11 +101,11 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
|
|||
panelOpen: reviewPanelOpen,
|
||||
snap: reviewSnap,
|
||||
},
|
||||
side: { layout: panelLayout, open: sidePanelOpen },
|
||||
side: { layout: panelLayout },
|
||||
size,
|
||||
terminal: {
|
||||
desktopOpen: desktopTerminalOpen,
|
||||
inlineOnlyOpen: createMemo(() => desktopTerminalOpen() && !reviewPanelOpen()),
|
||||
bottomOpen: bottomTerminalOpen,
|
||||
inlineOnlyOpen: createMemo(() => sideTerminalOpen() && !reviewPanelOpen()),
|
||||
open: terminalOpen,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,51 +119,88 @@ export function SessionScreen(props: { session: SessionModel }) {
|
|||
return (
|
||||
<SessionRouteFrame>
|
||||
<SessionHeader />
|
||||
<div ref={screen.panel.ref} class="flex-1 min-h-0 flex flex-col md:flex-row gap-2 p-2">
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
|
||||
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!screen.size.active() && !screen.review.snap() && !screen.terminal.inlineOnlyOpen(),
|
||||
}}
|
||||
style={{
|
||||
width: screen.panel.width(),
|
||||
}}
|
||||
>
|
||||
<Show when={screen.panel.key()} keyed>
|
||||
{(_) => (
|
||||
<SessionPanelFrame raised={!!session.identity.params.id}>
|
||||
<ErrorBoundary fallback={sessionErrorFallback}>{sessionPanelContent()}</ErrorBoundary>
|
||||
</SessionPanelFrame>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
|
||||
<div ref={screen.panel.ref} class="flex-1 min-h-0 flex flex-col md:flex-row gap-2">
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
|
||||
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!screen.size.active() && !screen.review.snap() && !screen.terminal.inlineOnlyOpen(),
|
||||
}}
|
||||
style={{
|
||||
width: screen.panel.width(),
|
||||
}}
|
||||
>
|
||||
<Show when={screen.panel.key()} keyed>
|
||||
{(_) => (
|
||||
<SessionPanelFrame raised={!!session.identity.params.id}>
|
||||
<ErrorBoundary fallback={sessionErrorFallback}>{sessionPanelContent()}</ErrorBoundary>
|
||||
</SessionPanelFrame>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={screen.panel.resizable()}>
|
||||
<div onPointerDown={() => screen.size.start()}>
|
||||
<ResizeHandle
|
||||
class="-end-1"
|
||||
direction="horizontal"
|
||||
size={screen.panel.resizedWidth()}
|
||||
min={SESSION_PANEL_WIDTH_MIN}
|
||||
max={screen.panel.max()}
|
||||
onResize={(width) => {
|
||||
screen.size.touch()
|
||||
layout.session.resize(width)
|
||||
}}
|
||||
/>
|
||||
<Show when={screen.panel.resizable()}>
|
||||
<div onPointerDown={() => screen.size.start()}>
|
||||
<ResizeHandle
|
||||
class="-end-1"
|
||||
direction="horizontal"
|
||||
size={screen.panel.resizedWidth()}
|
||||
min={SESSION_PANEL_WIDTH_MIN}
|
||||
max={screen.panel.max()}
|
||||
onResize={(width) => {
|
||||
screen.size.touch()
|
||||
layout.session.resize(width)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={isDesktop() && screen.side.layout().visible}>
|
||||
<div class="min-w-0 h-full flex flex-1 flex-col">
|
||||
<Show when={screen.review.panelOpen() || screen.files.open()}>
|
||||
<div class="min-h-0 flex-1">
|
||||
<SessionDesktopReview review={review} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={screen.side.layout().stacked}>
|
||||
<div class="relative h-2 shrink-0" onPointerDown={() => screen.size.start()}>
|
||||
<ResizeHandle
|
||||
class="!relative !inset-auto !h-full !w-full !transform-none"
|
||||
direction="vertical"
|
||||
size={layout.terminal.height()}
|
||||
min={100}
|
||||
max={typeof window === "undefined" ? 600 : window.innerHeight * 0.6}
|
||||
collapseThreshold={50}
|
||||
onResize={(height) => {
|
||||
screen.size.touch()
|
||||
layout.terminal.resize(height)
|
||||
}}
|
||||
onCollapse={() => session.layout.view().terminal.close()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={screen.terminal.open() && !screen.terminal.bottomOpen()}>
|
||||
<div
|
||||
classList={{
|
||||
"min-h-0 shrink-0": screen.side.layout().stacked,
|
||||
"min-h-0 flex-1": !screen.side.layout().stacked,
|
||||
}}
|
||||
>
|
||||
<TerminalPanel stacked={screen.side.layout().stacked} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={isDesktop() ? screen.side.layout().visible : screen.terminal.open()}>
|
||||
<div class="min-w-0 h-full flex flex-1 flex-col">
|
||||
<Show when={isDesktop() && (screen.review.panelOpen() || screen.files.open())}>
|
||||
<div class="min-h-0 flex-1">
|
||||
<SessionDesktopReview review={review} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={screen.side.layout().stacked}>
|
||||
<div class="relative h-2 shrink-0" onPointerDown={() => screen.size.start()}>
|
||||
<Show when={screen.terminal.open() && (!isDesktop() || screen.terminal.bottomOpen())}>
|
||||
<div classList={{ "relative min-h-0 shrink-0": isDesktop() }}>
|
||||
<Show when={isDesktop()}>
|
||||
<div
|
||||
class="absolute z-10 -top-1 left-0 right-0 h-2"
|
||||
onPointerDown={() => screen.size.start()}
|
||||
>
|
||||
<ResizeHandle
|
||||
class="!relative !inset-auto !h-full !w-full !transform-none"
|
||||
direction="vertical"
|
||||
|
|
@ -179,16 +216,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
|||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={screen.terminal.open()}>
|
||||
<div
|
||||
classList={{
|
||||
"min-h-0 shrink-0": screen.side.layout().stacked,
|
||||
"min-h-0 flex-1": !screen.side.layout().stacked,
|
||||
}}
|
||||
>
|
||||
<TerminalPanel stacked={screen.side.layout().stacked} />
|
||||
</div>
|
||||
</Show>
|
||||
<TerminalPanel stacked={isDesktop()} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ export function createTimelineVirtualizer(input: Input) {
|
|||
const item = createMemo(() => virtualItemByKey().get(rowProps.rowKey) ?? initialItem)
|
||||
const row = createMemo(() => rowByKey().get(rowProps.rowKey) ?? rows()[item().index] ?? initialRow)
|
||||
const [ready, setReady] = createSignal(initialItem.size <= fallbackItemSize || !props.deferred(initialRow))
|
||||
let contentMeasureFrame: number | undefined
|
||||
|
||||
onMount(() => virtualizer.measureElement(element))
|
||||
createEffect(
|
||||
|
|
@ -278,7 +279,10 @@ export function createTimelineVirtualizer(input: Input) {
|
|||
{ defer: true },
|
||||
),
|
||||
)
|
||||
onCleanup(() => virtualizer.measureElement(null))
|
||||
onCleanup(() => {
|
||||
if (contentMeasureFrame !== undefined) cancelAnimationFrame(contentMeasureFrame)
|
||||
queueMicrotask(() => virtualizer.measureElement(null))
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -302,6 +306,11 @@ export function createTimelineVirtualizer(input: Input) {
|
|||
>
|
||||
{props.renderRow(row, () => {
|
||||
setReady(true)
|
||||
if (contentMeasureFrame !== undefined) cancelAnimationFrame(contentMeasureFrame)
|
||||
contentMeasureFrame = requestAnimationFrame(() => {
|
||||
contentMeasureFrame = undefined
|
||||
if (element.isConnected) virtualizer.measureElement(element)
|
||||
})
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { TextInput } from "@opencode-ai/ui/text-input"
|
|||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useUpdaterAction } from "@/shell/updates/action"
|
||||
import { type WorkspaceDefaultDestination, useSettings } from "@/settings/model"
|
||||
import { type TerminalPlacement, type WorkspaceDefaultDestination, useSettings } from "@/settings/model"
|
||||
import { ExternalLink } from "@/runtime/platform/external-link"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import { SettingsRow } from "@/settings/row"
|
||||
|
|
@ -123,6 +123,33 @@ const ShellSetting: Component<{ controller: ShellSettingsController }> = (props)
|
|||
)
|
||||
}
|
||||
|
||||
const TerminalPlacementSetting: Component = () => {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const options = createMemo((): { value: TerminalPlacement; label: string }[] => [
|
||||
{ value: "side", label: language.t("settings.general.row.terminalPlacement.side") },
|
||||
{ value: "bottom", label: language.t("settings.general.row.terminalPlacement.bottom") },
|
||||
])
|
||||
|
||||
return (
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.terminalPlacement.title")}
|
||||
description={language.t("settings.general.row.terminalPlacement.description")}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-terminal-placement"
|
||||
options={options()}
|
||||
current={options().find((option) => option.value === settings.general.terminalPlacement())}
|
||||
value={(option) => option.value}
|
||||
label={(option) => option.label}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
onSelect={(option) => option && settings.general.setTerminalPlacement(option.value)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
)
|
||||
}
|
||||
|
||||
const AppearanceSection: Component<{ controller: AppearanceSettingsController }> = (props) => {
|
||||
const language = useLanguage()
|
||||
return (
|
||||
|
|
@ -273,6 +300,7 @@ export const SettingsGeneral: Component<{
|
|||
<PermissionScopeSetting controller={permissionScope} />
|
||||
|
||||
<ShellSetting controller={shell} />
|
||||
<TerminalPlacementSetting />
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ScopedKey, type ServerScope } from "@/runtime/server/scope"
|
|||
|
||||
export type WorkspaceDefaultDestination = "last-used" | "local" | "new"
|
||||
export type WorkspaceLastUsed = "local" | "workspace"
|
||||
export type TerminalPlacement = "side" | "bottom"
|
||||
|
||||
export interface NotificationSettings {
|
||||
agent: boolean
|
||||
|
|
@ -36,6 +37,7 @@ export interface Settings {
|
|||
editToolPartsExpanded: boolean
|
||||
showCustomAgents: boolean
|
||||
mobileTitlebarPosition: "top" | "bottom"
|
||||
terminalPlacement: TerminalPlacement
|
||||
}
|
||||
appearance: {
|
||||
fontSize: number
|
||||
|
|
@ -121,6 +123,7 @@ const defaultSettings: Settings = {
|
|||
editToolPartsExpanded: false,
|
||||
showCustomAgents: false,
|
||||
mobileTitlebarPosition: "top",
|
||||
terminalPlacement: "side",
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 14,
|
||||
|
|
@ -240,6 +243,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||
setMobileTitlebarPosition(value: "top" | "bottom") {
|
||||
setStore("general", "mobileTitlebarPosition", value)
|
||||
},
|
||||
terminalPlacement: withFallback(
|
||||
() => store.general?.terminalPlacement,
|
||||
defaultSettings.general.terminalPlacement,
|
||||
),
|
||||
setTerminalPlacement(value: TerminalPlacement) {
|
||||
setStore("general", "terminalPlacement", value)
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
fileTree: showFileTree,
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,7 @@ ToolRegistry.register({
|
|||
render(props) {
|
||||
const i18n = useI18n()
|
||||
const streaming = () => props.status === "streaming"
|
||||
const pending = () => streaming() || props.status === "running" || props.metadata.status === "running"
|
||||
const sawStreaming = streaming()
|
||||
const command = () => {
|
||||
if (typeof props.input.command === "string") return props.input.command
|
||||
|
|
@ -1295,7 +1296,7 @@ ToolRegistry.register({
|
|||
<span data-slot="basic-tool-tool-title">
|
||||
<TextShimmer
|
||||
text={i18n.t("ui.tool.shell")}
|
||||
active={streaming() || (props.status === "completed" && props.metadata.status === "running")}
|
||||
active={pending()}
|
||||
/>
|
||||
</span>
|
||||
<Show when={!open()}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue