Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGESET.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ Newest first. Each entry names the upstream range brought in.

### Features

- **#348** Add a configurable `message_window` setting for each session's loaded transcript, applying it consistently to message fetch, hydration reconciliation and part cleanup, and live-update trimming; keep recall-backed search independent of the loaded window and document the default and memory trade-off (swxtchio/swx-opencode#65). _Fork-only._
- **#297** Show the selected Ctrl+r history entry's complete original prompt in a wrapped, page-scrollable preview while
keeping all other entries as one-line summaries. ENTER still appends the selected original prompt to the existing
composer draft (swxtchio/swx-opencode#296). _Fork-only._
Expand Down
33 changes: 18 additions & 15 deletions packages/opencode/test/cli/serve/serve-questions-sync.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import path from "node:path"
import { testRender } from "@opentui/solid"
import { onMount } from "solid-js"
import { createOpencodeClient, type GlobalEvent } from "@opencode-ai/sdk/v2"
import { TuiConfig } from "@opencode-ai/tui/config"
import { ArgsProvider } from "@opencode-ai/tui/context/args"
import { ExitProvider } from "@opencode-ai/tui/context/exit"
import { KVProvider } from "@opencode-ai/tui/context/kv"
Expand Down Expand Up @@ -145,21 +146,23 @@ const attach = Effect.fn("attach")(function* (url: string, directory: string) {
>
<TuiTerminalEnvironmentProvider value={{ platform: "linux" }}>
<TuiStartupProvider value={{ skipInitialLoading: false }}>
<ArgsProvider>
<KVProvider>
<SDKProvider url={url} directory={directory} events={relay.source}>
<PermissionProvider>
<ProjectProvider>
<ExitProvider exit={() => {}}>
<SyncProvider>
<Probe />
</SyncProvider>
</ExitProvider>
</ProjectProvider>
</PermissionProvider>
</SDKProvider>
</KVProvider>
</ArgsProvider>
<TuiConfig.TuiConfigProvider config={TuiConfig.resolve({}, { terminalSuspend: process.platform !== "win32" })}>
<ArgsProvider>
<KVProvider>
<SDKProvider url={url} directory={directory} events={relay.source}>
<PermissionProvider>
<ProjectProvider>
<ExitProvider exit={() => {}}>
<SyncProvider>
<Probe />
</SyncProvider>
</ExitProvider>
</ProjectProvider>
</PermissionProvider>
</SDKProvider>
</KVProvider>
</ArgsProvider>
</TuiConfig.TuiConfigProvider>
</TuiStartupProvider>
</TuiTerminalEnvironmentProvider>
</TuiPathsProvider>
Expand Down
14 changes: 14 additions & 0 deletions packages/opencode/test/config/tui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ it.instance("loads the prompt enhancement model from the dedicated tui config",
),
)

it.instance("loads the message window from the dedicated tui config", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const test = yield* TestInstance

expect((yield* getTuiConfig(test.directory)).message_window).toBe(100)
yield* fs.writeJson(path.join(test.directory, "tui.json"), { message_window: 175 })

expect((yield* getTuiConfig(test.directory)).message_window).toBe(175)
}),
),
)

it.instance("resolves attention config defaults and overrides", () =>
withCleanState(
Effect.gen(function* () {
Expand Down
12 changes: 11 additions & 1 deletion packages/tui/src/config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export const LeaderTimeoutDefault = 2000
export const LeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({
description: "Leader key timeout in milliseconds",
})
export const MessageWindowDefault = 100
export const MessageWindow = Schema.Int.check(Schema.isGreaterThan(0)).annotate({
description: "Maximum number of messages retained in each session's loaded transcript",
})

export const ScrollSpeed = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))
export const ScrollAcceleration = Schema.Struct({
Expand Down Expand Up @@ -67,6 +71,7 @@ export const Info = Schema.Struct({
keybinds: Schema.optional(TuiKeybind.KeybindOverrides),
plugin: Schema.optional(Schema.Array(PluginSpec)),
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
message_window: Schema.optional(MessageWindow),
leader_timeout: Schema.optional(LeaderTimeout),
attention: Schema.optional(Attention),
prompt: Schema.optional(Prompt),
Expand All @@ -79,7 +84,10 @@ export const Info = Schema.Struct({
})
export type Info = Schema.Schema.Type<typeof Info>

export type Resolved = Omit<Info, "attention" | "keybinds" | "leader_timeout" | "mouse" | "cursor"> & {
export type Resolved = Omit<
Info,
"attention" | "keybinds" | "leader_timeout" | "mouse" | "cursor" | "message_window"
> & {
attention: {
enabled: boolean
notifications: boolean
Expand All @@ -90,6 +98,7 @@ export type Resolved = Omit<Info, "attention" | "keybinds" | "leader_timeout" |
}
keybinds: TuiKeybind.BindingLookupView
leader_timeout: number
message_window: number
mouse: boolean
cursor?: {
style: "block" | "underline" | "line" | "default"
Expand Down Expand Up @@ -129,6 +138,7 @@ export function resolve(input: Info, options: ResolveOptions): Resolved {
bindingDefaults: TuiKeybind.bindingDefaults(),
}),
leader_timeout: input.leader_timeout ?? LeaderTimeoutDefault,
message_window: input.message_window ?? MessageWindowDefault,
mouse: input.mouse ?? true,
cursor: input.cursor
? {
Expand Down
10 changes: 6 additions & 4 deletions packages/tui/src/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useTuiStartup } from "./runtime"
import { createSimpleContext } from "./helper"
import { useExit } from "./exit"
import { useArgs } from "./args"
import { useTuiConfig } from "../config"
import { batch, onMount } from "solid-js"
import path from "path"
import { useKV } from "./kv"
Expand Down Expand Up @@ -84,6 +85,7 @@ export const {
name: "Sync",
init: () => {
const startup = useTuiStartup()
const tuiConfig = useTuiConfig()
const kv = useKV()
const permission = usePermission()
const [store, setStore] = createStore<{
Expand Down Expand Up @@ -376,7 +378,7 @@ export const {
}),
)
const updated = store.message[event.properties.info.sessionID]
if (updated.length > 100) {
if (updated.length > tuiConfig.message_window) {
const oldest = updated[0]
batch(() => {
setStore(
Expand Down Expand Up @@ -669,7 +671,7 @@ export const {
const task = (async () => {
const [session, messages, todo, diff, queue] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.messages({ sessionID, limit: tuiConfig.message_window }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
sdk.client.session.queue.list({ sessionID }),
Expand Down Expand Up @@ -697,8 +699,8 @@ export const {
),
)
infos.sort(compareMessage)
const removed = infos.slice(0, -100)
const visible = infos.slice(-100)
const removed = infos.slice(0, -tuiConfig.message_window)
const visible = infos.slice(-tuiConfig.message_window)
const visibleIDs = new Set(visible.map((message) => message.id))
for (const message of messages.data ?? []) {
if (!visibleIDs.has(message.info.id)) {
Expand Down
37 changes: 21 additions & 16 deletions packages/tui/test/cli/cmd/tui/sync-fixture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { testRender } from "@opentui/solid"
import type { JSX } from "@opentui/solid"
import { onMount } from "solid-js"
import { ArgsProvider } from "../../../../src/context/args"
import { TuiConfigProvider } from "../../../../src/config"
import { KVProvider, useKV } from "../../../../src/context/kv"
import { ProjectProvider, useProject } from "../../../../src/context/project"
import { SDKProvider } from "../../../../src/context/sdk"
Expand All @@ -11,6 +12,7 @@ import { PermissionProvider } from "../../../../src/context/permission"
import { ExitProvider } from "../../../../src/context/exit"
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
import { TestTuiContexts } from "../../../fixture/tui-environment"
import { createTuiResolvedConfig } from "../../../fixture/tui-runtime"
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"

export async function wait(fn: () => boolean, timeout = 2000) {
Expand All @@ -28,6 +30,7 @@ export async function mount(
state?: string,
waitForReady = true,
children?: () => JSX.Element,
tuiConfig = createTuiResolvedConfig(),
) {
const calls = createFetch(override)
const events = createEventSource()
Expand Down Expand Up @@ -56,22 +59,24 @@ export async function mount(

const app = await testRender(() => (
<TestTuiContexts paths={state ? { state } : undefined}>
<ArgsProvider>
<KVProvider>
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
<PermissionProvider>
<ProjectProvider>
<ExitProvider exit={() => {}}>
<SyncProvider>
<Probe />
<Extra />
</SyncProvider>
</ExitProvider>
</ProjectProvider>
</PermissionProvider>
</SDKProvider>
</KVProvider>
</ArgsProvider>
<TuiConfigProvider config={tuiConfig}>
<ArgsProvider>
<KVProvider>
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
<PermissionProvider>
<ProjectProvider>
<ExitProvider exit={() => {}}>
<SyncProvider>
<Probe />
<Extra />
</SyncProvider>
</ExitProvider>
</ProjectProvider>
</PermissionProvider>
</SDKProvider>
</KVProvider>
</ArgsProvider>
</TuiConfigProvider>
</TestTuiContexts>
))

Expand Down
Loading
Loading