fix(tui): prevent duplicate renderable IDs (#32110)
This commit is contained in:
parent
cf2d1dd3e9
commit
fff0ec294c
@ -202,7 +202,6 @@ function match<T extends PanelEntry>(query: string, entries: T[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PanelShell(props: {
|
function PanelShell(props: {
|
||||||
id: string
|
|
||||||
title: string
|
title: string
|
||||||
countVisible?: boolean
|
countVisible?: boolean
|
||||||
query: string
|
query: string
|
||||||
@ -279,7 +278,7 @@ function PanelShell(props: {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<box id={props.id} width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
||||||
{minimal() ? (
|
{minimal() ? (
|
||||||
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
||||||
{content}
|
{content}
|
||||||
@ -299,7 +298,6 @@ function PanelShell(props: {
|
|||||||
)}
|
)}
|
||||||
{minimal() ? (
|
{minimal() ? (
|
||||||
<box
|
<box
|
||||||
id={`${props.id}-bottom`}
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height={1}
|
height={1}
|
||||||
border={false}
|
border={false}
|
||||||
@ -317,7 +315,6 @@ function PanelShell(props: {
|
|||||||
</box>
|
</box>
|
||||||
) : (
|
) : (
|
||||||
<box
|
<box
|
||||||
id={`${props.id}-bottom`}
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height={1}
|
height={1}
|
||||||
border={["left"]}
|
border={["left"]}
|
||||||
@ -549,7 +546,6 @@ export function RunCommandMenuBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell
|
<PanelShell
|
||||||
id="run-direct-footer-command-panel"
|
|
||||||
title="Commands"
|
title="Commands"
|
||||||
countVisible={false}
|
countVisible={false}
|
||||||
query={query()}
|
query={query()}
|
||||||
@ -565,7 +561,6 @@ export function RunCommandMenuBody(props: {
|
|||||||
chrome="minimal"
|
chrome="minimal"
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-command-list"
|
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
items={items}
|
items={items}
|
||||||
selected={menu.selected}
|
selected={menu.selected}
|
||||||
@ -649,7 +644,6 @@ export function RunSubagentSelectBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell
|
<PanelShell
|
||||||
id="run-direct-footer-subagent-panel"
|
|
||||||
title="Select subagent"
|
title="Select subagent"
|
||||||
query={query()}
|
query={query()}
|
||||||
count={items().length}
|
count={items().length}
|
||||||
@ -664,7 +658,6 @@ export function RunSubagentSelectBody(props: {
|
|||||||
chrome="minimal"
|
chrome="minimal"
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-subagent-list"
|
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
items={items}
|
items={items}
|
||||||
selected={menu.selected}
|
selected={menu.selected}
|
||||||
@ -748,7 +741,6 @@ export function RunQueuedPromptSelectBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell
|
<PanelShell
|
||||||
id="run-direct-footer-queued-panel"
|
|
||||||
title="Queued prompts"
|
title="Queued prompts"
|
||||||
query={query()}
|
query={query()}
|
||||||
count={items().length}
|
count={items().length}
|
||||||
@ -763,7 +755,6 @@ export function RunQueuedPromptSelectBody(props: {
|
|||||||
chrome="minimal"
|
chrome="minimal"
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-queued-list"
|
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
items={items}
|
items={items}
|
||||||
selected={menu.selected}
|
selected={menu.selected}
|
||||||
@ -827,7 +818,6 @@ export function RunSkillSelectBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell
|
<PanelShell
|
||||||
id="run-direct-footer-skill-panel"
|
|
||||||
title="Skills"
|
title="Skills"
|
||||||
query={query()}
|
query={query()}
|
||||||
count={items().length}
|
count={items().length}
|
||||||
@ -842,7 +832,6 @@ export function RunSkillSelectBody(props: {
|
|||||||
chrome="minimal"
|
chrome="minimal"
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-skill-list"
|
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
items={items}
|
items={items}
|
||||||
selected={menu.selected}
|
selected={menu.selected}
|
||||||
@ -927,7 +916,6 @@ export function RunVariantSelectBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell
|
<PanelShell
|
||||||
id="run-direct-footer-variant-panel"
|
|
||||||
title="Select variant"
|
title="Select variant"
|
||||||
query={query()}
|
query={query()}
|
||||||
count={items().length}
|
count={items().length}
|
||||||
@ -942,7 +930,6 @@ export function RunVariantSelectBody(props: {
|
|||||||
chrome="minimal"
|
chrome="minimal"
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-variant-list"
|
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
items={items}
|
items={items}
|
||||||
selected={menu.selected}
|
selected={menu.selected}
|
||||||
@ -1050,7 +1037,6 @@ export function RunModelSelectBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelShell
|
<PanelShell
|
||||||
id="run-direct-footer-model-panel"
|
|
||||||
title="Select model"
|
title="Select model"
|
||||||
query={query()}
|
query={query()}
|
||||||
count={items().length}
|
count={items().length}
|
||||||
@ -1065,7 +1051,6 @@ export function RunModelSelectBody(props: {
|
|||||||
chrome="minimal"
|
chrome="minimal"
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-model-list"
|
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
items={items}
|
items={items}
|
||||||
selected={menu.selected}
|
selected={menu.selected}
|
||||||
|
|||||||
@ -115,7 +115,6 @@ export function createFooterMenuState(input: { count: Accessor<number>; limit?:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RunFooterMenu(props: {
|
export function RunFooterMenu(props: {
|
||||||
id?: string
|
|
||||||
theme: Accessor<RunFooterTheme>
|
theme: Accessor<RunFooterTheme>
|
||||||
items: Accessor<RunFooterMenuItem[]>
|
items: Accessor<RunFooterMenuItem[]>
|
||||||
selected: Accessor<number>
|
selected: Accessor<number>
|
||||||
@ -226,7 +225,6 @@ export function RunFooterMenu(props: {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id={props.id ?? "run-direct-footer-menu"}
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height={props.rows()}
|
height={props.rows()}
|
||||||
backgroundColor={props.background ? props.theme().shade : transparent}
|
backgroundColor={props.background ? props.theme().shade : transparent}
|
||||||
|
|||||||
@ -96,7 +96,6 @@ export function RejectField(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
id="run-direct-footer-permission-reject"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
minHeight={1}
|
minHeight={1}
|
||||||
maxHeight={3}
|
maxHeight={3}
|
||||||
@ -259,14 +258,12 @@ export function RunPermissionBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-permission-body"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
backgroundColor={props.theme.surface}
|
backgroundColor={props.theme.surface}
|
||||||
>
|
>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-permission-head"
|
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={1}
|
gap={1}
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
@ -303,7 +300,6 @@ export function RunPermissionBody(props: {
|
|||||||
fallback={
|
fallback={
|
||||||
<box width="100%" flexGrow={1} flexShrink={1} justifyContent="flex-end">
|
<box width="100%" flexGrow={1} flexShrink={1} justifyContent="flex-end">
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-permission-reject-bar"
|
|
||||||
flexDirection={narrow() ? "column" : "row"}
|
flexDirection={narrow() ? "column" : "row"}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
backgroundColor={props.theme.line}
|
backgroundColor={props.theme.line}
|
||||||
@ -433,7 +429,6 @@ export function RunPermissionBody(props: {
|
|||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-permission-actions"
|
|
||||||
flexDirection={narrow() ? "column" : "row"}
|
flexDirection={narrow() ? "column" : "row"}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
backgroundColor={props.theme.pane}
|
backgroundColor={props.theme.pane}
|
||||||
|
|||||||
@ -250,10 +250,9 @@ export function RunPromptBody(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box id="run-direct-footer-prompt" width="100%">
|
<box width="100%">
|
||||||
<box id="run-direct-footer-input-shell" paddingTop={1} paddingBottom={1} paddingRight={2}>
|
<box paddingTop={1} paddingBottom={1} paddingRight={2}>
|
||||||
<textarea
|
<textarea
|
||||||
id="run-direct-footer-composer"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
minHeight={TEXTAREA_MIN_ROWS}
|
minHeight={TEXTAREA_MIN_ROWS}
|
||||||
maxHeight={TEXTAREA_MAX_ROWS}
|
maxHeight={TEXTAREA_MAX_ROWS}
|
||||||
|
|||||||
@ -268,9 +268,8 @@ export function RunQuestionBody(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box id="run-direct-footer-question-body" width="100%" height="100%" flexDirection="column">
|
<box width="100%" height="100%" flexDirection="column">
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-question-panel"
|
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={1}
|
gap={1}
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
@ -281,14 +280,13 @@ export function RunQuestionBody(props: {
|
|||||||
backgroundColor={props.theme.surface}
|
backgroundColor={props.theme.surface}
|
||||||
>
|
>
|
||||||
<Show when={!single()}>
|
<Show when={!single()}>
|
||||||
<box id="run-direct-footer-question-tabs" flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
||||||
<For each={props.request.questions}>
|
<For each={props.request.questions}>
|
||||||
{(item, index) => {
|
{(item, index) => {
|
||||||
const active = () => state().tab === index()
|
const active = () => state().tab === index()
|
||||||
const answered = () => (state().answers[index()]?.length ?? 0) > 0
|
const answered = () => (state().answers[index()]?.length ?? 0) > 0
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id={`run-direct-footer-question-tab-${index()}`}
|
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor={active() ? props.theme.highlight : props.theme.surface}
|
backgroundColor={active() ? props.theme.highlight : props.theme.surface}
|
||||||
@ -304,7 +302,6 @@ export function RunQuestionBody(props: {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-question-tab-confirm"
|
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor={confirm() ? props.theme.highlight : props.theme.surface}
|
backgroundColor={confirm() ? props.theme.highlight : props.theme.surface}
|
||||||
@ -382,7 +379,6 @@ export function RunQuestionBody(props: {
|
|||||||
const hit = () => state().answers[state().tab]?.includes(item.label) ?? false
|
const hit = () => state().answers[state().tab]?.includes(item.label) ?? false
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id={`run-direct-footer-question-option-${index()}`}
|
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={0}
|
gap={0}
|
||||||
onMouseOver={() => {
|
onMouseOver={() => {
|
||||||
@ -428,7 +424,6 @@ export function RunQuestionBody(props: {
|
|||||||
|
|
||||||
<Show when={questionCustom(props.request, state())}>
|
<Show when={questionCustom(props.request, state())}>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-question-option-custom"
|
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={0}
|
gap={0}
|
||||||
onMouseOver={() => {
|
onMouseOver={() => {
|
||||||
@ -480,7 +475,6 @@ export function RunQuestionBody(props: {
|
|||||||
>
|
>
|
||||||
<box paddingLeft={3}>
|
<box paddingLeft={3}>
|
||||||
<textarea
|
<textarea
|
||||||
id="run-direct-footer-question-custom"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
minHeight={1}
|
minHeight={1}
|
||||||
maxHeight={4}
|
maxHeight={4}
|
||||||
@ -518,7 +512,6 @@ export function RunQuestionBody(props: {
|
|||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-question-actions"
|
|
||||||
flexDirection={narrow() ? "column" : "row"}
|
flexDirection={narrow() ? "column" : "row"}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
gap={1}
|
gap={1}
|
||||||
|
|||||||
@ -120,7 +120,6 @@ export function RunFooterSubagentBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-subagent"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
|
|||||||
@ -621,7 +621,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-shell"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
border={false}
|
border={false}
|
||||||
@ -631,7 +630,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
padding={0}
|
padding={0}
|
||||||
>
|
>
|
||||||
<Show when={panel() || inspecting()}>
|
<Show when={panel() || inspecting()}>
|
||||||
<box id="run-direct-footer-panel-spacer" width="100%" height={1} flexShrink={0} backgroundColor="transparent" />
|
<box width="100%" height={1} flexShrink={0} backgroundColor="transparent" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
@ -641,7 +640,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
<For each={[promptView()]}>
|
<For each={[promptView()]}>
|
||||||
{() => (
|
{() => (
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-composer-frame"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
border={panel() || prompt() ? false : ["left"]}
|
border={panel() || prompt() ? false : ["left"]}
|
||||||
@ -656,7 +654,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-composer-area"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
paddingLeft={0}
|
paddingLeft={0}
|
||||||
@ -666,7 +663,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
backgroundColor={panel() || prompt() ? "transparent" : theme().surface}
|
backgroundColor={panel() || prompt() ? "transparent" : theme().surface}
|
||||||
gap={0}
|
gap={0}
|
||||||
>
|
>
|
||||||
<box id="run-direct-footer-body" width="100%" flexGrow={1} flexShrink={1} flexDirection="column">
|
<box width="100%" flexGrow={1} flexShrink={1} flexDirection="column">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={active().type === "prompt" && route().type === "composer"}>
|
<Match when={active().type === "prompt" && route().type === "composer"}>
|
||||||
<RunPromptBody
|
<RunPromptBody
|
||||||
@ -804,7 +801,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
|
|
||||||
<Show when={!panel() && menu()}>
|
<Show when={!panel() && menu()}>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
id="run-direct-footer-complete"
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
items={composer.options}
|
items={composer.options}
|
||||||
selected={composer.selected}
|
selected={composer.selected}
|
||||||
@ -818,7 +814,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
|
|
||||||
<Show when={!panel() && !menu()}>
|
<Show when={!panel() && !menu()}>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-statusline"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
height={1}
|
height={1}
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
@ -827,7 +822,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
backgroundColor={statuslineBackground()}
|
backgroundColor={statuslineBackground()}
|
||||||
>
|
>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-statusline-mode"
|
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor={theme().statusAccent}
|
backgroundColor={theme().statusAccent}
|
||||||
@ -839,7 +833,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-statusline-main"
|
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={1}
|
gap={1}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
@ -850,13 +843,12 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
>
|
>
|
||||||
<Show when={busy() && !exiting()}>
|
<Show when={busy() && !exiting()}>
|
||||||
<box id="run-direct-footer-status-spinner" flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
<spinner color={spin().color} frames={spin().frames} interval={40} />
|
<spinner color={spin().color} frames={spin().frames} interval={40} />
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<text
|
<text
|
||||||
id="run-direct-footer-statusline-text"
|
|
||||||
fg={statusColor()}
|
fg={statusColor()}
|
||||||
wrapMode="none"
|
wrapMode="none"
|
||||||
truncate
|
truncate
|
||||||
@ -874,7 +866,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
|
|
||||||
<Show when={activityMeta().length > 0}>
|
<Show when={activityMeta().length > 0}>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-statusline-meta"
|
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
flexShrink={1}
|
flexShrink={1}
|
||||||
@ -888,7 +879,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
<Show when={responsive().statusline.showModel && modelStatus()}>
|
<Show when={responsive().statusline.showModel && modelStatus()}>
|
||||||
{(info) => (
|
{(info) => (
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-statusline-model"
|
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
@ -913,7 +903,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
<For each={contextHints()}>
|
<For each={contextHints()}>
|
||||||
{(hint, index) => (
|
{(hint, index) => (
|
||||||
<box
|
<box
|
||||||
id={`run-direct-footer-statusline-${hint.kind}`}
|
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
@ -933,7 +922,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
<Show when={commandHint()}>
|
<Show when={commandHint()}>
|
||||||
{(hint) => (
|
{(hint) => (
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-statusline-hint"
|
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
@ -955,7 +943,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<box
|
<box
|
||||||
id="run-direct-footer-subagent-frame"
|
|
||||||
width="100%"
|
width="100%"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
flexShrink={1}
|
flexShrink={1}
|
||||||
|
|||||||
@ -34,8 +34,6 @@ type ActiveEntry = {
|
|||||||
rendered: boolean
|
rendered: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextId = 0
|
|
||||||
|
|
||||||
function commitMarkdownBlocks(input: {
|
function commitMarkdownBlocks(input: {
|
||||||
surface: ScrollbackSurface
|
surface: ScrollbackSurface
|
||||||
renderable: MarkdownRenderable
|
renderable: MarkdownRenderable
|
||||||
@ -152,12 +150,10 @@ export class RunScrollbackStream {
|
|||||||
const surface = this.renderer.createScrollbackSurface({
|
const surface = this.renderer.createScrollbackSurface({
|
||||||
startOnNewLine: entryFlags(commit).startOnNewLine,
|
startOnNewLine: entryFlags(commit).startOnNewLine,
|
||||||
})
|
})
|
||||||
const id = `run-scrollback-entry-${nextId++}`
|
|
||||||
const style = entryLook(commit, this.theme.entry)
|
const style = entryLook(commit, this.theme.entry)
|
||||||
const renderable =
|
const renderable =
|
||||||
body.type === "text"
|
body.type === "text"
|
||||||
? new TextRenderable(surface.renderContext, {
|
? new TextRenderable(surface.renderContext, {
|
||||||
id,
|
|
||||||
content: "",
|
content: "",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
wrapMode: "word",
|
wrapMode: "word",
|
||||||
@ -166,7 +162,6 @@ export class RunScrollbackStream {
|
|||||||
})
|
})
|
||||||
: body.type === "code"
|
: body.type === "code"
|
||||||
? new CodeRenderable(surface.renderContext, {
|
? new CodeRenderable(surface.renderContext, {
|
||||||
id,
|
|
||||||
content: "",
|
content: "",
|
||||||
filetype: body.filetype,
|
filetype: body.filetype,
|
||||||
syntaxStyle: entrySyntax(commit, this.theme),
|
syntaxStyle: entrySyntax(commit, this.theme),
|
||||||
@ -178,7 +173,6 @@ export class RunScrollbackStream {
|
|||||||
treeSitterClient: this.treeSitterClient,
|
treeSitterClient: this.treeSitterClient,
|
||||||
})
|
})
|
||||||
: new MarkdownRenderable(surface.renderContext, {
|
: new MarkdownRenderable(surface.renderContext, {
|
||||||
id,
|
|
||||||
content: "",
|
content: "",
|
||||||
syntaxStyle: entrySyntax(commit, this.theme),
|
syntaxStyle: entrySyntax(commit, this.theme),
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|||||||
@ -322,7 +322,6 @@ export function entryWriter(input: {
|
|||||||
export function spacerWriter(): ScrollbackWriter {
|
export function spacerWriter(): ScrollbackWriter {
|
||||||
return (ctx: ScrollbackRenderContext) => ({
|
return (ctx: ScrollbackRenderContext) => ({
|
||||||
root: new TextRenderable(ctx.renderContext, {
|
root: new TextRenderable(ctx.renderContext, {
|
||||||
id: "run-scrollback-spacer",
|
|
||||||
width: Math.max(1, Math.trunc(ctx.width)),
|
width: Math.max(1, Math.trunc(ctx.width)),
|
||||||
height: 1,
|
height: 1,
|
||||||
content: "",
|
content: "",
|
||||||
|
|||||||
@ -45,8 +45,6 @@ type Cell = {
|
|||||||
mark: "text" | "full" | "mix" | "top"
|
mark: "text" | "full" | "mix" | "top"
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = 0
|
|
||||||
|
|
||||||
function cells(line: string): Cell[] {
|
function cells(line: string): Cell[] {
|
||||||
const list: Cell[] = []
|
const list: Cell[] = []
|
||||||
for (const char of line) {
|
for (const char of line) {
|
||||||
@ -117,7 +115,6 @@ function write(
|
|||||||
|
|
||||||
root.add(
|
root.add(
|
||||||
new TextRenderable(ctx.renderContext, {
|
new TextRenderable(ctx.renderContext, {
|
||||||
id: `run-direct-splash-line-${id++}`,
|
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: line.left,
|
left: line.left,
|
||||||
top: line.top,
|
top: line.top,
|
||||||
@ -246,7 +243,6 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
|
|||||||
}
|
}
|
||||||
|
|
||||||
const root = new BoxRenderable(ctx.renderContext, {
|
const root = new BoxRenderable(ctx.renderContext, {
|
||||||
id: `run-direct-splash-${kind}-${id++}`,
|
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { RGBA, type BoxRenderable } from "@opentui/core"
|
import { BoxRenderable, RGBA, type RootRenderable } from "@opentui/core"
|
||||||
import { testRender, useRenderer } from "@opentui/solid"
|
import { testRender, useRenderer } from "@opentui/solid"
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||||
@ -248,6 +248,45 @@ function expectPaletteList(list: BoxRenderable, selectedIndex: number) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function child(root: BoxRenderable | RootRenderable, index: number) {
|
||||||
|
return root.getChildren()[index] as BoxRenderable
|
||||||
|
}
|
||||||
|
|
||||||
|
function boxPath(root: BoxRenderable | RootRenderable, name: string): BoxRenderable[] | undefined {
|
||||||
|
for (const item of root.getChildren()) {
|
||||||
|
if (item.constructor.name === name) return root instanceof BoxRenderable ? [root] : []
|
||||||
|
if (!(item instanceof BoxRenderable)) continue
|
||||||
|
const path = boxPath(item, name)
|
||||||
|
if (path) return root instanceof BoxRenderable ? [root, ...path] : path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function footerComposerFrame(root: BoxRenderable | RootRenderable) {
|
||||||
|
return boxPath(root, "TextareaRenderable")!.at(-5)!
|
||||||
|
}
|
||||||
|
|
||||||
|
function footerStatusline(root: BoxRenderable | RootRenderable) {
|
||||||
|
const status = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
|
||||||
|
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
|
||||||
|
const boxes = root.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
|
||||||
|
for (const box of boxes) {
|
||||||
|
const first = box.getChildren().find((item): item is BoxRenderable => item instanceof BoxRenderable)
|
||||||
|
if (
|
||||||
|
box.backgroundColor?.toInts().every((value, index) => value === status[index]) &&
|
||||||
|
first?.backgroundColor?.toInts().every((value, index) => value === accent[index])
|
||||||
|
)
|
||||||
|
return box
|
||||||
|
boxes.push(...box.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable))
|
||||||
|
}
|
||||||
|
throw new Error("Footer statusline not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelMenu(root: BoxRenderable | RootRenderable) {
|
||||||
|
const panel = child(child(root, 0), 0)
|
||||||
|
const content = child(panel, 0)
|
||||||
|
return child(content.getChildren().at(-1) as BoxRenderable, 0)
|
||||||
|
}
|
||||||
|
|
||||||
test("direct footer composer area does not adopt footer surface", async () => {
|
test("direct footer composer area does not adopt footer surface", async () => {
|
||||||
const surface = RGBA.fromHex("#123456")
|
const surface = RGBA.fromHex("#123456")
|
||||||
const [theme, setTheme] = createSignal(RUN_THEME_FALLBACK)
|
const [theme, setTheme] = createSignal(RUN_THEME_FALLBACK)
|
||||||
@ -255,7 +294,7 @@ test("direct footer composer area does not adopt footer surface", async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const area = app.renderer.root.findDescendantById("run-direct-footer-composer-area") as BoxRenderable
|
const area = child(footerComposerFrame(app.renderer.root), 0)
|
||||||
|
|
||||||
expect(area.backgroundColor.toInts()).not.toEqual(surface.toInts())
|
expect(area.backgroundColor.toInts()).not.toEqual(surface.toInts())
|
||||||
setTheme({
|
setTheme({
|
||||||
@ -588,7 +627,7 @@ test("direct subagent panel renders active subagents", async () => {
|
|||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
const list = app.renderer.root.findDescendantById("run-direct-footer-subagent-list") as BoxRenderable
|
const list = panelMenu(app.renderer.root)
|
||||||
|
|
||||||
expect(frame).toContain("Select subagent")
|
expect(frame).toContain("Select subagent")
|
||||||
expect(frame).toContain("Inspect auth flow")
|
expect(frame).toContain("Inspect auth flow")
|
||||||
@ -626,7 +665,7 @@ test("direct queued prompt panel renders pending prompt actions", async () => {
|
|||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
const list = app.renderer.root.findDescendantById("run-direct-footer-queued-list") as BoxRenderable
|
const list = panelMenu(app.renderer.root)
|
||||||
|
|
||||||
expect(frame).toContain("Queued prompts")
|
expect(frame).toContain("Queued prompts")
|
||||||
expect(frame).toContain("fix the auth test")
|
expect(frame).toContain("fix the auth test")
|
||||||
@ -648,12 +687,12 @@ test.skip("direct footer recreates the frame across command panel transitions",
|
|||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
|
|
||||||
for (let index = 0; index < 3; index++) {
|
for (let index = 0; index < 3; index++) {
|
||||||
const composerFrame = app.renderer.root.findDescendantById("run-direct-footer-composer-frame") as BoxRenderable
|
const composerFrame = footerComposerFrame(app.renderer.root)
|
||||||
app.mockInput.pressKey("p", { ctrl: true })
|
app.mockInput.pressKey("p", { ctrl: true })
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
|
|
||||||
expect(app.captureCharFrame()).toContain("Commands")
|
expect(app.captureCharFrame()).toContain("Commands")
|
||||||
expect(app.renderer.root.findDescendantById("run-direct-footer-composer-frame")).not.toBe(composerFrame)
|
expect(footerComposerFrame(app.renderer.root)).not.toBe(composerFrame)
|
||||||
app.mockInput.pressKey("c", { ctrl: true })
|
app.mockInput.pressKey("c", { ctrl: true })
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
expect(app.captureCharFrame()).not.toContain("Commands")
|
expect(app.captureCharFrame()).not.toContain("Commands")
|
||||||
@ -952,13 +991,14 @@ test("direct footer shows editable prompts and additional queued work while runn
|
|||||||
const transparent = RGBA.fromValues(0, 0, 0, 0).toInts()
|
const transparent = RGBA.fromValues(0, 0, 0, 0).toInts()
|
||||||
const tinted = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
|
const tinted = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
|
||||||
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
|
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
|
||||||
const statusline = app.renderer.root.findDescendantById("run-direct-footer-statusline") as BoxRenderable
|
const statusline = footerStatusline(app.renderer.root)
|
||||||
const mode = app.renderer.root.findDescendantById("run-direct-footer-statusline-mode") as BoxRenderable
|
const statusItems = statusline.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
|
||||||
const main = app.renderer.root.findDescendantById("run-direct-footer-statusline-main") as BoxRenderable
|
const mode = statusItems[0]
|
||||||
const spinner = app.renderer.root.findDescendantById("run-direct-footer-status-spinner")
|
const main = statusItems[1]
|
||||||
const model = app.renderer.root.findDescendantById("run-direct-footer-statusline-model") as BoxRenderable
|
const spinner = main.getChildren()[0]
|
||||||
const queued = app.renderer.root.findDescendantById("run-direct-footer-statusline-queued") as BoxRenderable
|
const model = statusItems[2]
|
||||||
const hint = app.renderer.root.findDescendantById("run-direct-footer-statusline-hint") as BoxRenderable
|
const queued = statusItems[3]
|
||||||
|
const hint = statusItems.at(-1)!
|
||||||
|
|
||||||
expect(spinner).toBeDefined()
|
expect(spinner).toBeDefined()
|
||||||
expect(frame).toContain("a-model-name-long-enough-to-force-responsive-truncation")
|
expect(frame).toContain("a-model-name-long-enough-to-force-responsive-truncation")
|
||||||
@ -1276,7 +1316,7 @@ test("direct model panel renders current model selector", async () => {
|
|||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
const list = app.renderer.root.findDescendantById("run-direct-footer-model-list") as BoxRenderable
|
const list = panelMenu(app.renderer.root)
|
||||||
|
|
||||||
expect(frame).toContain("Select model")
|
expect(frame).toContain("Select model")
|
||||||
expect(frame).toContain("Search")
|
expect(frame).toContain("Search")
|
||||||
@ -1319,7 +1359,7 @@ test("direct variant panel renders current variant selector", async () => {
|
|||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
const list = app.renderer.root.findDescendantById("run-direct-footer-variant-list") as BoxRenderable
|
const list = panelMenu(app.renderer.root)
|
||||||
|
|
||||||
expect(frame).toContain("Select variant")
|
expect(frame).toContain("Select variant")
|
||||||
expect(frame).toContain("Default")
|
expect(frame).toContain("Default")
|
||||||
|
|||||||
@ -784,7 +784,6 @@ function DiffViewer(props: { api: TuiPluginApi }) {
|
|||||||
<Panel flexGrow={1} minHeight={0} border="none">
|
<Panel flexGrow={1} minHeight={0} border="none">
|
||||||
<Separator axis="x" start={showFileTree() ? "edge-out" : undefined} />
|
<Separator axis="x" start={showFileTree() ? "edge-out" : undefined} />
|
||||||
<scrollbox
|
<scrollbox
|
||||||
id="diff-viewer-patches"
|
|
||||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
minHeight={0}
|
minHeight={0}
|
||||||
@ -824,7 +823,6 @@ function DiffViewer(props: { api: TuiPluginApi }) {
|
|||||||
{(patch) => (
|
{(patch) => (
|
||||||
<box border={patchLeftBorder()} borderColor={theme().border}>
|
<box border={patchLeftBorder()} borderColor={theme().border}>
|
||||||
<diff
|
<diff
|
||||||
id={`diff-viewer-patch-${entry.fileIndex}`}
|
|
||||||
ref={(element: DiffRenderable) => diffNodeByFileIndex.set(entry.fileIndex, element)}
|
ref={(element: DiffRenderable) => diffNodeByFileIndex.set(entry.fileIndex, element)}
|
||||||
diff={patch()}
|
diff={patch()}
|
||||||
view={view()}
|
view={view()}
|
||||||
|
|||||||
@ -1607,7 +1607,13 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={content()}>
|
<Show when={content()}>
|
||||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
|
<box
|
||||||
|
id={`text-${props.part.messageID}-${props.part.id}`}
|
||||||
|
paddingLeft={3}
|
||||||
|
marginTop={1}
|
||||||
|
flexDirection="column"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
<box onMouseUp={toggle}>
|
<box onMouseUp={toggle}>
|
||||||
<ReasoningHeader
|
<ReasoningHeader
|
||||||
toggleable={inMinimal()}
|
toggleable={inMinimal()}
|
||||||
@ -1684,7 +1690,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
|
|||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
return (
|
return (
|
||||||
<Show when={props.part.text.trim()}>
|
<Show when={props.part.text.trim()}>
|
||||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
<box id={`text-${props.part.messageID}-${props.part.id}`} paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||||
<markdown
|
<markdown
|
||||||
syntaxStyle={syntax()}
|
syntaxStyle={syntax()}
|
||||||
streaming={true}
|
streaming={true}
|
||||||
@ -1875,7 +1881,7 @@ function InlineTool(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineToolRow
|
<InlineToolRow
|
||||||
id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.id}`}
|
id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.messageID}-${props.part.id}`}
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
iconColor={props.iconColor}
|
iconColor={props.iconColor}
|
||||||
color={fg()}
|
color={fg()}
|
||||||
@ -2007,7 +2013,7 @@ function BlockTool(props: {
|
|||||||
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id={props.part ? "tool-block-" + props.part.id : undefined}
|
id={props.part ? `tool-block-${props.part.messageID}-${props.part.id}` : undefined}
|
||||||
border={["left"]}
|
border={["left"]}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
paddingBottom={1}
|
paddingBottom={1}
|
||||||
@ -2174,7 +2180,7 @@ function Read(props: ToolProps) {
|
|||||||
</InlineTool>
|
</InlineTool>
|
||||||
<For each={loaded()}>
|
<For each={loaded()}>
|
||||||
{(filepath, index) => (
|
{(filepath, index) => (
|
||||||
<box id={`tool-inline-loaded-${props.part.id}-${index()}`} paddingLeft={3}>
|
<box id={`tool-inline-loaded-${props.part.messageID}-${props.part.id}-${index()}`} paddingLeft={3}>
|
||||||
<text paddingLeft={3} fg={theme.textMuted}>
|
<text paddingLeft={3} fg={theme.textMuted}>
|
||||||
↳ Loaded {pathFormatter.format(filepath)}
|
↳ Loaded {pathFormatter.format(filepath)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@ -238,9 +238,19 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
const option = selected()
|
const option = selected()
|
||||||
if (option) props.onMove?.(option)
|
if (option) props.onMove?.(option)
|
||||||
if (!scroll) return
|
if (!scroll) return
|
||||||
const target = scroll.getChildren().find((child: { id?: string }) => {
|
let remaining = store.selected
|
||||||
return child.id === JSON.stringify(selected()?.value)
|
let index = 0
|
||||||
})
|
// Locate the row by position because a unique renderable ID cannot currently be ensured.
|
||||||
|
for (const [category, options] of grouped()) {
|
||||||
|
if (category) index++
|
||||||
|
if (remaining < options.length) {
|
||||||
|
index += remaining
|
||||||
|
break
|
||||||
|
}
|
||||||
|
index += options.length
|
||||||
|
remaining -= options.length
|
||||||
|
}
|
||||||
|
const target = scroll.getChildren()[index]
|
||||||
if (!target) return
|
if (!target) return
|
||||||
const y = target.y - scroll.y
|
const y = target.y - scroll.y
|
||||||
if (center) {
|
if (center) {
|
||||||
@ -553,7 +563,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id={JSON.stringify(option.value)}
|
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
position="relative"
|
position="relative"
|
||||||
onMouseMove={() => {
|
onMouseMove={() => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||||
import type { Renderable, ScrollBoxRenderable } from "@opentui/core"
|
import { DiffRenderable, type Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||||
import { testRender, useRenderer } from "@opentui/solid"
|
import { testRender, useRenderer } from "@opentui/solid"
|
||||||
import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
|
import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
|
||||||
import type { Session } from "@opencode-ai/sdk/v2"
|
import type { Session } from "@opencode-ai/sdk/v2"
|
||||||
@ -63,9 +63,9 @@ test("brackets navigate diff hunks", async () => {
|
|||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
await viewer.app.waitForFrame((frame) => frame.includes("const first"))
|
await viewer.app.waitForFrame((frame) => frame.includes("const first"))
|
||||||
await viewer.app.waitFor(() => Boolean(findRenderable(viewer.app.renderer.root, "diff-viewer-patches")))
|
await viewer.app.waitFor(() => Boolean(findScrollBox(viewer.app.renderer.root)))
|
||||||
await viewer.app.flush()
|
await viewer.app.flush()
|
||||||
const scroll = findRenderable(viewer.app.renderer.root, "diff-viewer-patches") as ScrollBoxRenderable
|
const scroll = findScrollBox(viewer.app.renderer.root)!
|
||||||
const initial = scroll.scrollTop
|
const initial = scroll.scrollTop
|
||||||
|
|
||||||
expect(TuiKeybind.defaultValue("diff_next_hunk")).toBe("]")
|
expect(TuiKeybind.defaultValue("diff_next_hunk")).toBe("]")
|
||||||
@ -178,14 +178,19 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
|
|||||||
|
|
||||||
const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } }
|
const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } }
|
||||||
|
|
||||||
function findRenderable(root: Renderable, id: string): Renderable | undefined {
|
function findScrollBox(root: Renderable): ScrollBoxRenderable | undefined {
|
||||||
if (root.id === id) return root
|
if (root instanceof ScrollBoxRenderable && containsDiff(root)) return root
|
||||||
return root
|
return root
|
||||||
.getChildren()
|
.getChildren()
|
||||||
.map((child) => findRenderable(child, id))
|
.map(findScrollBox)
|
||||||
.find(Boolean)
|
.find(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function containsDiff(root: Renderable): boolean {
|
||||||
|
if (root instanceof DiffRenderable) return true
|
||||||
|
return root.getChildren().some(containsDiff)
|
||||||
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
id: "session-1",
|
id: "session-1",
|
||||||
slug: "session-1",
|
slug: "session-1",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user