From 17b81643e7532cd3081340aa205c8151f509a728 Mon Sep 17 00:00:00 2001 From: Github Action Date: Wed, 26 Nov 2025 23:33:08 +0000 Subject: [PATCH 01/22] Update Nix flake.lock and hashes --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 231ac606b099..b0749bea4f0c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764081664, - "narHash": "sha256-sUoHmPr/EwXzRMpv1u/kH+dXuvJEyyF2Q7muE+t0EU4=", + "lastModified": 1764138170, + "narHash": "sha256-2bCmfCUZyi2yj9FFXYKwsDiaZmizN75cLhI/eWmf3tk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dc205f7b4fdb04c8b7877b43edb7b73be7730081", + "rev": "bb813de6d2241bcb1b5af2d3059f560c66329967", "type": "github" }, "original": { From 71e4bdf1f099cd76215c76d032254d6a5a052616 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 27 Nov 2025 00:34:19 +0100 Subject: [PATCH 02/22] feat(tui): implement search functionality in session view - Added a search input component to allow users to search through messages. - Implemented logic to highlight search matches and navigate between them. - Updated the theme syntax rules to include strikethrough styling. - Enhanced message rendering to support search highlighting for text and markdown parts. --- .../cli/cmd/tui/component/prompt/search.tsx | 234 ++++++++++++++ .../src/cli/cmd/tui/context/theme.tsx | 14 +- .../src/cli/cmd/tui/routes/session/index.tsx | 300 ++++++++++++++++-- 3 files changed, 524 insertions(+), 24 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/prompt/search.tsx diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/search.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/search.tsx new file mode 100644 index 000000000000..0158f2b7653d --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/search.tsx @@ -0,0 +1,234 @@ +import { BoxRenderable, TextareaRenderable, type KeyBinding } from "@opentui/core" +import { createEffect, createMemo, createSignal, type JSX, onMount, Show } from "solid-js" +import { useTheme } from "@tui/context/theme" +import { EmptyBorder } from "@tui/component/border" +import { createStore } from "solid-js/store" +import { useKeybind } from "@tui/context/keybind" +import { Locale } from "@/util/locale" +import { useLocal } from "@tui/context/local" +import { RGBA } from "@opentui/core" +import { useSDK } from "@tui/context/sdk" +import { useSync } from "@tui/context/sync" +import { useExit } from "../../context/exit" + +export type SearchInputProps = { + disabled?: boolean + onSubmit?: (query: string) => void + onExit?: () => void + onInput?: (query: string) => void + onNext?: () => void + onPrevious?: () => void + matchInfo?: { current: number; total: number } + sessionID?: string + ref?: (ref: SearchInputRef) => void + placeholder?: string +} + +export type SearchInputRef = { + focused: boolean + reset(): void + blur(): void + focus(): void + getValue(): string +} + +export function SearchInput(props: SearchInputProps) { + let input: TextareaRenderable + let anchor: BoxRenderable + + const exit = useExit() + const keybind = useKeybind() + const local = useLocal() + const sdk = useSDK() + const sync = useSync() + const { theme } = useTheme() + + const highlight = createMemo(() => { + const agent = local.agent.current() + if (agent?.color) return RGBA.fromHex(agent.color) + const agents = local.agent.list() + const index = agents.findIndex((x) => x.name === "search") + const colors = [theme.secondary, theme.accent, theme.success, theme.warning, theme.primary, theme.error] + if (index === -1) return colors[0] + return colors[index % colors.length] + }) + + const textareaKeybindings = createMemo(() => { + const submitBindings = keybind.all.input_submit || [] + return [ + { name: "return", action: "submit" }, + ...submitBindings.map((binding) => ({ + name: binding.name, + ctrl: binding.ctrl || undefined, + meta: binding.meta || undefined, + shift: binding.shift || undefined, + action: "submit" as const, + })), + ] satisfies KeyBinding[] + }) + + const [store, setStore] = createStore<{ + input: string + }>({ + input: "", + }) + + createEffect(() => { + if (props.disabled) input.cursorColor = theme.backgroundElement + if (!props.disabled) input.cursorColor = theme.primary + }) + + props.ref?.({ + get focused() { + return input.focused + }, + focus() { + input.focus() + }, + blur() { + input.blur() + }, + reset() { + input.clear() + setStore("input", "") + }, + getValue() { + return store.input + }, + }) + + function submit() { + if (props.disabled) return + if (!store.input) return + props.onSubmit?.(store.input) + input.clear() + setStore("input", "") + } + + onMount(() => { + input.focus() + }) + + return ( + <> + (anchor = r)}> + + +