From 4f07aecf185e07ac69ecbf2cb4d8ac89e8afb16f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 29 Sep 2025 02:12:28 -0400 Subject: [PATCH] sync --- bun.lock | 3 + packages/opencode/package.json | 1 + .../cmd/tui/component/dialog-session-list.tsx | 29 +++++- .../opencode/src/cli/cmd/tui/context/sync.tsx | 12 +++ .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 3 +- .../src/cli/cmd/tui/ui/dialog-select.tsx | 8 +- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 2 +- packages/opencode/src/session/index.ts | 12 +-- packages/sdk/js/src/gen/client/client.gen.ts | 98 +++++++++++++------ packages/sdk/js/src/gen/client/types.gen.ts | 12 +-- packages/sdk/js/src/gen/client/utils.gen.ts | 12 ++- .../js/src/gen/core/serverSentEvents.gen.ts | 29 +++++- packages/sdk/js/src/gen/core/types.gen.ts | 19 ++-- packages/sdk/js/src/gen/core/utils.gen.ts | 30 +++++- packages/sdk/js/src/gen/sdk.gen.ts | 2 +- 15 files changed, 202 insertions(+), 70 deletions(-) diff --git a/bun.lock b/bun.lock index 6599b8420..ca8303a35 100644 --- a/bun.lock +++ b/bun.lock @@ -184,6 +184,7 @@ "@types/yargs": "17.0.33", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", + "why-is-node-running": "3.2.2", "zod-to-json-schema": "3.24.5", }, }, @@ -2679,6 +2680,8 @@ "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 436528062..c3e0f8071 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -26,6 +26,7 @@ "@types/yargs": "17.0.33", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", + "why-is-node-running": "3.2.2", "zod-to-json-schema": "3.24.5" }, "randomField": "added_by_user_request", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 4463b93db..5a9bc7f6a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -2,14 +2,19 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" -import { createMemo, onMount } from "solid-js" +import { createMemo, createSignal, onMount } from "solid-js" import { Locale } from "@/util/locale" import { Keybind } from "@/util/keybind" +import { Theme } from "../context/theme" +import { useSDK } from "../context/sdk" export function DialogSessionList() { const dialog = useDialog() const sync = useSync() const route = useRoute() + const sdk = useSDK() + + const [toDelete, setToDelete] = createSignal() const options = createMemo(() => { const today = new Date().toDateString() @@ -21,8 +26,10 @@ export function DialogSessionList() { if (category === today) { category = "Today" } + const isDeleting = toDelete() === x.id return { - title: x.title, + title: isDeleting ? "Press delete again to confirm" : x.title, + bg: isDeleting ? Theme.error : undefined, value: x.id, category, footer: Locale.time(x.time.updated), @@ -39,6 +46,9 @@ export function DialogSessionList() { title="Sessions" options={options()} limit={50} + onMove={() => { + setToDelete(undefined) + }} onSelect={(option) => { route.navigate({ type: "session", @@ -48,9 +58,20 @@ export function DialogSessionList() { }} keybind={[ { - keybind: Keybind.parse("del")[0], + keybind: Keybind.parse("delete")[0], title: "delete", - onTrigger: () => {}, + onTrigger: async (option) => { + if (toDelete() === option.value) { + sdk.session.delete({ + path: { + id: option.value, + }, + }) + setToDelete(undefined) + return + } + setToDelete(option.value) + }, }, ]} /> diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0eabcf2f1..7653bd60f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -44,6 +44,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "todo.updated": setStore("todo", event.properties.sessionID, event.properties.todos) break + case "session.deleted": { + const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) + if (result.found) { + setStore( + "session", + produce((draft) => { + draft.splice(result.index, 1) + }), + ) + } + break + } case "session.updated": const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) if (result.found) { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 2363c96d8..021331484 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -4,6 +4,7 @@ import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" import { For } from "solid-js" import { useKeyboard } from "@opentui/solid" +import { Locale } from "@/util/locale" export type DialogConfirmProps = { title: string @@ -51,7 +52,7 @@ export function DialogConfirm(props: DialogConfirmProps) { dialog.clear() }} > - {key} + {Locale.titlecase(key)} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 9863439e9..c8144c62e 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -13,6 +13,7 @@ import { Keybind } from "@/util/keybind" export interface DialogSelectProps { title: string options: DialogSelectOption[] + onMove?: (option: DialogSelectOption) => void onFilter?: (query: string) => void onSelect?: (option: DialogSelectOption) => void keybind?: { @@ -31,6 +32,7 @@ export interface DialogSelectOption { footer?: string category?: string disabled?: boolean + bg?: string onSelect?: (ctx: DialogContext) => void } @@ -87,6 +89,7 @@ export function DialogSelect(props: DialogSelectProps) { function moveTo(next: number) { setStore("selected", next) + props.onMove?.(selected()!) const target = scroll.getChildren().find((child) => { return child.id === JSON.stringify(selected()?.value) }) @@ -117,7 +120,8 @@ export function DialogSelect(props: DialogSelectProps) { for (const item of props.keybind ?? []) { if (Keybind.match(item.keybind, keybind.parse(evt))) { - item.onTrigger(selected()) + const s = selected() + if (s) item.onTrigger(s) } } }) @@ -183,7 +187,7 @@ export function DialogSelect(props: DialogSelectProps) { if (index === -1) return moveTo(index) }} - backgroundColor={active() ? Theme.primary : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={active() ? (option.bg ?? Theme.primary) : RGBA.fromInts(0, 0, 0, 0)} paddingLeft={1} paddingRight={1} > diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 03ae2d86f..513e827cf 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -124,7 +124,7 @@ export function DialogProvider(props: ParentProps) { {(item, index) => ( - + {item.element} )} diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index cce1cf8c6..e6ba7779b 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -242,12 +242,12 @@ export namespace Session { return result } - export async function remove(sessionID: string, emitEvent = true) { + export async function remove(sessionID: string) { const project = Instance.project try { const session = await get(sessionID) for (const child of await children(sessionID)) { - await remove(child.id, false) + await remove(child.id) } await unshare(sessionID).catch(() => {}) for (const msg of await Storage.list(["message", sessionID])) { @@ -257,11 +257,9 @@ export namespace Session { await Storage.remove(msg) } await Storage.remove(["session", project.id, sessionID]) - if (emitEvent) { - Bus.publish(Event.Deleted, { - info: session, - }) - } + Bus.publish(Event.Deleted, { + info: session, + }) } catch (e) { log.error(e) } diff --git a/packages/sdk/js/src/gen/client/client.gen.ts b/packages/sdk/js/src/gen/client/client.gen.ts index 34a8d0bec..aab8586ee 100644 --- a/packages/sdk/js/src/gen/client/client.gen.ts +++ b/packages/sdk/js/src/gen/client/client.gen.ts @@ -1,6 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts import { createSseClient } from "../core/serverSentEvents.gen.js" +import type { HttpMethod } from "../core/types.gen.js" +import { getValidRequestBody } from "../core/utils.gen.js" import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js" import { buildUrl, @@ -49,12 +51,12 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts) } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body) } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === "") { + if (opts.body === undefined || opts.serializedBody === "") { opts.headers.delete("Content-Type") } @@ -69,7 +71,7 @@ export const createClient = (config: Config = {}): Client => { const requestInit: ReqInit = { redirect: "follow", ...opts, - body: opts.serializedBody, + body: getValidRequestBody(opts), } let request = new Request(url, requestInit) @@ -97,18 +99,36 @@ export const createClient = (config: Config = {}): Client => { } if (response.ok) { + const parseAs = + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" + if (response.status === 204 || response.headers.get("Content-Length") === "0") { + let emptyData: any + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "text": + emptyData = await response[parseAs]() + break + case "formData": + emptyData = new FormData() + break + case "stream": + emptyData = response.body + break + case "json": + default: + emptyData = {} + break + } return opts.responseStyle === "data" - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, } } - const parseAs = - (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" - let data: any switch (parseAs) { case "arrayBuffer": @@ -178,35 +198,53 @@ export const createClient = (config: Config = {}): Client => { } } - const makeMethod = (method: Required["method"]) => { - const fn = (options: RequestOptions) => request({ ...options, method }) - fn.sse = async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options) - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - url, - }) - } - return fn + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }) + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options) + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init) + for (const fn of interceptors.request._fns) { + if (fn) { + request = await fn(request, opts) + } + } + return request + }, + url, + }) } return { buildUrl, - connect: makeMethod("CONNECT"), - delete: makeMethod("DELETE"), - get: makeMethod("GET"), + connect: makeMethodFn("CONNECT"), + delete: makeMethodFn("DELETE"), + get: makeMethodFn("GET"), getConfig, - head: makeMethod("HEAD"), + head: makeMethodFn("HEAD"), interceptors, - options: makeMethod("OPTIONS"), - patch: makeMethod("PATCH"), - post: makeMethod("POST"), - put: makeMethod("PUT"), + options: makeMethodFn("OPTIONS"), + patch: makeMethodFn("PATCH"), + post: makeMethodFn("POST"), + put: makeMethodFn("PUT"), request, setConfig, - trace: makeMethod("TRACE"), + sse: { + connect: makeSseFn("CONNECT"), + delete: makeSseFn("DELETE"), + get: makeSseFn("GET"), + head: makeSseFn("HEAD"), + options: makeSseFn("OPTIONS"), + patch: makeSseFn("PATCH"), + post: makeSseFn("POST"), + put: makeSseFn("PUT"), + trace: makeSseFn("TRACE"), + }, + trace: makeMethodFn("TRACE"), } as Client } diff --git a/packages/sdk/js/src/gen/client/types.gen.ts b/packages/sdk/js/src/gen/client/types.gen.ts index db8e544cf..f16d2cd57 100644 --- a/packages/sdk/js/src/gen/client/types.gen.ts +++ b/packages/sdk/js/src/gen/client/types.gen.ts @@ -20,7 +20,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType + fetch?: typeof fetch /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -128,7 +128,7 @@ export interface ClientOptions { throwOnError?: boolean } -type MethodFnBase = < +type MethodFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, @@ -137,7 +137,7 @@ type MethodFnBase = < options: Omit, "method">, ) => RequestResult -type MethodFnServerSentEvents = < +type SseFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, @@ -146,10 +146,6 @@ type MethodFnServerSentEvents = < options: Omit, "method">, ) => Promise> -type MethodFn = MethodFnBase & { - sse: MethodFnServerSentEvents -} - type RequestFn = < TData = unknown, TError = unknown, @@ -171,7 +167,7 @@ type BuildUrlFn = < options: Pick & Options, ) => string -export type Client = CoreClient & { +export type Client = CoreClient & { interceptors: Middleware } diff --git a/packages/sdk/js/src/gen/client/utils.gen.ts b/packages/sdk/js/src/gen/client/utils.gen.ts index 209bfbe8e..71d52f776 100644 --- a/packages/sdk/js/src/gen/client/utils.gen.ts +++ b/packages/sdk/js/src/gen/client/utils.gen.ts @@ -162,14 +162,22 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config } +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = [] + headers.forEach((value, key) => { + entries.push([key, value]) + }) + return entries +} + export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { const mergedHeaders = new Headers() for (const header of headers) { - if (!header || typeof header !== "object") { + if (!header) { continue } - const iterator = header instanceof Headers ? header.entries() : Object.entries(header) + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) for (const [key, value] of iterator) { if (value === null) { diff --git a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts index 8f7fac549..09ef3fb39 100644 --- a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts @@ -4,6 +4,17 @@ import type { Config } from "./types.gen.js" export type ServerSentEventsOptions = Omit & Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise /** * Callback invoked when a network or parsing error occurs during streaming. * @@ -21,6 +32,7 @@ export type ServerSentEventsOptions = Omit) => void + serializedBody?: RequestInit["body"] /** * Default retry delay in milliseconds. * @@ -64,6 +76,7 @@ export type ServerSentEventsResult({ + onRequest, onSseError, onSseEvent, responseTransformer, @@ -99,7 +112,21 @@ export const createSseClient = ({ } try { - const response = await fetch(url, { ...options, headers, signal }) + const requestInit: RequestInit = { + redirect: "follow", + ...options, + body: options.serializedBody, + headers, + signal, + } + let request = new Request(url, requestInit) + if (onRequest) { + request = await onRequest(url, requestInit) + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch + const response = await _fetch(request) if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) diff --git a/packages/sdk/js/src/gen/core/types.gen.ts b/packages/sdk/js/src/gen/core/types.gen.ts index 16408b2d0..bfa77b8ac 100644 --- a/packages/sdk/js/src/gen/core/types.gen.ts +++ b/packages/sdk/js/src/gen/core/types.gen.ts @@ -3,24 +3,19 @@ import type { Auth, AuthToken } from "./auth.gen.js" import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js" -export interface Client { +export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace" + +export type Client = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn - connect: MethodFn - delete: MethodFn - get: MethodFn getConfig: () => Config - head: MethodFn - options: MethodFn - patch: MethodFn - post: MethodFn - put: MethodFn request: RequestFn setConfig: (config: Config) => Config - trace: MethodFn -} +} & { + [K in HttpMethod]: MethodFn +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) export interface Config { /** @@ -47,7 +42,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE" + method?: Uppercase /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/packages/sdk/js/src/gen/core/utils.gen.ts b/packages/sdk/js/src/gen/core/utils.gen.ts index be18c608a..8a45f7269 100644 --- a/packages/sdk/js/src/gen/core/utils.gen.ts +++ b/packages/sdk/js/src/gen/core/utils.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { QuerySerializer } from "./bodySerializer.gen.js" +import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen.js" import { type ArraySeparatorStyle, serializeArrayParam, @@ -107,3 +107,31 @@ export const getUrl = ({ } return url } + +export function getValidRequestBody(options: { + body?: unknown + bodySerializer?: BodySerializer | null + serializedBody?: unknown +}) { + const hasBody = options.body !== undefined + const isSerializedBody = hasBody && options.bodySerializer + + if (isSerializedBody) { + if ("serializedBody" in options) { + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "" + + return hasSerializedBody ? options.serializedBody : null + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== "" ? options.body : null + } + + // plain/text body + if (hasBody) { + return options.body + } + + // no body was provided + return undefined +} diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index a28ae5572..da1529c88 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -676,7 +676,7 @@ class Event extends _HeyApiClient { * Get events */ public subscribe(options?: Options) { - return (options?.client ?? this._client).get.sse({ + return (options?.client ?? this._client).sse.get({ url: "/event", ...options, })