From 5761fd1a79c6ea033fb1f0b3c41b2a55ccc9b21f Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:41:45 +0200 Subject: [PATCH] feat(app): add experimental turn navigator setting (#57522) --- packages/app/e2e/regression/settings.spec.ts | 58 ++++++++++++++++++- packages/app/src/runtime/i18n/en.ts | 3 + .../src/session/timeline/message-timeline.tsx | 4 +- .../settings/experimental/experimental.tsx | 14 +++++ packages/app/src/settings/model.test.ts | 1 + packages/app/src/settings/model.tsx | 9 +++ packages/app/src/settings/search-catalog.ts | 7 +++ 7 files changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/regression/settings.spec.ts b/packages/app/e2e/regression/settings.spec.ts index f7583590ce8..6945e5c97e2 100644 --- a/packages/app/e2e/regression/settings.spec.ts +++ b/packages/app/e2e/regression/settings.spec.ts @@ -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" diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 364224ba202..cfdaa0eda11 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -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", diff --git a/packages/app/src/session/timeline/message-timeline.tsx b/packages/app/src/session/timeline/message-timeline.tsx index cb149399677..b6bc6f214d6 100644 --- a/packages/app/src/session/timeline/message-timeline.tsx +++ b/packages/app/src/session/timeline/message-timeline.tsx @@ -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 ( + {(id) => ( { /> + +
+ settings.general.setShowTurnNavigator(checked)} + hideLabel + > + {language.t("settings.experimental.row.turnNavigator.title")} + +
+
diff --git a/packages/app/src/settings/model.test.ts b/packages/app/src/settings/model.test.ts index 5b6676cad3e..4848ee78d6d 100644 --- a/packages/app/src/settings/model.test.ts +++ b/packages/app/src/settings/model.test.ts @@ -94,6 +94,7 @@ describe("settings schema", () => { timelineDetail: timelinePresets[2].value, showCustomAgents: false, showTurnDiffs: false, + showTurnNavigator: true, backgroundActivity: "left", generatedUI: true, mobileTitlebarPosition: "top", diff --git a/packages/app/src/settings/model.tsx b/packages/app/src/settings/model.tsx index e1ee13249e2..404b7436953 100644 --- a/packages/app/src/settings/model.tsx +++ b/packages/app/src/settings/model.tsx @@ -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, diff --git a/packages/app/src/settings/search-catalog.ts b/packages/app/src/settings/search-catalog.ts index 43395cbfd43..591dbd52af1 100644 --- a/packages/app/src/settings/search-catalog.ts +++ b/packages/app/src/settings/search-catalog.ts @@ -89,6 +89,13 @@ export const clientSettings: Entry[] = [ 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",