feat(app): add experimental turn navigator setting (#57522)

This commit is contained in:
usrnk1
2026-10-20 22:41:45 +02:00
committed by GitHub
Unverified
parent 356f739f38
commit 5761fd1a79
7 changed files with 94 additions and 2 deletions
+57 -1
View File
@@ -1,6 +1,6 @@
import { expect, test, type Page } from "@playwright/test"
import type { ConfigEntry, OpenCodeEvent, SessionMessageInfo, WorktreeDirectory } from "@opencode/client/promise"
import { NO_PROVIDER, REMOTE_SERVER, SERVER, holdRoute, project, seed, session } from "../utils/app"
import { NO_PROVIDER, REMOTE_SERVER, SERVER, holdRoute, pageMessagesFrom, project, seed, session } from "../utils/app"
import { mockMissingLocation, mockOpenCodeServer } from "../utils/mock-server"
import { mockRemoteServer, mockWorkspace, openSession, openSettings, type WorkspaceInput } from "../utils/workspace"
@@ -94,6 +94,62 @@ test("a settings page survives refresh", async ({ page }) => {
await expect(page).toHaveURL("/settings?tab=appearance")
})
test("the experimental turn navigator defaults on and can be disabled persistently", async ({ page }) => {
const id = "ses_navigator_settings"
const messages: SessionMessageInfo[] = Array.from({ length: 60 }, (_, index) => ({
id: `msg_navigator_${String(index).padStart(4, "0")}`,
type: "user",
text: `Prompt ${index}`,
time: { created: 1700000000000 + index },
}))
const indexes: string[] = []
page.on("request", (request) => {
const url = new URL(request.url())
if (url.pathname === `/api/session/${id}/message` && url.searchParams.get("type") === "user")
indexes.push(request.url())
})
await openSession(page, {
name: "Turn navigator settings",
sessions: [{ id, title: "Turn navigator settings" }],
pageMessages: pageMessagesFrom({ [id]: messages }),
})
const navigator = page.locator('[data-component="session-turn-navigator"]')
await expect(navigator.locator("[data-turn]")).toHaveCount(60)
expect(indexes).toHaveLength(1)
await page.keyboard.press("Control+,")
const settings = page.getByTestId("settings-screen")
await settings.getByRole("tab", { name: "Experimental", exact: true }).click()
await expect(settings.getByRole("heading", { name: "Experimental", exact: true })).toBeVisible()
const toggle = settings.getByRole("switch", { name: "Turn navigator", exact: true })
await expect(toggle).toBeChecked()
await toggle.press("Space")
await expect(toggle).not.toBeChecked()
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(navigator).toHaveCount(0)
await expect
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("settings.v3") ?? "{}").general?.showTurnNavigator))
.toBe(false)
await page.clock.install()
await page.reload()
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
await expect(page.locator("#message-msg_navigator_0059")).toBeVisible()
await page.clock.fastForward(1500)
await expect(navigator).toHaveCount(0)
expect(indexes).toHaveLength(1)
await page.keyboard.press("Control+,")
await settings.getByRole("tab", { name: "Experimental", exact: true }).click()
await expect(toggle).not.toBeChecked()
await toggle.press("Space")
await expect(toggle).toBeChecked()
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(navigator.locator("[data-turn]")).toHaveCount(60)
expect(indexes).toHaveLength(2)
})
for (const locale of ["en", "ar"]) {
test(`background activity alignment stays physical and persists (${locale})`, async ({ page }) => {
const id = "ses_activity_settings"
+3
View File
@@ -753,6 +753,9 @@ export const dict = {
"settings.experimental.description": "Try experimental features",
"settings.experimental.row.generatedUI.title": "Generated UI",
"settings.experimental.row.generatedUI.description": "Let agents show dashboards, charts and forms in the timeline",
"settings.experimental.row.turnNavigator.title": "Turn navigator",
"settings.experimental.row.turnNavigator.description":
"Show a rail beside the timeline to preview and jump between prompts",
"settings.guiExtensions.title": "Extensions",
"settings.guiExtensions.description": "Built-in GUI extensions in this window. Development builds only.",
"settings.guiExtensions.reload": "Reload",
@@ -27,6 +27,7 @@ import { parseCommentNote, readPromptPresentation } from "@/composer/comment-not
import { parseVoiceDelegation } from "./voice-delegation"
import { useCommand } from "@/shell/commands/command"
import { useExtensionScoped } from "@/runtime/extension/commands"
import { useSettings } from "@/settings/model"
import {
SessionAncestorTrail,
SessionHeaderLayout,
@@ -132,6 +133,7 @@ function MessageTimelineView(
const language = useLanguage()
const command = useCommand()
const extensionScoped = useExtensionScoped()
const settings = useSettings()
const server = useServer()
const data = server.ctx.data
const sdk = useWorkspaceLocation()
@@ -464,7 +466,7 @@ function MessageTimelineView(
return (
<VirtualizedTimeline
navigator={
<Show when={props.onRevealTurn && sessionID()} keyed>
<Show when={settings.general.showTurnNavigator() && props.onRevealTurn && sessionID()} keyed>
{(id) => (
<TurnNavigator
sessionID={id}
@@ -39,6 +39,20 @@ export const SettingsExperimental: Component = () => {
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.experimental.row.turnNavigator.title")}
description={language.t("settings.experimental.row.turnNavigator.description")}
>
<div data-action="settings-turn-navigator">
<Switch
checked={settings.general.showTurnNavigator()}
onChange={(checked) => settings.general.setShowTurnNavigator(checked)}
hideLabel
>
{language.t("settings.experimental.row.turnNavigator.title")}
</Switch>
</div>
</SettingsRow>
</SettingsList>
</div>
</div>
+1
View File
@@ -94,6 +94,7 @@ describe("settings schema", () => {
timelineDetail: timelinePresets[2].value,
showCustomAgents: false,
showTurnDiffs: false,
showTurnNavigator: true,
backgroundActivity: "left",
generatedUI: true,
mobileTitlebarPosition: "top",
+9
View File
@@ -121,6 +121,7 @@ const generalSchema = Persistence.struct({
}),
showCustomAgents: Schema.Boolean,
showTurnDiffs: Schema.Boolean,
showTurnNavigator: Schema.Boolean,
backgroundActivity: Schema.Literals(["off", "left", "center", "right"]),
// Lets agents render generated UI in the timeline; an experiment users can turn off.
generatedUI: Schema.Boolean,
@@ -290,6 +291,7 @@ export const defaultSettings: Settings = {
timelineDetail: { ...timelinePresets[2].value },
showCustomAgents: false,
showTurnDiffs: false,
showTurnNavigator: true,
backgroundActivity: "left",
generatedUI: true,
mobileTitlebarPosition: "top",
@@ -379,6 +381,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setShowTurnDiffs(value: boolean) {
setStore("general", "showTurnDiffs", value)
},
showTurnNavigator: withFallback(
() => store.general?.showTurnNavigator,
defaultSettings.general.showTurnNavigator,
),
setShowTurnNavigator(value: boolean) {
setStore("general", "showTurnNavigator", value)
},
backgroundActivity: withFallback(
() => store.general?.backgroundActivity,
defaultSettings.general.backgroundActivity,
@@ -89,6 +89,13 @@ export const clientSettings: Entry<SettingsRootTab>[] = [
description: "settings.experimental.row.generatedUI.description",
keywords: "genui generative interface dashboards charts forms",
},
{
tab: "experimental",
label: "settings.experimental.row.turnNavigator.title",
target: "settings-turn-navigator",
description: "settings.experimental.row.turnNavigator.description",
keywords: "conversation prompts messages timeline navigation rail",
},
{
tab: "appearance",
label: "settings.general.row.colorScheme.title",