summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/utils
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-22 19:38:50 -0600
committerAdam <[email protected]>2025-12-22 19:39:00 -0600
commit794fe8f381c846f5241800363023d892c12cf495 (patch)
treebff98689edfa635a2a9f39cb4ea61639b97f5b2d /packages/desktop/src/utils
parenta4eebf9f08262f6bf63017710e2e6d9672ec6708 (diff)
downloadopencode-794fe8f381c846f5241800363023d892c12cf495.tar.gz
opencode-794fe8f381c846f5241800363023d892c12cf495.zip
chore: rename packages/desktop -> packages/app
Diffstat (limited to 'packages/desktop/src/utils')
-rw-r--r--packages/desktop/src/utils/dom.ts51
-rw-r--r--packages/desktop/src/utils/id.ts99
-rw-r--r--packages/desktop/src/utils/index.ts1
-rw-r--r--packages/desktop/src/utils/persist.ts26
-rw-r--r--packages/desktop/src/utils/prompt.ts47
-rw-r--r--packages/desktop/src/utils/solid-dnd.tsx55
-rw-r--r--packages/desktop/src/utils/speech.ts302
7 files changed, 0 insertions, 581 deletions
diff --git a/packages/desktop/src/utils/dom.ts b/packages/desktop/src/utils/dom.ts
deleted file mode 100644
index 4f3724c7c..000000000
--- a/packages/desktop/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/desktop/src/utils/id.ts b/packages/desktop/src/utils/id.ts
deleted file mode 100644
index fa27cf4c5..000000000
--- a/packages/desktop/src/utils/id.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import z from "zod"
-
-const prefixes = {
- session: "ses",
- message: "msg",
- permission: "per",
- user: "usr",
- part: "prt",
- pty: "pty",
-} as const
-
-const LENGTH = 26
-let lastTimestamp = 0
-let counter = 0
-
-type Prefix = keyof typeof prefixes
-export namespace Identifier {
- export function schema(prefix: Prefix) {
- return z.string().startsWith(prefixes[prefix])
- }
-
- export function ascending(prefix: Prefix, given?: string) {
- return generateID(prefix, false, given)
- }
-
- export function descending(prefix: Prefix, given?: string) {
- return generateID(prefix, true, given)
- }
-}
-
-function generateID(prefix: Prefix, descending: boolean, given?: string): string {
- if (!given) {
- return create(prefix, descending)
- }
-
- if (!given.startsWith(prefixes[prefix])) {
- throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
- }
-
- return given
-}
-
-function create(prefix: Prefix, descending: boolean, timestamp?: number): string {
- const currentTimestamp = timestamp ?? Date.now()
-
- if (currentTimestamp !== lastTimestamp) {
- lastTimestamp = currentTimestamp
- counter = 0
- }
-
- counter += 1
-
- let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
-
- if (descending) {
- now = ~now
- }
-
- const timeBytes = new Uint8Array(6)
- for (let i = 0; i < 6; i += 1) {
- timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
- }
-
- return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12)
-}
-
-function bytesToHex(bytes: Uint8Array): string {
- let hex = ""
- for (let i = 0; i < bytes.length; i += 1) {
- hex += bytes[i].toString(16).padStart(2, "0")
- }
- return hex
-}
-
-function randomBase62(length: number): string {
- const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
- const bytes = getRandomBytes(length)
- let result = ""
- for (let i = 0; i < length; i += 1) {
- result += chars[bytes[i] % 62]
- }
- return result
-}
-
-function getRandomBytes(length: number): Uint8Array {
- const bytes = new Uint8Array(length)
- const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : undefined
-
- if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
- cryptoObj.getRandomValues(bytes)
- return bytes
- }
-
- for (let i = 0; i < length; i += 1) {
- bytes[i] = Math.floor(Math.random() * 256)
- }
-
- return bytes
-}
diff --git a/packages/desktop/src/utils/index.ts b/packages/desktop/src/utils/index.ts
deleted file mode 100644
index d87053269..000000000
--- a/packages/desktop/src/utils/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./dom"
diff --git a/packages/desktop/src/utils/persist.ts b/packages/desktop/src/utils/persist.ts
deleted file mode 100644
index 12b334f9f..000000000
--- a/packages/desktop/src/utils/persist.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { usePlatform } from "@/context/platform"
-import { makePersisted } from "@solid-primitives/storage"
-import { createResource, type Accessor } from "solid-js"
-import type { SetStoreFunction, Store } from "solid-js/store"
-
-type InitType = Promise<string> | string | null
-type PersistedWithReady<T> = [Store<T>, SetStoreFunction<T>, InitType, Accessor<boolean>]
-
-export function persisted<T>(key: string, store: [Store<T>, SetStoreFunction<T>]): PersistedWithReady<T> {
- const platform = usePlatform()
- const [state, setState, init] = makePersisted(store, { name: key, storage: platform.storage?.() ?? localStorage })
-
- // Create a resource that resolves when the store is initialized
- // This integrates with Suspense and provides a ready signal
- const isAsync = init instanceof Promise
- const [ready] = createResource(
- () => init,
- async (initValue) => {
- if (initValue instanceof Promise) await initValue
- return true
- },
- { initialValue: !isAsync },
- )
-
- return [state, setState, init, () => ready() === true]
-}
diff --git a/packages/desktop/src/utils/prompt.ts b/packages/desktop/src/utils/prompt.ts
deleted file mode 100644
index 45c5ce1f3..000000000
--- a/packages/desktop/src/utils/prompt.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { Part, TextPart, FilePart } from "@opencode-ai/sdk/v2"
-import type { Prompt, FileAttachmentPart } from "@/context/prompt"
-
-/**
- * Extract prompt content from message parts for restoring into the prompt input.
- * This is used by undo to restore the original user prompt.
- */
-export function extractPromptFromParts(parts: Part[]): Prompt {
- const result: Prompt = []
- let position = 0
-
- for (const part of parts) {
- if (part.type === "text") {
- const textPart = part as TextPart
- if (!textPart.synthetic && textPart.text) {
- result.push({
- type: "text",
- content: textPart.text,
- start: position,
- end: position + textPart.text.length,
- })
- position += textPart.text.length
- }
- } else if (part.type === "file") {
- const filePart = part as FilePart
- if (filePart.source?.type === "file") {
- const path = filePart.source.path
- const content = "@" + path
- const attachment: FileAttachmentPart = {
- type: "file",
- path,
- content,
- start: position,
- end: position + content.length,
- }
- result.push(attachment)
- position += content.length
- }
- }
- }
-
- if (result.length === 0) {
- result.push({ type: "text", content: "", start: 0, end: 0 })
- }
-
- return result
-}
diff --git a/packages/desktop/src/utils/solid-dnd.tsx b/packages/desktop/src/utils/solid-dnd.tsx
deleted file mode 100644
index a634be4b4..000000000
--- a/packages/desktop/src/utils/solid-dnd.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useDragDropContext } from "@thisbeyond/solid-dnd"
-import { JSXElement } from "solid-js"
-import type { Transformer } from "@thisbeyond/solid-dnd"
-
-export const 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 ConstrainDragXAxis = (): JSXElement => {
- const context = useDragDropContext()
- if (!context) return <></>
- const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
- const transformer: Transformer = {
- id: "constrain-x-axis",
- order: 100,
- callback: (transform) => ({ ...transform, x: 0 }),
- }
- onDragStart((event) => {
- const id = getDraggableId(event)
- if (!id) return
- addTransformer("draggables", id, transformer)
- })
- onDragEnd((event) => {
- const id = getDraggableId(event)
- if (!id) return
- removeTransformer("draggables", id, transformer.id)
- })
- return <></>
-}
-
-export const ConstrainDragYAxis = (): JSXElement => {
- const context = useDragDropContext()
- if (!context) return <></>
- const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
- const transformer: Transformer = {
- id: "constrain-y-axis",
- order: 100,
- callback: (transform) => ({ ...transform, y: 0 }),
- }
- onDragStart((event) => {
- const id = getDraggableId(event)
- if (!id) return
- addTransformer("draggables", id, transformer)
- })
- onDragEnd((event) => {
- const id = getDraggableId(event)
- if (!id) return
- removeTransformer("draggables", id, transformer.id)
- })
- return <></>
-}
diff --git a/packages/desktop/src/utils/speech.ts b/packages/desktop/src/utils/speech.ts
deleted file mode 100644
index 921e0a159..000000000
--- a/packages/desktop/src/utils/speech.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-import { createSignal, onCleanup } from "solid-js"
-
-// 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 hasSupport =
- typeof window !== "undefined" &&
- Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
-
- const [isRecording, setIsRecording] = createSignal(false)
- const [committed, setCommitted] = createSignal("")
- const [interim, setInterim] = createSignal("")
-
- let recognition: Recognition | undefined
- let shouldContinue = false
- let committedText = ""
- let sessionCommitted = ""
- let pendingHypothesis = ""
- let lastInterimSuffix = ""
- let shrinkCandidate: string | undefined
- let commitTimer: number | undefined
-
- const cancelPendingCommit = () => {
- if (commitTimer === undefined) return
- clearTimeout(commitTimer)
- commitTimer = undefined
- }
-
- const commitSegment = (segment: string) => {
- const nextCommitted = appendSegment(committedText, segment)
- if (nextCommitted === committedText) return
- committedText = nextCommitted
- setCommitted(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
- setInterim("")
- if (opts?.onInterim) opts.onInterim("")
- }
-
- const applyInterim = (suffix: string, hypothesis: string) => {
- cancelPendingCommit()
- pendingHypothesis = hypothesis
- lastInterimSuffix = suffix
- shrinkCandidate = undefined
- setInterim(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
- setInterim("")
- if (opts?.onInterim) opts.onInterim("")
- }, COMMIT_DELAY)
- }
-
- if (hasSupport) {
- const Ctor: new () => Recognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition
-
- 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
- setInterim("")
- 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 }) => {
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- if (e.error === "no-speech" && shouldContinue) {
- setInterim("")
- if (opts?.onInterim) opts.onInterim("")
- setTimeout(() => {
- try {
- recognition?.start()
- } catch {}
- }, 150)
- return
- }
- shouldContinue = false
- setIsRecording(false)
- }
-
- recognition.onstart = () => {
- sessionCommitted = ""
- pendingHypothesis = ""
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setInterim("")
- if (opts?.onInterim) opts.onInterim("")
- setIsRecording(true)
- }
-
- recognition.onend = () => {
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setIsRecording(false)
- if (shouldContinue) {
- setTimeout(() => {
- try {
- recognition?.start()
- } catch {}
- }, 150)
- }
- }
- }
-
- const start = () => {
- if (!recognition) return
- shouldContinue = true
- sessionCommitted = ""
- pendingHypothesis = ""
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setInterim("")
- try {
- recognition.start()
- } catch {}
- }
-
- const stop = () => {
- if (!recognition) return
- shouldContinue = false
- promotePending()
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setInterim("")
- if (opts?.onInterim) opts.onInterim("")
- try {
- recognition.stop()
- } catch {}
- }
-
- onCleanup(() => {
- shouldContinue = false
- promotePending()
- cancelPendingCommit()
- lastInterimSuffix = ""
- shrinkCandidate = undefined
- setInterim("")
- if (opts?.onInterim) opts.onInterim("")
- try {
- recognition?.stop()
- } catch {}
- })
-
- return {
- isSupported: () => hasSupport,
- isRecording,
- committed,
- interim,
- start,
- stop,
- }
-}