From 0c710be628fa9380c3ec1192a9eb9b5b084bc011 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 11 Mar 2026 20:00:51 -0500
Subject: [PATCH] chore: cleanup
---
.../session/session-sortable-terminal-tab.tsx | 18 +-
.../app/src/components/settings-agents.tsx | 16 -
.../app/src/components/settings-commands.tsx | 16 -
packages/app/src/components/settings-mcp.tsx | 16 -
.../src/components/settings-permissions.tsx | 230 ------------
packages/app/src/context/global-sync.test.ts | 8 +-
packages/app/src/context/global-sync.tsx | 3 -
.../app/src/context/notification-index.ts | 66 ----
packages/app/src/context/notification.test.ts | 73 ----
packages/app/src/pages/layout.tsx | 6 +-
packages/app/src/pages/layout/helpers.test.ts | 16 +-
packages/app/src/pages/layout/helpers.ts | 8 -
.../layout/sidebar-project-helpers.test.ts | 63 ----
.../pages/layout/sidebar-project-helpers.ts | 11 -
.../app/src/pages/layout/sidebar-project.tsx | 18 +-
.../src/pages/layout/sidebar-shell-helpers.ts | 1 -
.../src/pages/layout/sidebar-shell.test.ts | 13 -
.../app/src/pages/layout/sidebar-shell.tsx | 3 +-
.../pages/layout/sidebar-workspace-helpers.ts | 2 -
.../pages/layout/sidebar-workspace.test.ts | 13 -
.../src/pages/layout/sidebar-workspace.tsx | 100 ++----
.../app/src/pages/session/composer/index.ts | 3 +-
.../composer/session-composer-helpers.ts | 10 -
.../composer/session-composer-state.test.ts | 2 +-
.../composer/session-composer-state.ts | 31 +-
packages/app/src/pages/session/helpers.ts | 56 +--
.../pages/session/session-command-helpers.ts | 10 -
.../pages/session/session-prompt-dock.test.ts | 22 --
.../pages/session/session-prompt-helpers.ts | 4 -
.../session/use-session-commands.test.ts | 44 ---
.../pages/session/use-session-commands.tsx | 15 +-
.../pages/session/use-session-hash-scroll.ts | 2 -
packages/app/src/utils/dom.ts | 51 ---
packages/app/src/utils/index.ts | 1 -
packages/app/src/utils/speech.ts | 326 ------------------
35 files changed, 75 insertions(+), 1202 deletions(-)
delete mode 100644 packages/app/src/components/settings-agents.tsx
delete mode 100644 packages/app/src/components/settings-commands.tsx
delete mode 100644 packages/app/src/components/settings-mcp.tsx
delete mode 100644 packages/app/src/components/settings-permissions.tsx
delete mode 100644 packages/app/src/context/notification-index.ts
delete mode 100644 packages/app/src/context/notification.test.ts
delete mode 100644 packages/app/src/pages/layout/sidebar-project-helpers.test.ts
delete mode 100644 packages/app/src/pages/layout/sidebar-project-helpers.ts
delete mode 100644 packages/app/src/pages/layout/sidebar-shell-helpers.ts
delete mode 100644 packages/app/src/pages/layout/sidebar-shell.test.ts
delete mode 100644 packages/app/src/pages/layout/sidebar-workspace-helpers.ts
delete mode 100644 packages/app/src/pages/layout/sidebar-workspace.test.ts
delete mode 100644 packages/app/src/pages/session/composer/session-composer-helpers.ts
delete mode 100644 packages/app/src/pages/session/session-command-helpers.ts
delete mode 100644 packages/app/src/pages/session/session-prompt-dock.test.ts
delete mode 100644 packages/app/src/pages/session/session-prompt-helpers.ts
delete mode 100644 packages/app/src/pages/session/use-session-commands.test.ts
delete mode 100644 packages/app/src/utils/dom.ts
delete mode 100644 packages/app/src/utils/index.ts
delete mode 100644 packages/app/src/utils/speech.ts
diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx
index 6fe6186d510..4f49911c127 100644
--- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx
+++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx
@@ -8,6 +8,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Icon } from "@opencode-ai/ui/icon"
import { useTerminal, type LocalPTY } from "@/context/terminal"
import { useLanguage } from "@/context/language"
+import { focusTerminalById } from "@/pages/session/helpers"
export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => void }): JSX.Element {
const terminal = useTerminal()
@@ -53,21 +54,8 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
const focus = () => {
if (store.editing) return
-
- if (document.activeElement instanceof HTMLElement) {
- document.activeElement.blur()
- }
- const wrapper = document.getElementById(`terminal-wrapper-${props.terminal.id}`)
- const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement
- if (!element) return
-
- const textarea = element.querySelector("textarea") as HTMLTextAreaElement
- if (textarea) {
- textarea.focus()
- return
- }
- element.focus()
- element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }))
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
+ focusTerminalById(props.terminal.id)
}
const edit = (e?: Event) => {
diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx
deleted file mode 100644
index 74a942f7770..00000000000
--- a/packages/app/src/components/settings-agents.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component } from "solid-js"
-import { useLanguage } from "@/context/language"
-
-export const SettingsAgents: Component = () => {
- // TODO: Replace this placeholder with full agents settings controls.
- const language = useLanguage()
-
- return (
-
-
-
{language.t("settings.agents.title")}
-
{language.t("settings.agents.description")}
-
-
- )
-}
diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx
deleted file mode 100644
index e158d231cee..00000000000
--- a/packages/app/src/components/settings-commands.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component } from "solid-js"
-import { useLanguage } from "@/context/language"
-
-export const SettingsCommands: Component = () => {
- // TODO: Replace this placeholder with full commands settings controls.
- const language = useLanguage()
-
- return (
-
-
-
{language.t("settings.commands.title")}
-
{language.t("settings.commands.description")}
-
-
- )
-}
diff --git a/packages/app/src/components/settings-mcp.tsx b/packages/app/src/components/settings-mcp.tsx
deleted file mode 100644
index 507e041aa89..00000000000
--- a/packages/app/src/components/settings-mcp.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component } from "solid-js"
-import { useLanguage } from "@/context/language"
-
-export const SettingsMcp: Component = () => {
- // TODO: Replace this placeholder with full MCP settings controls.
- const language = useLanguage()
-
- return (
-
-
-
{language.t("settings.mcp.title")}
-
{language.t("settings.mcp.description")}
-
-
- )
-}
diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx
deleted file mode 100644
index 5c922ba44a4..00000000000
--- a/packages/app/src/components/settings-permissions.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import { Select } from "@opencode-ai/ui/select"
-import { showToast } from "@opencode-ai/ui/toast"
-import { Component, For, createMemo, type JSX } from "solid-js"
-import { useGlobalSync } from "@/context/global-sync"
-import { useLanguage } from "@/context/language"
-
-type PermissionAction = "allow" | "ask" | "deny"
-
-type PermissionObject = Record
-type PermissionValue = PermissionAction | PermissionObject | string[] | undefined
-type PermissionMap = Record
-
-type PermissionItem = {
- id: string
- title: string
- description: string
-}
-
-const ACTIONS = [
- { value: "allow", label: "settings.permissions.action.allow" },
- { value: "ask", label: "settings.permissions.action.ask" },
- { value: "deny", label: "settings.permissions.action.deny" },
-] as const
-
-const ITEMS = [
- {
- id: "read",
- title: "settings.permissions.tool.read.title",
- description: "settings.permissions.tool.read.description",
- },
- {
- id: "edit",
- title: "settings.permissions.tool.edit.title",
- description: "settings.permissions.tool.edit.description",
- },
- {
- id: "glob",
- title: "settings.permissions.tool.glob.title",
- description: "settings.permissions.tool.glob.description",
- },
- {
- id: "grep",
- title: "settings.permissions.tool.grep.title",
- description: "settings.permissions.tool.grep.description",
- },
- {
- id: "list",
- title: "settings.permissions.tool.list.title",
- description: "settings.permissions.tool.list.description",
- },
- {
- id: "bash",
- title: "settings.permissions.tool.bash.title",
- description: "settings.permissions.tool.bash.description",
- },
- {
- id: "task",
- title: "settings.permissions.tool.task.title",
- description: "settings.permissions.tool.task.description",
- },
- {
- id: "skill",
- title: "settings.permissions.tool.skill.title",
- description: "settings.permissions.tool.skill.description",
- },
- {
- id: "lsp",
- title: "settings.permissions.tool.lsp.title",
- description: "settings.permissions.tool.lsp.description",
- },
- {
- id: "todoread",
- title: "settings.permissions.tool.todoread.title",
- description: "settings.permissions.tool.todoread.description",
- },
- {
- id: "todowrite",
- title: "settings.permissions.tool.todowrite.title",
- description: "settings.permissions.tool.todowrite.description",
- },
- {
- id: "webfetch",
- title: "settings.permissions.tool.webfetch.title",
- description: "settings.permissions.tool.webfetch.description",
- },
- {
- id: "websearch",
- title: "settings.permissions.tool.websearch.title",
- description: "settings.permissions.tool.websearch.description",
- },
- {
- id: "codesearch",
- title: "settings.permissions.tool.codesearch.title",
- description: "settings.permissions.tool.codesearch.description",
- },
- {
- id: "external_directory",
- title: "settings.permissions.tool.external_directory.title",
- description: "settings.permissions.tool.external_directory.description",
- },
- {
- id: "doom_loop",
- title: "settings.permissions.tool.doom_loop.title",
- description: "settings.permissions.tool.doom_loop.description",
- },
-] as const
-
-const VALID_ACTIONS = new Set(["allow", "ask", "deny"])
-
-function toMap(value: unknown): PermissionMap {
- if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap
-
- const action = getAction(value)
- if (action) return { "*": action }
-
- return {}
-}
-
-function getAction(value: unknown): PermissionAction | undefined {
- if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction
- return
-}
-
-function getRuleDefault(value: unknown): PermissionAction | undefined {
- const action = getAction(value)
- if (action) return action
-
- if (!value || typeof value !== "object" || Array.isArray(value)) return
-
- return getAction((value as Record)["*"])
-}
-
-export const SettingsPermissions: Component = () => {
- const globalSync = useGlobalSync()
- const language = useLanguage()
-
- const actions = createMemo(
- (): Array<{ value: PermissionAction; label: string }> =>
- ACTIONS.map((action) => ({
- value: action.value,
- label: language.t(action.label),
- })),
- )
-
- const permission = createMemo(() => {
- return toMap(globalSync.data.config.permission)
- })
-
- const actionFor = (id: string): PermissionAction => {
- const value = permission()[id]
- const direct = getRuleDefault(value)
- if (direct) return direct
-
- const wildcard = getRuleDefault(permission()["*"])
- if (wildcard) return wildcard
-
- return "allow"
- }
-
- const setPermission = async (id: string, action: PermissionAction) => {
- const before = globalSync.data.config.permission
- const map = toMap(before)
- const existing = map[id]
-
- const nextValue =
- existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action
-
- const rollback = (err: unknown) => {
- globalSync.set("config", "permission", before)
- const message = err instanceof Error ? err.message : String(err)
- showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message })
- }
-
- globalSync.set("config", "permission", { ...map, [id]: nextValue })
- globalSync.updateConfig({ permission: { [id]: nextValue } }).catch(rollback)
- }
-
- return (
-
- )
-}
-
-interface SettingsRowProps {
- title: string
- description: string
- children: JSX.Element
-}
-
-const SettingsRow: Component = (props) => {
- return (
-
-
- {props.title}
- {props.description}
-
-
{props.children}
-
- )
-}
diff --git a/packages/app/src/context/global-sync.test.ts b/packages/app/src/context/global-sync.test.ts
index 7956057fd09..93e9c417555 100644
--- a/packages/app/src/context/global-sync.test.ts
+++ b/packages/app/src/context/global-sync.test.ts
@@ -1,10 +1,6 @@
import { describe, expect, test } from "bun:test"
-import {
- canDisposeDirectory,
- estimateRootSessionTotal,
- loadRootSessionsWithFallback,
- pickDirectoriesToEvict,
-} from "./global-sync"
+import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction"
+import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
describe("pickDirectoriesToEvict", () => {
test("keeps pinned stores and evicts idle stores", () => {
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index 4090699a8bd..645bd678b7d 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -402,6 +402,3 @@ export function useGlobalSync() {
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
return context
}
-
-export { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction"
-export { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
diff --git a/packages/app/src/context/notification-index.ts b/packages/app/src/context/notification-index.ts
deleted file mode 100644
index 0b316e7ec10..00000000000
--- a/packages/app/src/context/notification-index.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-type NotificationIndexItem = {
- directory?: string
- session?: string
- viewed: boolean
- type: string
-}
-
-export function buildNotificationIndex(list: T[]) {
- const sessionAll = new Map()
- const sessionUnseen = new Map()
- const sessionUnseenCount = new Map()
- const sessionUnseenHasError = new Map()
- const projectAll = new Map()
- const projectUnseen = new Map()
- const projectUnseenCount = new Map()
- const projectUnseenHasError = new Map()
-
- for (const notification of list) {
- const session = notification.session
- if (session) {
- const all = sessionAll.get(session)
- if (all) all.push(notification)
- else sessionAll.set(session, [notification])
-
- if (!notification.viewed) {
- const unseen = sessionUnseen.get(session)
- if (unseen) unseen.push(notification)
- else sessionUnseen.set(session, [notification])
-
- sessionUnseenCount.set(session, (sessionUnseenCount.get(session) ?? 0) + 1)
- if (notification.type === "error") sessionUnseenHasError.set(session, true)
- }
- }
-
- const directory = notification.directory
- if (directory) {
- const all = projectAll.get(directory)
- if (all) all.push(notification)
- else projectAll.set(directory, [notification])
-
- if (!notification.viewed) {
- const unseen = projectUnseen.get(directory)
- if (unseen) unseen.push(notification)
- else projectUnseen.set(directory, [notification])
-
- projectUnseenCount.set(directory, (projectUnseenCount.get(directory) ?? 0) + 1)
- if (notification.type === "error") projectUnseenHasError.set(directory, true)
- }
- }
- }
-
- return {
- session: {
- all: sessionAll,
- unseen: sessionUnseen,
- unseenCount: sessionUnseenCount,
- unseenHasError: sessionUnseenHasError,
- },
- project: {
- all: projectAll,
- unseen: projectUnseen,
- unseenCount: projectUnseenCount,
- unseenHasError: projectUnseenHasError,
- },
- }
-}
diff --git a/packages/app/src/context/notification.test.ts b/packages/app/src/context/notification.test.ts
deleted file mode 100644
index 44bacb70493..00000000000
--- a/packages/app/src/context/notification.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { buildNotificationIndex } from "./notification-index"
-
-type Notification = {
- type: "turn-complete" | "error"
- session: string
- directory: string
- viewed: boolean
- time: number
-}
-
-const turn = (session: string, directory: string, viewed = false): Notification => ({
- type: "turn-complete",
- session,
- directory,
- viewed,
- time: 1,
-})
-
-const error = (session: string, directory: string, viewed = false): Notification => ({
- type: "error",
- session,
- directory,
- viewed,
- time: 1,
-})
-
-describe("buildNotificationIndex", () => {
- test("builds unseen counts and unseen error flags", () => {
- const list = [
- turn("s1", "d1", false),
- error("s1", "d1", false),
- turn("s1", "d1", true),
- turn("s2", "d1", false),
- error("s3", "d2", true),
- ]
-
- const index = buildNotificationIndex(list)
-
- expect(index.session.all.get("s1")?.length).toBe(3)
- expect(index.session.unseen.get("s1")?.length).toBe(2)
- expect(index.session.unseenCount.get("s1")).toBe(2)
- expect(index.session.unseenHasError.get("s1")).toBe(true)
-
- expect(index.session.unseenCount.get("s2")).toBe(1)
- expect(index.session.unseenHasError.get("s2") ?? false).toBe(false)
- expect(index.session.unseenCount.get("s3") ?? 0).toBe(0)
- expect(index.session.unseenHasError.get("s3") ?? false).toBe(false)
-
- expect(index.project.unseenCount.get("d1")).toBe(3)
- expect(index.project.unseenHasError.get("d1")).toBe(true)
- expect(index.project.unseenCount.get("d2") ?? 0).toBe(0)
- expect(index.project.unseenHasError.get("d2") ?? false).toBe(false)
- })
-
- test("updates selectors after viewed transitions", () => {
- const list = [turn("s1", "d1", false), error("s1", "d1", false), turn("s2", "d1", false)]
- const next = list.map((item) => (item.session === "s1" ? { ...item, viewed: true } : item))
-
- const before = buildNotificationIndex(list)
- const after = buildNotificationIndex(next)
-
- expect(before.session.unseenCount.get("s1")).toBe(2)
- expect(before.session.unseenHasError.get("s1")).toBe(true)
- expect(before.project.unseenCount.get("d1")).toBe(3)
- expect(before.project.unseenHasError.get("d1")).toBe(true)
-
- expect(after.session.unseenCount.get("s1") ?? 0).toBe(0)
- expect(after.session.unseenHasError.get("s1") ?? false).toBe(false)
- expect(after.project.unseenCount.get("d1")).toBe(1)
- expect(after.project.unseenHasError.get("d1") ?? false).toBe(false)
- })
-})
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 052a03c5491..daad100c355 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -51,7 +51,7 @@ import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { DialogSettings } from "@/components/dialog-settings"
import { useCommand, type CommandOption } from "@/context/command"
-import { ConstrainDragXAxis } from "@/utils/solid-dnd"
+import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { DebugBar } from "@/components/debug-bar"
@@ -62,7 +62,6 @@ import {
displayName,
effectiveWorkspaceOrder,
errorMessage,
- getDraggableId,
latestRootSession,
sortedRootSessions,
workspaceKey,
@@ -80,7 +79,6 @@ import {
WorkspaceDragOverlay,
type WorkspaceSidebarContext,
} from "./layout/sidebar-workspace"
-import { workspaceOpenState } from "./layout/sidebar-workspace-helpers"
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
import { SidebarContent } from "./layout/sidebar-shell"
@@ -1860,7 +1858,7 @@ export default function Layout(props: ParentProps) {
setEditor,
InlineEditor,
isBusy,
- workspaceExpanded: (directory, local) => workspaceOpenState(store.workspaceExpanded, directory, local),
+ workspaceExpanded: (directory, local) => store.workspaceExpanded[directory] ?? local,
setWorkspaceExpanded: (directory, value) => setStore("workspaceExpanded", directory, value),
showResetWorkspaceDialog: (root, directory) =>
dialog.show(() => ),
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index d1569dbd9a6..916b802147b 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -6,9 +6,15 @@ import {
parseDeepLink,
parseNewSessionDeepLink,
} from "./deep-links"
-import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
import { type Session } from "@opencode-ai/sdk/v2/client"
-import { hasProjectPermissions, latestRootSession } from "./helpers"
+import {
+ displayName,
+ errorMessage,
+ hasProjectPermissions,
+ latestRootSession,
+ syncWorkspaceOrder,
+ workspaceKey,
+} from "./helpers"
const session = (input: Partial & Pick) =>
({
@@ -192,12 +198,6 @@ describe("layout workspace helpers", () => {
expect(result?.id).toBe("root")
})
- test("extracts draggable id safely", () => {
- expect(getDraggableId({ draggable: { id: "x" } })).toBe("x")
- expect(getDraggableId({ draggable: { id: 42 } })).toBeUndefined()
- expect(getDraggableId(null)).toBeUndefined()
- })
-
test("formats fallback project display name", () => {
expect(displayName({ worktree: "/tmp/app" })).toBe("app")
expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App")
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 42315e5893c..8881b8a48f7 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -54,14 +54,6 @@ export const childMapByParent = (sessions: Session[]) => {
return map
}
-export function getDraggableId(event: unknown): string | undefined {
- if (typeof event !== "object" || event === null) return undefined
- if (!("draggable" in event)) return undefined
- const draggable = (event as { draggable?: { id?: unknown } }).draggable
- if (!draggable) return undefined
- return typeof draggable.id === "string" ? draggable.id : undefined
-}
-
export const displayName = (project: { name?: string; worktree: string }) =>
project.name || getFilename(project.worktree)
diff --git a/packages/app/src/pages/layout/sidebar-project-helpers.test.ts b/packages/app/src/pages/layout/sidebar-project-helpers.test.ts
deleted file mode 100644
index 75958d49e92..00000000000
--- a/packages/app/src/pages/layout/sidebar-project-helpers.test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { projectSelected, projectTileActive } from "./sidebar-project-helpers"
-
-describe("projectSelected", () => {
- test("matches direct worktree", () => {
- expect(projectSelected("/tmp/root", "/tmp/root")).toBe(true)
- })
-
- test("matches sandbox worktree", () => {
- expect(projectSelected("/tmp/branch", "/tmp/root", ["/tmp/branch"])).toBe(true)
- expect(projectSelected("/tmp/other", "/tmp/root", ["/tmp/branch"])).toBe(false)
- })
-})
-
-describe("projectTileActive", () => {
- test("menu state always wins", () => {
- expect(
- projectTileActive({
- menu: true,
- preview: false,
- open: false,
- overlay: false,
- worktree: "/tmp/root",
- }),
- ).toBe(true)
- })
-
- test("preview mode uses open state", () => {
- expect(
- projectTileActive({
- menu: false,
- preview: true,
- open: true,
- overlay: true,
- hoverProject: "/tmp/other",
- worktree: "/tmp/root",
- }),
- ).toBe(true)
- })
-
- test("overlay mode uses hovered project", () => {
- expect(
- projectTileActive({
- menu: false,
- preview: false,
- open: false,
- overlay: true,
- hoverProject: "/tmp/root",
- worktree: "/tmp/root",
- }),
- ).toBe(true)
- expect(
- projectTileActive({
- menu: false,
- preview: false,
- open: false,
- overlay: true,
- hoverProject: "/tmp/other",
- worktree: "/tmp/root",
- }),
- ).toBe(false)
- })
-})
diff --git a/packages/app/src/pages/layout/sidebar-project-helpers.ts b/packages/app/src/pages/layout/sidebar-project-helpers.ts
deleted file mode 100644
index 06d38a3cd1b..00000000000
--- a/packages/app/src/pages/layout/sidebar-project-helpers.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export const projectSelected = (currentDir: string, worktree: string, sandboxes?: string[]) =>
- worktree === currentDir || sandboxes?.includes(currentDir) === true
-
-export const projectTileActive = (args: {
- menu: boolean
- preview: boolean
- open: boolean
- overlay: boolean
- hoverProject?: string
- worktree: string
-}) => args.menu || (args.preview ? args.open : args.overlay && args.hoverProject === args.worktree)
diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx
index 187cd2f3353..551090fd5a6 100644
--- a/packages/app/src/pages/layout/sidebar-project.tsx
+++ b/packages/app/src/pages/layout/sidebar-project.tsx
@@ -12,7 +12,6 @@ import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification"
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
-import { projectSelected, projectTileActive } from "./sidebar-project-helpers"
export type ProjectSidebarContext = {
currentDir: Accessor
@@ -277,8 +276,10 @@ export const SortableProject = (props: {
const globalSync = useGlobalSync()
const language = useLanguage()
const sortable = createSortable(props.project.worktree)
- const selected = createMemo(() =>
- projectSelected(props.ctx.currentDir(), props.project.worktree, props.project.sandboxes),
+ const selected = createMemo(
+ () =>
+ props.project.worktree === props.ctx.currentDir() ||
+ props.project.sandboxes?.includes(props.ctx.currentDir()) === true,
)
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
@@ -291,15 +292,8 @@ export const SortableProject = (props: {
const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
- const active = createMemo(() =>
- projectTileActive({
- menu: state.menu,
- preview: preview(),
- open: state.open,
- overlay: overlay(),
- hoverProject: props.ctx.hoverProject(),
- worktree: props.project.worktree,
- }),
+ const active = createMemo(
+ () => state.menu || (preview() ? state.open : overlay() && props.ctx.hoverProject() === props.project.worktree),
)
createEffect(() => {
diff --git a/packages/app/src/pages/layout/sidebar-shell-helpers.ts b/packages/app/src/pages/layout/sidebar-shell-helpers.ts
deleted file mode 100644
index 93c286c1523..00000000000
--- a/packages/app/src/pages/layout/sidebar-shell-helpers.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const sidebarExpanded = (mobile: boolean | undefined, opened: boolean) => !!mobile || opened
diff --git a/packages/app/src/pages/layout/sidebar-shell.test.ts b/packages/app/src/pages/layout/sidebar-shell.test.ts
deleted file mode 100644
index 694025a6532..00000000000
--- a/packages/app/src/pages/layout/sidebar-shell.test.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { sidebarExpanded } from "./sidebar-shell-helpers"
-
-describe("sidebarExpanded", () => {
- test("expands on mobile regardless of desktop open state", () => {
- expect(sidebarExpanded(true, false)).toBe(true)
- })
-
- test("follows desktop open state when not mobile", () => {
- expect(sidebarExpanded(false, true)).toBe(true)
- expect(sidebarExpanded(false, false)).toBe(false)
- })
-})
diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx
index d3070e37491..82be4f02488 100644
--- a/packages/app/src/pages/layout/sidebar-shell.tsx
+++ b/packages/app/src/pages/layout/sidebar-shell.tsx
@@ -11,7 +11,6 @@ import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { type LocalProject } from "@/context/layout"
-import { sidebarExpanded } from "./sidebar-shell-helpers"
export const SidebarContent = (props: {
mobile?: boolean
@@ -33,7 +32,7 @@ export const SidebarContent = (props: {
onOpenHelp: () => void
renderPanel: () => JSX.Element
}): JSX.Element => {
- const expanded = createMemo(() => sidebarExpanded(props.mobile, props.opened()))
+ const expanded = createMemo(() => !!props.mobile || props.opened())
const placement = () => (props.mobile ? "bottom" : "right")
let panel: HTMLDivElement | undefined
diff --git a/packages/app/src/pages/layout/sidebar-workspace-helpers.ts b/packages/app/src/pages/layout/sidebar-workspace-helpers.ts
deleted file mode 100644
index aa7cb480e5e..00000000000
--- a/packages/app/src/pages/layout/sidebar-workspace-helpers.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const workspaceOpenState = (expanded: Record, directory: string, local: boolean) =>
- expanded[directory] ?? local
diff --git a/packages/app/src/pages/layout/sidebar-workspace.test.ts b/packages/app/src/pages/layout/sidebar-workspace.test.ts
deleted file mode 100644
index d71c39fc8bf..00000000000
--- a/packages/app/src/pages/layout/sidebar-workspace.test.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { workspaceOpenState } from "./sidebar-workspace-helpers"
-
-describe("workspaceOpenState", () => {
- test("defaults to local workspace open", () => {
- expect(workspaceOpenState({}, "/tmp/root", true)).toBe(true)
- })
-
- test("uses persisted expansion state when present", () => {
- expect(workspaceOpenState({ "/tmp/root": false }, "/tmp/root", true)).toBe(false)
- expect(workspaceOpenState({ "/tmp/branch": true }, "/tmp/branch", false)).toBe(true)
- })
-})
diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx
index c317b9c5efb..1a54fdd8adc 100644
--- a/packages/app/src/pages/layout/sidebar-workspace.tsx
+++ b/packages/app/src/pages/layout/sidebar-workspace.tsx
@@ -144,8 +144,6 @@ const WorkspaceActions = (props: {
setMenuOpen: (open: boolean) => void
setPendingRename: (value: boolean) => void
sidebarHovering: Accessor
- mobile?: boolean
- nav: Accessor
touch: Accessor
language: ReturnType
workspaceValue: Accessor
@@ -340,6 +338,22 @@ export const SortableWorkspace = (props: {
}
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
+ const header = () => (
+ workspaceStore.vcs?.branch}
+ workspaceValue={workspaceValue}
+ workspaceEditActive={workspaceEditActive}
+ InlineEditor={props.ctx.InlineEditor}
+ renameWorkspace={props.ctx.renameWorkspace}
+ setEditor={props.ctx.setEditor}
+ projectId={props.project.id}
+ />
+ )
const openWrapper = (value: boolean) => {
props.ctx.setWorkspaceExpanded(props.directory, value)
@@ -379,20 +393,7 @@ export const SortableWorkspace = (props: {
data-action="workspace-toggle"
data-workspace={base64Encode(props.directory)}
>
- workspaceStore.vcs?.branch}
- workspaceValue={workspaceValue}
- workspaceEditActive={workspaceEditActive}
- InlineEditor={props.ctx.InlineEditor}
- renameWorkspace={props.ctx.renameWorkspace}
- setEditor={props.ctx.setEditor}
- projectId={props.project.id}
- />
+ {header()}
}
>
@@ -401,20 +402,7 @@ export const SortableWorkspace = (props: {
menu.open ? "pr-16" : "pr-2"
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
>
- workspaceStore.vcs?.branch}
- workspaceValue={workspaceValue}
- workspaceEditActive={workspaceEditActive}
- InlineEditor={props.ctx.InlineEditor}
- renameWorkspace={props.ctx.renameWorkspace}
- setEditor={props.ctx.setEditor}
- projectId={props.project.id}
- />
+ {header()}
setMenu("open", open)}
setPendingRename={(value) => setMenu("pendingRename", value)}
sidebarHovering={props.ctx.sidebarHovering}
- mobile={props.mobile}
- nav={props.ctx.nav}
touch={touch}
language={language}
workspaceValue={workspaceValue}
@@ -490,44 +476,18 @@ export const LocalWorkspace = (props: {
ref={(el) => props.ctx.setScrollContainerRef(el, props.mobile)}
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]"
>
-
+ false}
+ loading={loading}
+ sessions={sessions}
+ children={children}
+ hasMore={hasMore}
+ loadMore={loadMore}
+ language={language}
+ />
)
}
diff --git a/packages/app/src/pages/session/composer/index.ts b/packages/app/src/pages/session/composer/index.ts
index e244a15363a..b0069de53fb 100644
--- a/packages/app/src/pages/session/composer/index.ts
+++ b/packages/app/src/pages/session/composer/index.ts
@@ -1,3 +1,2 @@
export { SessionComposerRegion } from "./session-composer-region"
-export { createSessionComposerBlocked, createSessionComposerState } from "./session-composer-state"
-export type { SessionComposerState } from "./session-composer-state"
+export { createSessionComposerState } from "./session-composer-state"
diff --git a/packages/app/src/pages/session/composer/session-composer-helpers.ts b/packages/app/src/pages/session/composer/session-composer-helpers.ts
deleted file mode 100644
index 90c238af46d..00000000000
--- a/packages/app/src/pages/session/composer/session-composer-helpers.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const todoState = (input: {
- count: number
- done: boolean
- live: boolean
-}): "hide" | "clear" | "open" | "close" => {
- if (input.count === 0) return "hide"
- if (!input.live) return "clear"
- if (!input.done) return "open"
- return "close"
-}
diff --git a/packages/app/src/pages/session/composer/session-composer-state.test.ts b/packages/app/src/pages/session/composer/session-composer-state.test.ts
index f7c11715c2b..c27454f7e18 100644
--- a/packages/app/src/pages/session/composer/session-composer-state.test.ts
+++ b/packages/app/src/pages/session/composer/session-composer-state.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
-import { todoState } from "./session-composer-helpers"
+import { todoState } from "./session-composer-state"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
const session = (input: { id: string; parentID?: string }) =>
diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts
index a007e4c8491..525766dcfae 100644
--- a/packages/app/src/pages/session/composer/session-composer-state.ts
+++ b/packages/app/src/pages/session/composer/session-composer-state.ts
@@ -8,30 +8,21 @@ import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
-import { todoState } from "./session-composer-helpers"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
-const idle = { type: "idle" as const }
-
-export function createSessionComposerBlocked() {
- const params = useParams()
- const permission = usePermission()
- const sdk = useSDK()
- const sync = useSync()
- const permissionRequest = createMemo(() =>
- sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
- return !permission.autoResponds(item, sdk.directory)
- }),
- )
- const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id))
-
- return createMemo(() => {
- const id = params.id
- if (!id) return false
- return !!permissionRequest() || !!questionRequest()
- })
+export const todoState = (input: {
+ count: number
+ done: boolean
+ live: boolean
+}): "hide" | "clear" | "open" | "close" => {
+ if (input.count === 0) return "hide"
+ if (!input.live) return "clear"
+ if (!input.done) return "open"
+ return "close"
}
+const idle = { type: "idle" as const }
+
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
const params = useParams()
const sdk = useSDK()
diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts
index be9656900d3..2da5ce6b82d 100644
--- a/packages/app/src/pages/session/helpers.ts
+++ b/packages/app/src/pages/session/helpers.ts
@@ -1,4 +1,4 @@
-import { batch, createEffect, on, onCleanup, onMount, type Accessor } from "solid-js"
+import { batch, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
export const focusTerminalById = (id: string) => {
@@ -117,57 +117,3 @@ export const createSizing = () => {
}
export type Sizing = ReturnType
-
-export const createPresence = (open: Accessor, wait = 200) => {
- const [state, setState] = createStore({
- show: open(),
- open: open(),
- })
- let frame: number | undefined
- let t: number | undefined
-
- const clear = () => {
- if (frame !== undefined) {
- cancelAnimationFrame(frame)
- frame = undefined
- }
- if (t !== undefined) {
- clearTimeout(t)
- t = undefined
- }
- }
-
- createEffect(
- on(open, (next) => {
- clear()
-
- if (next) {
- if (state.show) {
- setState("open", true)
- return
- }
-
- setState({ show: true, open: false })
- frame = requestAnimationFrame(() => {
- frame = undefined
- setState("open", true)
- })
- return
- }
-
- if (!state.show) return
- setState("open", false)
- t = window.setTimeout(() => {
- t = undefined
- setState("show", false)
- }, wait)
- }),
- )
-
- onCleanup(clear)
-
- return {
- show: () => state.show,
- open: () => state.open,
- }
-}
diff --git a/packages/app/src/pages/session/session-command-helpers.ts b/packages/app/src/pages/session/session-command-helpers.ts
deleted file mode 100644
index b71a7b76883..00000000000
--- a/packages/app/src/pages/session/session-command-helpers.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const canAddSelectionContext = (input: {
- active?: string
- pathFromTab: (tab: string) => string | undefined
- selectedLines: (path: string) => unknown
-}) => {
- if (!input.active) return false
- const path = input.pathFromTab(input.active)
- if (!path) return false
- return input.selectedLines(path) != null
-}
diff --git a/packages/app/src/pages/session/session-prompt-dock.test.ts b/packages/app/src/pages/session/session-prompt-dock.test.ts
deleted file mode 100644
index b3a9945d66c..00000000000
--- a/packages/app/src/pages/session/session-prompt-dock.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { questionSubtitle } from "./session-prompt-helpers"
-
-describe("questionSubtitle", () => {
- const t = (key: string) => {
- if (key === "ui.common.question.one") return "question"
- if (key === "ui.common.question.other") return "questions"
- return key
- }
-
- test("returns empty for zero", () => {
- expect(questionSubtitle(0, t)).toBe("")
- })
-
- test("uses singular label", () => {
- expect(questionSubtitle(1, t)).toBe("1 question")
- })
-
- test("uses plural label", () => {
- expect(questionSubtitle(3, t)).toBe("3 questions")
- })
-})
diff --git a/packages/app/src/pages/session/session-prompt-helpers.ts b/packages/app/src/pages/session/session-prompt-helpers.ts
deleted file mode 100644
index ac3234c939a..00000000000
--- a/packages/app/src/pages/session/session-prompt-helpers.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const questionSubtitle = (count: number, t: (key: string) => string) => {
- if (count === 0) return ""
- return `${count} ${t(count > 1 ? "ui.common.question.other" : "ui.common.question.one")}`
-}
diff --git a/packages/app/src/pages/session/use-session-commands.test.ts b/packages/app/src/pages/session/use-session-commands.test.ts
deleted file mode 100644
index ada1871e1c0..00000000000
--- a/packages/app/src/pages/session/use-session-commands.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { canAddSelectionContext } from "./session-command-helpers"
-
-describe("canAddSelectionContext", () => {
- test("returns false without active tab", () => {
- expect(
- canAddSelectionContext({
- active: undefined,
- pathFromTab: () => "src/a.ts",
- selectedLines: () => ({ start: 1, end: 1 }),
- }),
- ).toBe(false)
- })
-
- test("returns false when active tab is not a file", () => {
- expect(
- canAddSelectionContext({
- active: "context",
- pathFromTab: () => undefined,
- selectedLines: () => ({ start: 1, end: 1 }),
- }),
- ).toBe(false)
- })
-
- test("returns false without selected lines", () => {
- expect(
- canAddSelectionContext({
- active: "file://src/a.ts",
- pathFromTab: () => "src/a.ts",
- selectedLines: () => null,
- }),
- ).toBe(false)
- })
-
- test("returns true when file and selection exist", () => {
- expect(
- canAddSelectionContext({
- active: "file://src/a.ts",
- pathFromTab: () => "src/a.ts",
- selectedLines: () => ({ start: 1, end: 2 }),
- }),
- ).toBe(true)
- })
-})
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index b8ddeda8235..ea3b5ec5742 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -19,7 +19,6 @@ import { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/util/array"
import { extractPromptFromParts } from "@/utils/prompt"
import { UserMessage } from "@opencode-ai/sdk/v2"
-import { canAddSelectionContext } from "@/pages/session/session-command-helpers"
export type SessionCommandContext = {
navigateMessageByOffset: (offset: number) => void
@@ -84,6 +83,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
prompt.context.add({ type: "file", path, selection, preview })
}
+ const canAddSelectionContext = () => {
+ const active = tabs().active()
+ if (!active) return false
+ const path = file.pathFromTab(active)
+ if (!path) return false
+ return file.selectedLines(path) != null
+ }
+
const navigateMessageByOffset = actions.navigateMessageByOffset
const setActiveMessage = actions.setActiveMessage
const focusInput = actions.focusInput
@@ -136,11 +143,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
title: language.t("command.context.addSelection"),
description: language.t("command.context.addSelection.description"),
keybind: "mod+shift+l",
- disabled: !canAddSelectionContext({
- active: tabs().active(),
- pathFromTab: file.pathFromTab,
- selectedLines: file.selectedLines,
- }),
+ disabled: !canAddSelectionContext(),
onSelect: () => {
const active = tabs().active()
if (!active) return
diff --git a/packages/app/src/pages/session/use-session-hash-scroll.ts b/packages/app/src/pages/session/use-session-hash-scroll.ts
index 1ea6a302b95..5fadb1f22a0 100644
--- a/packages/app/src/pages/session/use-session-hash-scroll.ts
+++ b/packages/app/src/pages/session/use-session-hash-scroll.ts
@@ -3,8 +3,6 @@ import { useLocation, useNavigate } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { messageIdFromHash } from "./message-id-from-hash"
-export { messageIdFromHash } from "./message-id-from-hash"
-
export const useSessionHashScroll = (input: {
sessionKey: () => string
sessionID: () => string | undefined
diff --git a/packages/app/src/utils/dom.ts b/packages/app/src/utils/dom.ts
deleted file mode 100644
index 4f3724c7c95..00000000000
--- a/packages/app/src/utils/dom.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-export function getCharacterOffsetInLine(lineElement: Element, targetNode: Node, offset: number): number {
- const r = document.createRange()
- r.selectNodeContents(lineElement)
- r.setEnd(targetNode, offset)
- return r.toString().length
-}
-
-export function getNodeOffsetInLine(lineElement: Element, charIndex: number): { node: Node; offset: number } | null {
- const walker = document.createTreeWalker(lineElement, NodeFilter.SHOW_TEXT, null)
- let remaining = Math.max(0, charIndex)
- let lastText: Node | null = null
- let lastLen = 0
- let node: Node | null
- while ((node = walker.nextNode())) {
- const len = node.textContent?.length || 0
- lastText = node
- lastLen = len
- if (remaining <= len) return { node, offset: remaining }
- remaining -= len
- }
- if (lastText) return { node: lastText, offset: lastLen }
- if (lineElement.firstChild) return { node: lineElement.firstChild, offset: 0 }
- return null
-}
-
-export function getSelectionInContainer(
- container: HTMLElement,
-): { sl: number; sch: number; el: number; ech: number } | null {
- const s = window.getSelection()
- if (!s || s.rangeCount === 0) return null
- const r = s.getRangeAt(0)
- const sc = r.startContainer
- const ec = r.endContainer
- const getLineElement = (n: Node) =>
- (n.nodeType === Node.TEXT_NODE ? (n.parentElement as Element) : (n as Element))?.closest(".line")
- const sle = getLineElement(sc)
- const ele = getLineElement(ec)
- if (!sle || !ele) return null
- if (!container.contains(sle as Node) || !container.contains(ele as Node)) return null
- const cc = container.querySelector("code") as HTMLElement | null
- if (!cc) return null
- const lines = Array.from(cc.querySelectorAll(".line"))
- const sli = lines.indexOf(sle as Element)
- const eli = lines.indexOf(ele as Element)
- if (sli === -1 || eli === -1) return null
- const sl = sli + 1
- const el = eli + 1
- const sch = getCharacterOffsetInLine(sle as Element, sc, r.startOffset)
- const ech = getCharacterOffsetInLine(ele as Element, ec, r.endOffset)
- return { sl, sch, el, ech }
-}
diff --git a/packages/app/src/utils/index.ts b/packages/app/src/utils/index.ts
deleted file mode 100644
index d87053269df..00000000000
--- a/packages/app/src/utils/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./dom"
diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts
deleted file mode 100644
index 52fc46b6931..00000000000
--- a/packages/app/src/utils/speech.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-import { onCleanup } from "solid-js"
-import { createStore } from "solid-js/store"
-import { getSpeechRecognitionCtor } from "@/utils/runtime-adapters"
-
-// Minimal types to avoid relying on non-standard DOM typings
-type RecognitionResult = {
- 0: { transcript: string }
- isFinal: boolean
-}
-
-type RecognitionEvent = {
- results: RecognitionResult[]
- resultIndex: number
-}
-
-interface Recognition {
- continuous: boolean
- interimResults: boolean
- lang: string
- start: () => void
- stop: () => void
- onresult: ((e: RecognitionEvent) => void) | null
- onerror: ((e: { error: string }) => void) | null
- onend: (() => void) | null
- onstart: (() => void) | null
-}
-
-const COMMIT_DELAY = 250
-
-const appendSegment = (base: string, addition: string) => {
- const trimmed = addition.trim()
- if (!trimmed) return base
- if (!base) return trimmed
- const needsSpace = /\S$/.test(base) && !/^[,.;!?]/.test(trimmed)
- return `${base}${needsSpace ? " " : ""}${trimmed}`
-}
-
-const extractSuffix = (committed: string, hypothesis: string) => {
- const cleanHypothesis = hypothesis.trim()
- if (!cleanHypothesis) return ""
- const baseTokens = committed.trim() ? committed.trim().split(/\s+/) : []
- const hypothesisTokens = cleanHypothesis.split(/\s+/)
- let index = 0
- while (
- index < baseTokens.length &&
- index < hypothesisTokens.length &&
- baseTokens[index] === hypothesisTokens[index]
- ) {
- index += 1
- }
- if (index < baseTokens.length) return ""
- return hypothesisTokens.slice(index).join(" ")
-}
-
-export function createSpeechRecognition(opts?: {
- lang?: string
- onFinal?: (text: string) => void
- onInterim?: (text: string) => void
-}) {
- const ctor = getSpeechRecognitionCtor(typeof window === "undefined" ? undefined : window)
- const hasSupport = Boolean(ctor)
-
- const [store, setStore] = createStore({
- isRecording: false,
- committed: "",
- interim: "",
- })
-
- const isRecording = () => store.isRecording
- const committed = () => store.committed
- const interim = () => store.interim
-
- let recognition: Recognition | undefined
- let shouldContinue = false
- let committedText = ""
- let sessionCommitted = ""
- let pendingHypothesis = ""
- let lastInterimSuffix = ""
- let shrinkCandidate: string | undefined
- let commitTimer: number | undefined
- let restartTimer: number | undefined
-
- const cancelPendingCommit = () => {
- if (commitTimer === undefined) return
- clearTimeout(commitTimer)
- commitTimer = undefined
- }
-
- const clearRestart = () => {
- if (restartTimer === undefined) return
- window.clearTimeout(restartTimer)
- restartTimer = undefined
- }
-
- const scheduleRestart = () => {
- clearRestart()
- if (!shouldContinue) return
- if (!recognition) return
- restartTimer = window.setTimeout(() => {
- restartTimer = undefined
- if (!shouldContinue) return
- if (!recognition) return
- try {
- recognition.start()
- } catch {}
- }, 150)
- }
-
- const commitSegment = (segment: string) => {
- const nextCommitted = appendSegment(committedText, segment)
- if (nextCommitted === committedText) return
- committedText = nextCommitted
- setStore("committed", committedText)
- if (opts?.onFinal) opts.onFinal(segment.trim())
- }
-
- const promotePending = () => {
- if (!pendingHypothesis) return
- const suffix = extractSuffix(sessionCommitted, pendingHypothesis)
- if (!suffix) {
- pendingHypothesis = ""
- return
- }
- sessionCommitted = appendSegment(sessionCommitted, suffix)
- commitSegment(suffix)
- pendingHypothesis = ""
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- }
-
- const applyInterim = (suffix: string, hypothesis: string) => {
- cancelPendingCommit()
- pendingHypothesis = hypothesis
- lastInterimSuffix = suffix
- shrinkCandidate = undefined
- setStore("interim", suffix)
- if (opts?.onInterim) {
- opts.onInterim(suffix ? appendSegment(committedText, suffix) : "")
- }
- if (!suffix) return
- const snapshot = hypothesis
- commitTimer = window.setTimeout(() => {
- if (pendingHypothesis !== snapshot) return
- const currentSuffix = extractSuffix(sessionCommitted, pendingHypothesis)
- if (!currentSuffix) return
- sessionCommitted = appendSegment(sessionCommitted, currentSuffix)
- commitSegment(currentSuffix)
- pendingHypothesis = ""
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- }, COMMIT_DELAY)
- }
-
- if (ctor) {
- recognition = new ctor()
- recognition.continuous = false
- recognition.interimResults = true
- recognition.lang = opts?.lang || (typeof navigator !== "undefined" ? navigator.language : "en-US")
-
- recognition.onresult = (event: RecognitionEvent) => {
- if (!event.results.length) return
-
- let aggregatedFinal = ""
- let latestHypothesis = ""
-
- for (let i = 0; i < event.results.length; i += 1) {
- const result = event.results[i]
- const transcript = (result[0]?.transcript || "").trim()
- if (!transcript) continue
- if (result.isFinal) {
- aggregatedFinal = appendSegment(aggregatedFinal, transcript)
- } else {
- latestHypothesis = transcript
- }
- }
-
- if (aggregatedFinal) {
- cancelPendingCommit()
- const finalSuffix = extractSuffix(sessionCommitted, aggregatedFinal)
- if (finalSuffix) {
- sessionCommitted = appendSegment(sessionCommitted, finalSuffix)
- commitSegment(finalSuffix)
- }
- pendingHypothesis = ""
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- return
- }
-
- cancelPendingCommit()
-
- if (!latestHypothesis) {
- shrinkCandidate = undefined
- applyInterim("", "")
- return
- }
-
- const suffix = extractSuffix(sessionCommitted, latestHypothesis)
-
- if (!suffix) {
- if (!lastInterimSuffix) {
- shrinkCandidate = undefined
- applyInterim("", latestHypothesis)
- return
- }
- if (shrinkCandidate === "") {
- applyInterim("", latestHypothesis)
- return
- }
- shrinkCandidate = ""
- pendingHypothesis = latestHypothesis
- return
- }
-
- if (lastInterimSuffix && suffix.length < lastInterimSuffix.length) {
- if (shrinkCandidate === suffix) {
- applyInterim(suffix, latestHypothesis)
- return
- }
- shrinkCandidate = suffix
- pendingHypothesis = latestHypothesis
- return
- }
-
- shrinkCandidate = undefined
- applyInterim(suffix, latestHypothesis)
- }
-
- recognition.onerror = (e: { error: string }) => {
- clearRestart()
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- if (e.error === "no-speech" && shouldContinue) {
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- scheduleRestart()
- return
- }
- shouldContinue = false
- setStore("isRecording", false)
- }
-
- recognition.onstart = () => {
- clearRestart()
- sessionCommitted = ""
- pendingHypothesis = ""
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- setStore("isRecording", true)
- }
-
- recognition.onend = () => {
- clearRestart()
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("isRecording", false)
- if (shouldContinue) {
- scheduleRestart()
- }
- }
- }
-
- const start = () => {
- if (!recognition) return
- clearRestart()
- shouldContinue = true
- sessionCommitted = ""
- pendingHypothesis = ""
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- try {
- recognition.start()
- } catch {}
- }
-
- const stop = () => {
- if (!recognition) return
- shouldContinue = false
- clearRestart()
- promotePending()
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- try {
- recognition.stop()
- } catch {}
- }
-
- onCleanup(() => {
- shouldContinue = false
- clearRestart()
- promotePending()
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setStore("interim", "")
- if (opts?.onInterim) opts.onInterim("")
- try {
- recognition?.stop()
- } catch {}
- })
-
- return {
- isSupported: () => hasSupport,
- isRecording,
- committed,
- interim,
- start,
- stop,
- }
-}