diff --git a/bun.lock b/bun.lock index ff4f144470..aad651621c 100644 --- a/bun.lock +++ b/bun.lock @@ -135,13 +135,11 @@ "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "4.3.3", - "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "0.3.0", "luxon": "catalog:", "marked": "16.2.0", "marked-shiki": "1.2.1", @@ -248,7 +246,6 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", "decimal.js": "10.5.0", @@ -460,7 +457,7 @@ "ai": "5.0.97", "diff": "8.0.2", "fuzzysort": "3.1.0", - "hono": "4.10.7", + "hono": "4.7.10", "hono-openapi": "1.1.1", "luxon": "3.6.1", "remeda": "2.26.0", @@ -1509,8 +1506,6 @@ "@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="], - "@solid-primitives/websocket": ["@solid-primitives/websocket@1.3.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-F06tA2FKa5VsnS4E4WEc3jHpsJfXRlMTGOtolugTzCqV3JmJTyvk9UVg1oz6PgGHKGi1CQ91OP8iW34myyJgaQ=="], - "@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="], "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], @@ -1895,8 +1890,6 @@ "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], - "bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="], - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], @@ -2341,8 +2334,6 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="], - "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], @@ -2437,7 +2428,7 @@ "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], - "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], + "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], "hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="], diff --git a/flake.lock b/flake.lock index ca9fd5f8f3..4e7cf41e1b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764794580, - "narHash": "sha256-UMVihg0OQ980YqmOAPz+zkuCEb9hpE5Xj2v+ZGNjQ+M=", + "lastModified": 1764733908, + "narHash": "sha256-QJiih52NU+nm7XQWCj+K8SwUdIEayDQ1FQgjkYISt4I=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ebc94f855ef25347c314258c10393a92794e7ab9", + "rev": "cadcc8de247676e4751c9d4a935acb2c0b059113", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 6bc2eaec15..47634e2ed8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-Wrfwnmo0lpck2rbt6ttkAuDGvBvqqWJfNA8QDQxoZ6I=" + "nodeModules": "sha256-ZGKC7h4ScHDzVYj8qb1lN/weZhyZivPS8kpNAZvgO0I=" } diff --git a/package.json b/package.json index a962be9260..a5e7c14621 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "ai": "5.0.97", - "hono": "4.10.7", + "hono": "4.7.10", "hono-openapi": "1.1.1", "fuzzysort": "3.1.0", "luxon": "3.6.1", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index addff88b04..f2f8768cbb 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -33,13 +33,11 @@ "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "4.3.3", - "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "0.3.0", "luxon": "catalog:", "marked": "16.2.0", "marked-shiki": "1.2.1", diff --git a/packages/desktop/src/addons/serialize.ts b/packages/desktop/src/addons/serialize.ts deleted file mode 100644 index 03899ff109..0000000000 --- a/packages/desktop/src/addons/serialize.ts +++ /dev/null @@ -1,649 +0,0 @@ -/** - * SerializeAddon - Serialize terminal buffer contents - * - * Port of xterm.js addon-serialize for ghostty-web. - * Enables serialization of terminal contents to a string that can - * be written back to restore terminal state. - * - * Usage: - * ```typescript - * const serializeAddon = new SerializeAddon(); - * term.loadAddon(serializeAddon); - * const content = serializeAddon.serialize(); - * ``` - */ - -import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web" - -// ============================================================================ -// Buffer Types (matching ghostty-web internal interfaces) -// ============================================================================ - -interface IBuffer { - readonly type: "normal" | "alternate" - readonly cursorX: number - readonly cursorY: number - readonly viewportY: number - readonly baseY: number - readonly length: number - getLine(y: number): IBufferLine | undefined - getNullCell(): IBufferCell -} - -interface IBufferLine { - readonly length: number - readonly isWrapped: boolean - getCell(x: number): IBufferCell | undefined - translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string -} - -interface IBufferCell { - getChars(): string - getCode(): number - getWidth(): number - getFgColorMode(): number - getBgColorMode(): number - getFgColor(): number - getBgColor(): number - isBold(): number - isItalic(): number - isUnderline(): number - isStrikethrough(): number - isBlink(): number - isInverse(): number - isInvisible(): number - isFaint(): number - isDim(): boolean -} - -// ============================================================================ -// Types -// ============================================================================ - -export interface ISerializeOptions { - /** - * The row range to serialize. When an explicit range is specified, the cursor - * will get its final repositioning. - */ - range?: ISerializeRange - /** - * The number of rows in the scrollback buffer to serialize, starting from - * the bottom of the scrollback buffer. When not specified, all available - * rows in the scrollback buffer will be serialized. - */ - scrollback?: number - /** - * Whether to exclude the terminal modes from the serialization. - * Default: false - */ - excludeModes?: boolean - /** - * Whether to exclude the alt buffer from the serialization. - * Default: false - */ - excludeAltBuffer?: boolean -} - -export interface ISerializeRange { - /** - * The line to start serializing (inclusive). - */ - start: number - /** - * The line to end serializing (inclusive). - */ - end: number -} - -export interface IHTMLSerializeOptions { - /** - * The number of rows in the scrollback buffer to serialize, starting from - * the bottom of the scrollback buffer. - */ - scrollback?: number - /** - * Whether to only serialize the selection. - * Default: false - */ - onlySelection?: boolean - /** - * Whether to include the global background of the terminal. - * Default: false - */ - includeGlobalBackground?: boolean - /** - * The range to serialize. This is prioritized over onlySelection. - */ - range?: { - startLine: number - endLine: number - startCol: number - } -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -function constrain(value: number, low: number, high: number): number { - return Math.max(low, Math.min(value, high)) -} - -function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { - return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor() -} - -function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean { - return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor() -} - -function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { - return ( - !!cell1.isInverse() === !!cell2.isInverse() && - !!cell1.isBold() === !!cell2.isBold() && - !!cell1.isUnderline() === !!cell2.isUnderline() && - !!cell1.isBlink() === !!cell2.isBlink() && - !!cell1.isInvisible() === !!cell2.isInvisible() && - !!cell1.isItalic() === !!cell2.isItalic() && - !!cell1.isDim() === !!cell2.isDim() && - !!cell1.isStrikethrough() === !!cell2.isStrikethrough() - ) -} - -// ============================================================================ -// Base Serialize Handler -// ============================================================================ - -abstract class BaseSerializeHandler { - constructor(protected readonly _buffer: IBuffer) {} - - public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string { - let oldCell = this._buffer.getNullCell() - - const startRow = range.start.y - const endRow = range.end.y - const startColumn = range.start.x - const endColumn = range.end.x - - this._beforeSerialize(endRow - startRow, startRow, endRow) - - for (let row = startRow; row <= endRow; row++) { - const line = this._buffer.getLine(row) - if (line) { - const startLineColumn = row === range.start.y ? startColumn : 0 - const endLineColumn = row === range.end.y ? endColumn : line.length - for (let col = startLineColumn; col < endLineColumn; col++) { - const c = line.getCell(col) - if (!c) { - continue - } - this._nextCell(c, oldCell, row, col) - oldCell = c - } - } - this._rowEnd(row, row === endRow) - } - - this._afterSerialize() - - return this._serializeString(excludeFinalCursorPosition) - } - - protected _nextCell(_cell: IBufferCell, _oldCell: IBufferCell, _row: number, _col: number): void {} - protected _rowEnd(_row: number, _isLastRow: boolean): void {} - protected _beforeSerialize(_rows: number, _startRow: number, _endRow: number): void {} - protected _afterSerialize(): void {} - protected _serializeString(_excludeFinalCursorPosition?: boolean): string { - return "" - } -} - -// ============================================================================ -// String Serialize Handler -// ============================================================================ - -class StringSerializeHandler extends BaseSerializeHandler { - private _rowIndex: number = 0 - private _allRows: string[] = [] - private _allRowSeparators: string[] = [] - private _currentRow: string = "" - private _nullCellCount: number = 0 - private _cursorStyle: IBufferCell - private _cursorStyleRow: number = 0 - private _cursorStyleCol: number = 0 - private _backgroundCell: IBufferCell - private _firstRow: number = 0 - private _lastCursorRow: number = 0 - private _lastCursorCol: number = 0 - private _lastContentCursorRow: number = 0 - private _lastContentCursorCol: number = 0 - private _thisRowLastChar: IBufferCell - private _thisRowLastSecondChar: IBufferCell - private _nextRowFirstChar: IBufferCell - - constructor( - buffer: IBuffer, - private readonly _terminal: ITerminalCore, - ) { - super(buffer) - this._cursorStyle = this._buffer.getNullCell() - this._backgroundCell = this._buffer.getNullCell() - this._thisRowLastChar = this._buffer.getNullCell() - this._thisRowLastSecondChar = this._buffer.getNullCell() - this._nextRowFirstChar = this._buffer.getNullCell() - } - - protected _beforeSerialize(rows: number, start: number, _end: number): void { - this._allRows = new Array(rows) - this._lastContentCursorRow = start - this._lastCursorRow = start - this._firstRow = start - } - - protected _rowEnd(row: number, isLastRow: boolean): void { - // if there is colorful empty cell at line end, we must pad it back - if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) { - this._currentRow += `\u001b[${this._nullCellCount}X` - } - - let rowSeparator = "" - - if (!isLastRow) { - // Enable BCE - if (row - this._firstRow >= this._terminal.rows) { - const line = this._buffer.getLine(this._cursorStyleRow) - const cell = line?.getCell(this._cursorStyleCol) - if (cell) { - this._backgroundCell = cell - } - } - - const currentLine = this._buffer.getLine(row)! - const nextLine = this._buffer.getLine(row + 1)! - - if (!nextLine.isWrapped) { - rowSeparator = "\r\n" - this._lastCursorRow = row + 1 - this._lastCursorCol = 0 - } else { - rowSeparator = "" - const thisRowLastChar = currentLine.getCell(currentLine.length - 1) - const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2) - const nextRowFirstChar = nextLine.getCell(0) - - if (thisRowLastChar) this._thisRowLastChar = thisRowLastChar - if (thisRowLastSecondChar) this._thisRowLastSecondChar = thisRowLastSecondChar - if (nextRowFirstChar) this._nextRowFirstChar = nextRowFirstChar - - const isNextRowFirstCharDoubleWidth = this._nextRowFirstChar.getWidth() > 1 - - let isValid = false - - if ( - this._nextRowFirstChar.getChars() && - (isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0) - ) { - if ( - (this._thisRowLastChar.getChars() || this._thisRowLastChar.getWidth() === 0) && - equalBg(this._thisRowLastChar, this._nextRowFirstChar) - ) { - isValid = true - } - - if ( - isNextRowFirstCharDoubleWidth && - (this._thisRowLastSecondChar.getChars() || this._thisRowLastSecondChar.getWidth() === 0) && - equalBg(this._thisRowLastChar, this._nextRowFirstChar) && - equalBg(this._thisRowLastSecondChar, this._nextRowFirstChar) - ) { - isValid = true - } - } - - if (!isValid) { - rowSeparator = "-".repeat(this._nullCellCount + 1) - rowSeparator += "\u001b[1D\u001b[1X" - - if (this._nullCellCount > 0) { - rowSeparator += "\u001b[A" - rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C` - rowSeparator += `\u001b[${this._nullCellCount}X` - rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D` - rowSeparator += "\u001b[B" - } - - this._lastContentCursorRow = row + 1 - this._lastContentCursorCol = 0 - this._lastCursorRow = row + 1 - this._lastCursorCol = 0 - } - } - } - - this._allRows[this._rowIndex] = this._currentRow - this._allRowSeparators[this._rowIndex++] = rowSeparator - this._currentRow = "" - this._nullCellCount = 0 - } - - private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { - const sgrSeq: number[] = [] - const fgChanged = !equalFg(cell, oldCell) - const bgChanged = !equalBg(cell, oldCell) - const flagsChanged = !equalFlags(cell, oldCell) - - if (fgChanged || bgChanged || flagsChanged) { - if (this._isAttributeDefault(cell)) { - if (!this._isAttributeDefault(oldCell)) { - sgrSeq.push(0) - } - } else { - if (fgChanged) { - const color = cell.getFgColor() - const mode = cell.getFgColorMode() - if (mode === 2) { - // RGB - sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff) - } else if (mode === 1) { - // Palette - if (color >= 16) { - sgrSeq.push(38, 5, color) - } else { - sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)) - } - } else { - sgrSeq.push(39) - } - } - if (bgChanged) { - const color = cell.getBgColor() - const mode = cell.getBgColorMode() - if (mode === 2) { - // RGB - sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff) - } else if (mode === 1) { - // Palette - if (color >= 16) { - sgrSeq.push(48, 5, color) - } else { - sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)) - } - } else { - sgrSeq.push(49) - } - } - if (flagsChanged) { - if (!!cell.isInverse() !== !!oldCell.isInverse()) { - sgrSeq.push(cell.isInverse() ? 7 : 27) - } - if (!!cell.isBold() !== !!oldCell.isBold()) { - sgrSeq.push(cell.isBold() ? 1 : 22) - } - if (!!cell.isUnderline() !== !!oldCell.isUnderline()) { - sgrSeq.push(cell.isUnderline() ? 4 : 24) - } - if (!!cell.isBlink() !== !!oldCell.isBlink()) { - sgrSeq.push(cell.isBlink() ? 5 : 25) - } - if (!!cell.isInvisible() !== !!oldCell.isInvisible()) { - sgrSeq.push(cell.isInvisible() ? 8 : 28) - } - if (!!cell.isItalic() !== !!oldCell.isItalic()) { - sgrSeq.push(cell.isItalic() ? 3 : 23) - } - if (!!cell.isDim() !== !!oldCell.isDim()) { - sgrSeq.push(cell.isDim() ? 2 : 22) - } - if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) { - sgrSeq.push(cell.isStrikethrough() ? 9 : 29) - } - } - } - } - - return sgrSeq - } - - private _isAttributeDefault(cell: IBufferCell): boolean { - return ( - cell.getFgColorMode() === 0 && - cell.getBgColorMode() === 0 && - !cell.isBold() && - !cell.isItalic() && - !cell.isUnderline() && - !cell.isBlink() && - !cell.isInverse() && - !cell.isInvisible() && - !cell.isDim() && - !cell.isStrikethrough() - ) - } - - protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void { - const isPlaceHolderCell = cell.getWidth() === 0 - - if (isPlaceHolderCell) { - return - } - - const isEmptyCell = cell.getChars() === "" - - const sgrSeq = this._diffStyle(cell, this._cursorStyle) - - const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0 - - if (styleChanged) { - if (this._nullCellCount > 0) { - if (!equalBg(this._cursorStyle, this._backgroundCell)) { - this._currentRow += `\u001b[${this._nullCellCount}X` - } - this._currentRow += `\u001b[${this._nullCellCount}C` - this._nullCellCount = 0 - } - - this._lastContentCursorRow = this._lastCursorRow = row - this._lastContentCursorCol = this._lastCursorCol = col - - this._currentRow += `\u001b[${sgrSeq.join(";")}m` - - const line = this._buffer.getLine(row) - const cellFromLine = line?.getCell(col) - if (cellFromLine) { - this._cursorStyle = cellFromLine - this._cursorStyleRow = row - this._cursorStyleCol = col - } - } - - if (isEmptyCell) { - this._nullCellCount += cell.getWidth() - } else { - if (this._nullCellCount > 0) { - if (equalBg(this._cursorStyle, this._backgroundCell)) { - this._currentRow += `\u001b[${this._nullCellCount}C` - } else { - this._currentRow += `\u001b[${this._nullCellCount}X` - this._currentRow += `\u001b[${this._nullCellCount}C` - } - this._nullCellCount = 0 - } - - this._currentRow += cell.getChars() - - this._lastContentCursorRow = this._lastCursorRow = row - this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth() - } - } - - protected _serializeString(excludeFinalCursorPosition?: boolean): string { - let rowEnd = this._allRows.length - - if (this._buffer.length - this._firstRow <= this._terminal.rows) { - rowEnd = this._lastContentCursorRow + 1 - this._firstRow - this._lastCursorCol = this._lastContentCursorCol - this._lastCursorRow = this._lastContentCursorRow - } - - let content = "" - - for (let i = 0; i < rowEnd; i++) { - content += this._allRows[i] - if (i + 1 < rowEnd) { - content += this._allRowSeparators[i] - } - } - - if (!excludeFinalCursorPosition) { - // Get cursor position relative to viewport (1-indexed for ANSI) - // cursorY is relative to the viewport, cursorX is column position - const cursorRow = this._buffer.cursorY + 1 // 1-indexed - const cursorCol = this._buffer.cursorX + 1 // 1-indexed - - // Use absolute cursor positioning (CUP - Cursor Position) - // This is more reliable than relative moves which depend on knowing - // exactly where the cursor ended up after all the content - content += `\u001b[${cursorRow};${cursorCol}H` - } - - return content - } -} - -// ============================================================================ -// SerializeAddon Class -// ============================================================================ - -export class SerializeAddon implements ITerminalAddon { - private _terminal?: ITerminalCore - - /** - * Activate the addon (called by Terminal.loadAddon) - */ - public activate(terminal: ITerminalCore): void { - this._terminal = terminal - } - - /** - * Dispose the addon and clean up resources - */ - public dispose(): void { - this._terminal = undefined - } - - /** - * Serializes terminal rows into a string that can be written back to the - * terminal to restore the state. The cursor will also be positioned to the - * correct cell. - * - * @param options Custom options to allow control over what gets serialized. - */ - public serialize(options?: ISerializeOptions): string { - if (!this._terminal) { - throw new Error("Cannot use addon until it has been loaded") - } - - const terminal = this._terminal as any - const buffer = terminal.buffer - - if (!buffer) { - return "" - } - - const activeBuffer = buffer.active || buffer.normal - if (!activeBuffer) { - return "" - } - - let content = options?.range - ? this._serializeBufferByRange(activeBuffer, options.range, true) - : this._serializeBufferByScrollback(activeBuffer, options?.scrollback) - - // Handle alternate buffer if active and not excluded - if (!options?.excludeAltBuffer) { - const altBuffer = buffer.alternate - if (altBuffer && buffer.active?.type === "alternate") { - const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined) - content += `\u001b[?1049h\u001b[H${alternateContent}` - } - } - - return content - } - - /** - * Serializes terminal content as plain text (no escape sequences) - * @param options Custom options to allow control over what gets serialized. - */ - public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string { - if (!this._terminal) { - throw new Error("Cannot use addon until it has been loaded") - } - - const terminal = this._terminal as any - const buffer = terminal.buffer - - if (!buffer) { - return "" - } - - const activeBuffer = buffer.active || buffer.normal - if (!activeBuffer) { - return "" - } - - const maxRows = activeBuffer.length - const scrollback = options?.scrollback - const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows) - - const startRow = maxRows - correctRows - const endRow = maxRows - 1 - const lines: string[] = [] - - for (let row = startRow; row <= endRow; row++) { - const line = activeBuffer.getLine(row) - if (line) { - const text = line.translateToString(options?.trimWhitespace ?? true) - lines.push(text) - } - } - - // Trim trailing empty lines if requested - if (options?.trimWhitespace) { - while (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop() - } - } - - return lines.join("\n") - } - - private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length - const rows = this._terminal?.rows ?? 24 - const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows) - return this._serializeBufferByRange( - buffer, - { - start: maxRows - correctRows, - end: maxRows - 1, - }, - false, - ) - } - - private _serializeBufferByRange( - buffer: IBuffer, - range: ISerializeRange, - excludeFinalCursorPosition: boolean, - ): string { - const handler = new StringSerializeHandler(buffer, this._terminal!) - const cols = this._terminal?.cols ?? 80 - return handler.serialize( - { - start: { x: 0, y: range.start }, - end: { x: cols, y: range.end }, - }, - excludeFinalCursorPosition, - ) - } -} diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx deleted file mode 100644 index 49a45a432b..0000000000 --- a/packages/desktop/src/components/terminal.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { init, Terminal as Term, FitAddon } from "ghostty-web" -import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js" -import { createReconnectingWS, ReconnectingWebSocket } from "@solid-primitives/websocket" -import { useSDK } from "@/context/sdk" -import { SerializeAddon } from "@/addons/serialize" -import { LocalPTY } from "@/context/session" - -await init() - -export interface TerminalProps extends ComponentProps<"div"> { - pty: LocalPTY - onSubmit?: () => void - onCleanup?: (pty: LocalPTY) => void -} - -export const Terminal = (props: TerminalProps) => { - const sdk = useSDK() - let container!: HTMLDivElement - const [local, others] = splitProps(props, ["pty", "class", "classList"]) - let ws: ReconnectingWebSocket - let term: Term - let serializeAddon: SerializeAddon - let fitAddon: FitAddon - - onMount(async () => { - ws = createReconnectingWS(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) - term = new Term({ - cursorBlink: true, - fontSize: 14, - fontFamily: "TX-02, monospace", - allowTransparency: true, - theme: { - background: "#191515", - foreground: "#d4d4d4", - }, - scrollback: 10_000, - }) - term.attachCustomKeyEventHandler((event) => { - // allow for ctrl-` to toggle terminal in parent - if (event.ctrlKey && event.key.toLowerCase() === "`") { - event.preventDefault() - return true - } - return false - }) - - fitAddon = new FitAddon() - serializeAddon = new SerializeAddon() - term.loadAddon(serializeAddon) - term.loadAddon(fitAddon) - - term.open(container) - - if (local.pty.buffer) { - const originalSize = { cols: term.cols, rows: term.rows } - let resized = false - if (local.pty.rows && local.pty.cols) { - term.resize(local.pty.cols, local.pty.rows) - resized = true - } - term.write(local.pty.buffer) - if (local.pty.scrollY) { - term.scrollToLine(local.pty.scrollY) - } - if (resized) { - term.resize(originalSize.cols, originalSize.rows) - } - } - - container.focus() - - fitAddon.fit() - fitAddon.observeResize() - window.addEventListener("resize", () => fitAddon.fit()) - term.onResize(async (size) => { - if (ws && ws.readyState === WebSocket.OPEN) { - await sdk.client.pty.update({ - path: { id: local.pty.id }, - body: { - size: { - cols: size.cols, - rows: size.rows, - }, - }, - }) - } - }) - term.onData((data) => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(data) - } - }) - term.onKey((key) => { - if (key.key == "Enter") { - props.onSubmit?.() - } - }) - // term.onScroll((ydisp) => { - // console.log("Scroll position:", ydisp) - // }) - ws.addEventListener("open", () => { - console.log("WebSocket connected") - sdk.client.pty.update({ - path: { id: local.pty.id }, - body: { - size: { - cols: term.cols, - rows: term.rows, - }, - }, - }) - }) - ws.addEventListener("message", (event) => { - term.write(event.data) - }) - ws.addEventListener("error", (error) => { - console.error("WebSocket error:", error) - }) - ws.addEventListener("close", () => { - console.log("WebSocket disconnected") - }) - }) - - onCleanup(() => { - if (serializeAddon && props.onCleanup) { - const buffer = serializeAddon.serialize() - props.onCleanup({ - ...local.pty, - buffer, - rows: term.rows, - cols: term.cols, - scrollY: term.getViewportY(), - }) - } - ws?.close() - term?.dispose() - }) - - return ( -
- ) -} diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index ca736e84e6..81e8b537ab 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -15,16 +15,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( opened: true, width: 280, }, - terminal: { - opened: false, - height: 280, - }, review: { state: "pane" as "pane" | "tab", }, }), { - name: "____default-layout", + name: "___default-layout", }, ) @@ -65,22 +61,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("sidebar", "width", width) }, }, - terminal: { - opened: createMemo(() => store.terminal.opened), - open() { - setStore("terminal", "opened", true) - }, - close() { - setStore("terminal", "opened", false) - }, - toggle() { - setStore("terminal", "opened", (x) => !x) - }, - height: createMemo(() => store.terminal.height), - resize(height: number) { - setStore("terminal", "height", height) - }, - }, review: { state: createMemo(() => store.review?.state ?? "closed"), pane() { diff --git a/packages/desktop/src/context/sdk.tsx b/packages/desktop/src/context/sdk.tsx index 144202ee20..81b32035a0 100644 --- a/packages/desktop/src/context/sdk.tsx +++ b/packages/desktop/src/context/sdk.tsx @@ -27,6 +27,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ abort.abort() }) - return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url } + return { directory: props.directory, client: sdk, event: emitter } }, }) diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 4e9fe71f8a..72098a9395 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -8,25 +8,14 @@ import { pipe, sumBy } from "remeda" import { AssistantMessage, UserMessage } from "@opencode-ai/sdk" import { useParams } from "@solidjs/router" import { base64Encode } from "@/utils" -import { useSDK } from "./sdk" - -export type LocalPTY = { - id: string - title: string - rows?: number - cols?: number - buffer?: string - scrollY?: number -} export const { use: useSession, provider: SessionProvider } = createSimpleContext({ name: "Session", init: () => { - const sdk = useSDK() const params = useParams() const sync = useSync() const name = createMemo( - () => `______${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`, + () => `___${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`, ) const [store, setStore] = makePersisted( @@ -34,21 +23,16 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex messageId?: string tabs: { active?: string - all: string[] + opened: string[] } prompt: Prompt cursor?: number - terminals: { - active?: string - all: LocalPTY[] - } }>({ tabs: { - all: [], + opened: [], }, prompt: clonePrompt(DEFAULT_PROMPT), cursor: undefined, - terminals: { all: [] }, }), { name: name(), @@ -154,7 +138,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex setStore("tabs", "active", tab) }, setOpenedTabs(tabs: string[]) { - setStore("tabs", "all", tabs) + setStore("tabs", "opened", tabs) }, async openTab(tab: string) { if (tab === "chat") { @@ -162,8 +146,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex return } if (tab !== "review") { - if (!store.tabs.all.includes(tab)) { - setStore("tabs", "all", [...store.tabs.all, tab]) + if (!store.tabs.opened.includes(tab)) { + setStore("tabs", "opened", [...store.tabs.opened, tab]) } } setStore("tabs", "active", tab) @@ -172,88 +156,28 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex batch(() => { setStore( "tabs", - "all", - store.tabs.all.filter((x) => x !== tab), + "opened", + store.tabs.opened.filter((x) => x !== tab), ) if (store.tabs.active === tab) { - const index = store.tabs.all.findIndex((f) => f === tab) - const previous = store.tabs.all[Math.max(0, index - 1)] + const index = store.tabs.opened.findIndex((f) => f === tab) + const previous = store.tabs.opened[Math.max(0, index - 1)] setStore("tabs", "active", previous) } }) }, moveTab(tab: string, to: number) { - const index = store.tabs.all.findIndex((f) => f === tab) + const index = store.tabs.opened.findIndex((f) => f === tab) if (index === -1) return setStore( "tabs", - "all", + "opened", produce((opened) => { opened.splice(to, 0, opened.splice(index, 1)[0]) }), ) }, }, - terminal: { - all: createMemo(() => Object.values(store.terminals.all)), - active: createMemo(() => store.terminals.active), - new() { - sdk.client.pty.create({ body: { title: `Terminal ${store.terminals.all.length + 1}` } }).then((pty) => { - const id = pty.data?.id - if (!id) return - batch(() => { - setStore("terminals", "all", [ - ...store.terminals.all, - { - id, - title: pty.data?.title ?? "Terminal", - // rows: pty.data?.rows ?? 24, - // cols: pty.data?.cols ?? 80, - // buffer: "", - // scrollY: 0, - }, - ]) - setStore("terminals", "active", id) - }) - }) - }, - update(pty: Partial & { id: string }) { - setStore("terminals", "all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x))) - sdk.client.pty.update({ - path: { id: pty.id }, - body: { title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined }, - }) - }, - open(id: string) { - setStore("terminals", "active", id) - }, - async close(id: string) { - batch(() => { - setStore( - "terminals", - "all", - store.terminals.all.filter((x) => x.id !== id), - ) - if (store.terminals.active === id) { - const index = store.terminals.all.findIndex((f) => f.id === id) - const previous = store.tabs.all[Math.max(0, index - 1)] - setStore("terminals", "active", previous) - } - }) - await sdk.client.pty.remove({ path: { id } }) - }, - move(id: string, to: number) { - const index = store.terminals.all.findIndex((f) => f.id === id) - if (index === -1) return - setStore( - "terminals", - "all", - produce((all) => { - all.splice(to, 0, all.splice(index, 1)[0]) - }), - ) - }, - }, } }, }) diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 106a2e733f..15180c8856 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,9 +1,9 @@ import { createMemo, For, ParentProps, Show } from "solid-js" import { DateTime } from "luxon" -import { A, useNavigate, useParams } from "@solidjs/router" +import { A, useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" -import { base64Decode, base64Encode } from "@/utils" +import { base64Encode } from "@/utils" import { Mark } from "@opencode-ai/ui/logo" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" @@ -12,21 +12,11 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { getFilename } from "@opencode-ai/util/path" -import { Select } from "@opencode-ai/ui/select" -import { Session } from "@opencode-ai/sdk/client" export default function Layout(props: ParentProps) { - const navigate = useNavigate() const params = useParams() const globalSync = useGlobalSync() const layout = useLayout() - const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) - const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? []) - const currentSession = createMemo(() => sessions().find((s) => s.id === params.id) ?? sessions().at(0)) - - function navigateToSession(session: Session | undefined) { - navigate(`/${params.dir}/session/${session?.id}`) - } const handleOpenProject = async () => { // layout.projects.open(dir.) @@ -34,7 +24,7 @@ export default function Layout(props: ParentProps) { return (
-
+
-
-
-
- x.title} - value={(x) => x.id} - onSelect={navigateToSession} - class="text-14-regular text-text-base max-w-3xs" - variant="ghost" - /> -
- -
-
- - Toggle terminal - Ctrl ` -
- } - > - - -
-
- -
{ - e.preventDefault() - const startX = e.clientX - const startWidth = layout.sidebar.width() - const maxWidth = window.innerWidth * 0.3 - const minWidth = 150 - const collapseThreshold = 80 - let currentWidth = startWidth - - document.body.style.userSelect = "none" - document.body.style.overflow = "hidden" - - const onMouseMove = (moveEvent: MouseEvent) => { - const deltaX = moveEvent.clientX - startX - currentWidth = startWidth + deltaX - const clampedWidth = Math.min(maxWidth, Math.max(minWidth, currentWidth)) - layout.sidebar.resize(clampedWidth) - } - - const onMouseUp = () => { - document.body.style.userSelect = "" - document.body.style.overflow = "" - document.removeEventListener("mousemove", onMouseMove) - document.removeEventListener("mouseup", onMouseUp) - - if (currentWidth < collapseThreshold) { - layout.sidebar.close() - } - } - - document.addEventListener("mousemove", onMouseMove) - document.addEventListener("mouseup", onMouseUp) - }} - /> -
-
{props.children}
+
{props.children}
) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 7736253340..d6ce62b703 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js" import { useLocal, type LocalFile } from "@/context/local" import { createStore } from "solid-js/store" import { PromptInput } from "@/components/prompt-input" @@ -31,7 +31,6 @@ import { useSession } from "@/context/session" import { useLayout } from "@/context/layout" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Diff } from "@opencode-ai/ui/diff" -import { Terminal } from "@/components/terminal" export default function Page() { const layout = useLayout() @@ -55,14 +54,6 @@ export default function Page() { document.removeEventListener("keydown", handleKeyDown) }) - createEffect(() => { - if (layout.terminal.opened()) { - if (session.terminal.all().length === 0) { - session.terminal.new() - } - } - }) - const handleKeyDown = (event: KeyboardEvent) => { if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") { event.preventDefault() @@ -82,16 +73,6 @@ export default function Page() { document.documentElement.setAttribute("data-theme", nextTheme) return } - if (event.ctrlKey && event.key.toLowerCase() === "`") { - event.preventDefault() - layout.terminal.toggle() - return - } - - // @ts-expect-error - if (document.activeElement?.dataset?.component === "terminal") { - return - } const focused = document.activeElement === inputRef if (focused) { @@ -160,7 +141,7 @@ export default function Page() { const handleDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (draggable && droppable) { - const currentTabs = session.layout.tabs.all + const currentTabs = session.layout.tabs.opened const fromIndex = currentTabs?.indexOf(draggable.id.toString()) const toIndex = currentTabs?.indexOf(droppable.id.toString()) if (fromIndex !== toIndex && toIndex !== undefined) { @@ -278,397 +259,317 @@ export default function Page() { const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length) return ( -
-
- - - - -
- - -
-
Session
- - -
{session.usage.context() ?? 0}%
-
-
-
- - - } +
+ + + + +
+ + +
+
Session
+ -
- - - -
-
Review
- -
- {session.info()?.summary?.files ?? 0} -
-
-
-
- - - - - {(tab) => ( - - )} - - -
- - setStore("fileSelectOpen", true)} - /> + +
{session.usage.context() ?? 0}%
- -
- -
-
+ + + } > - - -
- - 1 - ? "pr-6 pl-18" - : "px-6"), - }} - diffComponent={Diff} - /> -
-
- -
-
New session
-
- -
- {getDirectory(sync.data.path.directory)} - {getFilename(sync.data.path.directory)} -
+
+ + + +
+
Review
+ +
+ {session.info()?.summary?.files ?? 0}
-
- -
- Last modified  - - {DateTime.fromMillis(sync.data.project.time.created).toRelative()} - -
-
-
- - -
-
- { - inputRef = el - }} - /> +
-
- -
- + + + + {(tab) => } + + +
+ + setStore("fileSelectOpen", true)} + /> + +
+ +
+ +
+
+ + +
+ + 1 + ? "pr-6 pl-18" + : "px-6"), + }} + diffComponent={Diff} + /> +
+
+ +
+
New session
+
+ +
+ {getDirectory(sync.data.path.directory)} + {getFilename(sync.data.path.directory)} +
+
+
+ +
+ Last modified  + + {DateTime.fromMillis(sync.data.project.time.created).toRelative()} + +
+
+
+
+
+
+
+ { + inputRef = el }} - diffs={session.diffs()} - diffComponent={Diff} - actions={ - - { - layout.review.tab() - session.layout.setActiveTab("review") - }} - /> - - } />
- +
- - - +
+ { + layout.review.tab() + session.layout.setActiveTab("review") + }} + /> + + } />
-
-
- - {(tab) => { - const [file] = createResource( - () => tab, - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, - ) - return ( - - - - {(f) => ( - - )} - - - - ) - }} - - - - - {(draggedFile) => { - const [file] = createResource( - () => draggedFile(), - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, - ) - return ( -
- {(f) => } -
- ) - }} -
-
- - -
- { - inputRef = el - }} - /> -
-
- - } - > -
    - - {(path) => ( -
  • - -
  • - )} -
    -
- -
- - x} - onOpenChange={(open) => setStore("fileSelectOpen", open)} - onSelect={(x) => { - if (x) { - local.file.open(x) - return session.layout.openTab("file://" + x) - } - return undefined - }} - > - {(i) => ( + +
+ + +
-
- -
- - {getDirectory(i)} - - {getFilename(i)} -
-
-
+
- )} - -
-
- -
-
{ - e.preventDefault() - const startY = e.clientY - const startHeight = layout.terminal.height() - const maxHeight = window.innerHeight * 0.6 - const minHeight = 100 - const collapseThreshold = 50 - let currentHeight = startHeight - - document.body.style.userSelect = "none" - document.body.style.overflow = "hidden" - - const onMouseMove = (moveEvent: MouseEvent) => { - const deltaY = startY - moveEvent.clientY - currentHeight = startHeight + deltaY - const clampedHeight = Math.min(maxHeight, Math.max(minHeight, currentHeight)) - layout.terminal.resize(clampedHeight) - } - - const onMouseUp = () => { - document.body.style.userSelect = "" - document.body.style.overflow = "" - document.removeEventListener("mousemove", onMouseMove) - document.removeEventListener("mouseup", onMouseUp) - - if (currentHeight < collapseThreshold) { - layout.terminal.close() - } - } - - document.addEventListener("mousemove", onMouseMove) - document.addEventListener("mouseup", onMouseUp) + + + + {(tab) => { + const [file] = createResource( + () => tab, + async (tab) => { + if (tab.startsWith("file://")) { + return local.file.node(tab.replace("file://", "")) + } + return undefined + }, + ) + return ( + + + + {(f) => ( + + )} + + + + ) + }} + + + + + {(draggedFile) => { + const [file] = createResource( + () => draggedFile(), + async (tab) => { + if (tab.startsWith("file://")) { + return local.file.node(tab.replace("file://", "")) + } + return undefined + }, + ) + return ( +
+ {(f) => } +
+ ) + }} +
+
+ + +
+ { + inputRef = el }} /> - - - - {(terminal) => ( - 1 && ( - session.terminal.close(terminal.id)} /> - ) - } +
+
+ + }> +
    + + {(path) => ( +
  • + +
  • )}
    - -
+ + +
+ + x} + onOpenChange={(open) => setStore("fileSelectOpen", open)} + onSelect={(x) => { + if (x) { + local.file.open(x) + return session.layout.openTab("file://" + x) + } + return undefined + }} + > + {(i) => ( +
+
+ +
+ + {getDirectory(i)} + + {getFilename(i)} +
+
+
+
+ )} +
) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 2c5441998b..46c8c3200a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -72,7 +72,6 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", "decimal.js": "10.5.0", diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 76f78f3faa..7754b4a395 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -5,7 +5,6 @@ import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" -import type { BunWebSocketData } from "hono/bun" await Log.init({ print: process.argv.includes("--print-logs"), @@ -28,7 +27,7 @@ process.on("uncaughtException", (e) => { }) }) -let server: Bun.Server +let server: Bun.Server export const rpc = { async server(input: { port: number; hostname: string }) { if (server) await server.stop(true) @@ -54,9 +53,7 @@ export const rpc = { async shutdown() { Log.Default.info("worker shutting down") await Instance.disposeAll() - // TODO: this should be awaited, but ws connections are - // causing this to hang, need to revisit this - server.stop(true) + await server.stop(true) }, } diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index ad6e22e1be..99eb6c9ff0 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -8,7 +8,6 @@ export namespace Identifier { permission: "per", user: "usr", part: "prt", - pty: "pty", } as const export function schema(prefix: keyof typeof prefixes) { diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts deleted file mode 100644 index efb519ff2a..0000000000 --- a/packages/opencode/src/pty/index.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { spawn, type IPty } from "bun-pty" -import z from "zod" -import { Identifier } from "../id/id" -import { Log } from "../util/log" -import { Bus } from "../bus" -import type { WSContext } from "hono/ws" -import { Instance } from "../project/instance" -import { shell } from "@opencode-ai/util/shell" - -export namespace Pty { - const log = Log.create({ service: "pty" }) - - export const Info = z - .object({ - id: Identifier.schema("pty"), - title: z.string(), - command: z.string(), - args: z.array(z.string()), - cwd: z.string(), - status: z.enum(["running", "exited"]), - pid: z.number(), - }) - .meta({ ref: "Pty" }) - - export type Info = z.infer - - export const CreateInput = z.object({ - command: z.string().optional(), - args: z.array(z.string()).optional(), - cwd: z.string().optional(), - title: z.string().optional(), - env: z.record(z.string(), z.string()).optional(), - }) - - export type CreateInput = z.infer - - export const UpdateInput = z.object({ - title: z.string().optional(), - size: z - .object({ - rows: z.number(), - cols: z.number(), - }) - .optional(), - }) - - export type UpdateInput = z.infer - - export const Event = { - Created: Bus.event("pty.created", z.object({ info: Info })), - Updated: Bus.event("pty.updated", z.object({ info: Info })), - Exited: Bus.event("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })), - Deleted: Bus.event("pty.deleted", z.object({ id: Identifier.schema("pty") })), - } - - interface ActiveSession { - info: Info - process: IPty - buffer: string - subscribers: Set - } - - const state = Instance.state( - () => new Map(), - async (sessions) => { - for (const session of sessions.values()) { - try { - session.process.kill() - } catch {} - for (const ws of session.subscribers) { - ws.close() - } - } - sessions.clear() - }, - ) - - export function list() { - return Array.from(state().values()).map((s) => s.info) - } - - export function get(id: string) { - return state().get(id)?.info - } - - export async function create(input: CreateInput) { - const id = Identifier.create("pty", false) - const command = input.command || shell() - const args = input.args || [] - const cwd = input.cwd || Instance.directory - const env = { ...process.env, ...input.env } as Record - log.info("creating session", { id, cmd: command, args, cwd }) - - const ptyProcess = spawn(command, args, { - name: "xterm-256color", - cwd, - env, - }) - const info = { - id, - title: input.title || `Terminal ${id.slice(-4)}`, - command, - args, - cwd, - status: "running", - pid: ptyProcess.pid, - } as const - const session: ActiveSession = { - info, - process: ptyProcess, - buffer: "", - subscribers: new Set(), - } - state().set(id, session) - ptyProcess.onData((data) => { - if (session.subscribers.size === 0) { - session.buffer += data - return - } - for (const ws of session.subscribers) { - if (ws.readyState === 1) { - ws.send(data) - } - } - }) - ptyProcess.onExit(({ exitCode }) => { - log.info("session exited", { id, exitCode }) - session.info.status = "exited" - Bus.publish(Event.Exited, { id, exitCode }) - state().delete(id) - }) - Bus.publish(Event.Created, { info }) - return info - } - - export async function update(id: string, input: UpdateInput) { - const session = state().get(id) - if (!session) return - if (input.title) { - session.info.title = input.title - } - if (input.size) { - session.process.resize(input.size.cols, input.size.rows) - } - Bus.publish(Event.Updated, { info: session.info }) - return session.info - } - - export async function remove(id: string) { - const session = state().get(id) - if (!session) return - log.info("removing session", { id }) - try { - session.process.kill() - } catch {} - for (const ws of session.subscribers) { - ws.close() - } - state().delete(id) - Bus.publish(Event.Deleted, { id }) - } - - export function resize(id: string, cols: number, rows: number) { - const session = state().get(id) - if (session && session.info.status === "running") { - session.process.resize(cols, rows) - } - } - - export function write(id: string, data: string) { - const session = state().get(id) - if (session && session.info.status === "running") { - session.process.write(data) - } - } - - export function connect(id: string, ws: WSContext) { - const session = state().get(id) - if (!session) { - ws.close() - return - } - log.info("client connected to session", { id }) - session.subscribers.add(ws) - if (session.buffer) { - ws.send(session.buffer) - session.buffer = "" - } - return { - onMessage: (message: string | ArrayBuffer) => { - session.process.write(String(message)) - }, - onClose: () => { - log.info("client disconnected from session", { id }) - session.subscribers.delete(ws) - }, - } - } -} diff --git a/packages/opencode/src/server/error.ts b/packages/opencode/src/server/error.ts deleted file mode 100644 index 26e2dfcb12..0000000000 --- a/packages/opencode/src/server/error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { resolver } from "hono-openapi" -import z from "zod" -import { Storage } from "../storage/storage" - -export const ERRORS = { - 400: { - description: "Bad request", - content: { - "application/json": { - schema: resolver( - z - .object({ - data: z.any(), - errors: z.array(z.record(z.string(), z.any())), - success: z.literal(false), - }) - .meta({ - ref: "BadRequestError", - }), - ), - }, - }, - }, - 404: { - description: "Not found", - content: { - "application/json": { - schema: resolver(Storage.NotFoundError.Schema), - }, - }, - }, -} as const - -export function errors(...codes: number[]) { - return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]])) -} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index a74b7876f1..31d0822762 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -43,13 +43,43 @@ import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" import { GlobalBus } from "@/bus/global" import { SessionStatus } from "@/session/status" -import { upgradeWebSocket, websocket } from "hono/bun" -import { errors } from "./error" -import { Pty } from "@/pty" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false +const ERRORS = { + 400: { + description: "Bad request", + content: { + "application/json": { + schema: resolver( + z + .object({ + data: z.any(), + errors: z.array(z.record(z.string(), z.any())), + success: z.literal(false), + }) + .meta({ + ref: "BadRequestError", + }), + ), + }, + }, + }, + 404: { + description: "Not found", + content: { + "application/json": { + schema: resolver(Storage.NotFoundError.Schema), + }, + }, + }, +} as const + +function errors(...codes: number[]) { + return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]])) +} + export namespace Server { const log = Log.create({ service: "server" }) @@ -162,167 +192,7 @@ export namespace Server { }), ) .use(validator("query", z.object({ directory: z.string().optional() }))) - .route("/project", ProjectRoute) - - .get( - "/pty", - describeRoute({ - description: "List all PTY sessions", - operationId: "pty.list", - responses: { - 200: { - description: "List of sessions", - content: { - "application/json": { - schema: resolver(Pty.Info.array()), - }, - }, - }, - }, - }), - async (c) => { - return c.json(Pty.list()) - }, - ) - .post( - "/pty", - describeRoute({ - description: "Create a new PTY session", - operationId: "pty.create", - responses: { - 200: { - description: "Created session", - content: { - "application/json": { - schema: resolver(Pty.Info), - }, - }, - }, - ...errors(400), - }, - }), - validator("json", Pty.CreateInput), - async (c) => { - const info = await Pty.create(c.req.valid("json")) - return c.json(info) - }, - ) - .put( - "/pty/:id", - describeRoute({ - description: "Update PTY session", - operationId: "pty.update", - responses: { - 200: { - description: "Updated session", - content: { - "application/json": { - schema: resolver(Pty.Info), - }, - }, - }, - ...errors(400), - }, - }), - validator("param", z.object({ id: z.string() })), - validator("json", Pty.UpdateInput), - async (c) => { - const info = await Pty.update(c.req.valid("param").id, c.req.valid("json")) - return c.json(info) - }, - ) - .get( - "/pty/:id", - describeRoute({ - description: "Get PTY session info", - operationId: "pty.get", - responses: { - 200: { - description: "Session info", - content: { - "application/json": { - schema: resolver(Pty.Info), - }, - }, - }, - ...errors(404), - }, - }), - validator("param", z.object({ id: z.string() })), - async (c) => { - const info = Pty.get(c.req.valid("param").id) - if (!info) { - throw new Storage.NotFoundError({ message: "Session not found" }) - } - return c.json(info) - }, - ) - .delete( - "/pty/:id", - describeRoute({ - description: "Remove a PTY session", - operationId: "pty.remove", - responses: { - 200: { - description: "Session removed", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - ...errors(404), - }, - }), - validator("param", z.object({ id: z.string() })), - async (c) => { - await Pty.remove(c.req.valid("param").id) - return c.json(true) - }, - ) - .get( - "/pty/:id/connect", - describeRoute({ - description: "Connect to a PTY session", - operationId: "pty.connect", - responses: { - 200: { - description: "Connected session", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - 404: { - description: "Session not found", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - validator("param", z.object({ id: z.string() })), - upgradeWebSocket((c) => { - const id = c.req.param("id") - let handler: ReturnType - return { - onOpen(_event, ws) { - handler = Pty.connect(id, ws) - }, - onMessage(event) { - handler?.onMessage(String(event.data)) - }, - onClose() { - handler?.onClose() - }, - } - }), - ) - .get( "/config", describeRoute({ @@ -2213,7 +2083,6 @@ export namespace Server { hostname: opts.hostname, idleTimeout: 0, fetch: App().fetch, - websocket: websocket, }) return server } diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index d04277cbc8..0dc470566e 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -8,23 +8,6 @@ import type { ProjectListResponses, ProjectCurrentData, ProjectCurrentResponses, - PtyListData, - PtyListResponses, - PtyCreateData, - PtyCreateResponses, - PtyCreateErrors, - PtyRemoveData, - PtyRemoveResponses, - PtyRemoveErrors, - PtyGetData, - PtyGetResponses, - PtyGetErrors, - PtyUpdateData, - PtyUpdateResponses, - PtyUpdateErrors, - PtyConnectData, - PtyConnectResponses, - PtyConnectErrors, ConfigGetData, ConfigGetResponses, ConfigUpdateData, @@ -248,76 +231,6 @@ class Project extends _HeyApiClient { } } -class Pty extends _HeyApiClient { - /** - * List all PTY sessions - */ - public list(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/pty", - ...options, - }) - } - - /** - * Create a new PTY session - */ - public create(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/pty", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * Remove a PTY session - */ - public remove(options: Options) { - return (options.client ?? this._client).delete({ - url: "/pty/{id}", - ...options, - }) - } - - /** - * Get PTY session info - */ - public get(options: Options) { - return (options.client ?? this._client).get({ - url: "/pty/{id}", - ...options, - }) - } - - /** - * Update PTY session - */ - public update(options: Options) { - return (options.client ?? this._client).put({ - url: "/pty/{id}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }) - } - - /** - * Connect to a PTY session - */ - public connect(options: Options) { - return (options.client ?? this._client).get({ - url: "/pty/{id}/connect", - ...options, - }) - } -} - class Config extends _HeyApiClient { /** * Get config info @@ -1092,7 +1005,6 @@ export class OpencodeClient extends _HeyApiClient { } global = new Global({ client: this._client }) project = new Project({ client: this._client }) - pty = new Pty({ client: this._client }) config = new Config({ client: this._client }) tool = new Tool({ client: this._client }) instance = new Instance({ client: this._client }) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 58ba58d359..6c80f0b7c5 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -655,45 +655,6 @@ export type EventTuiToastShow = { } } -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string - } -} - export type EventServerConnected = { type: "server.connected" properties: { @@ -729,10 +690,6 @@ export type Event = | EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted | EventServerConnected export type GlobalEvent = { @@ -751,21 +708,6 @@ export type Project = { } } -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - /** * Custom keybind configurations */ @@ -1324,6 +1266,14 @@ export type Config = { } } +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + export type ToolIds = Array export type ToolListItem = { @@ -1345,6 +1295,13 @@ export type VcsInfo = { branch: string } +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string + } +} + export type TextPartInput = { id?: string type: "text" @@ -1657,181 +1614,6 @@ export type ProjectCurrentResponses = { export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] -export type PtyListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/pty" -} - -export type PtyListResponses = { - /** - * List of sessions - */ - 200: Array -} - -export type PtyListResponse = PtyListResponses[keyof PtyListResponses] - -export type PtyCreateData = { - body?: { - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - } - path?: never - query?: { - directory?: string - } - url: "/pty" -} - -export type PtyCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] - -export type PtyCreateResponses = { - /** - * Created session - */ - 200: Pty -} - -export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] - -export type PtyRemoveData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}" -} - -export type PtyRemoveErrors = { - /** - * Not found - */ - 404: NotFoundError -} - -export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] - -export type PtyRemoveResponses = { - /** - * Session removed - */ - 200: boolean -} - -export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] - -export type PtyGetData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}" -} - -export type PtyGetErrors = { - /** - * Not found - */ - 404: NotFoundError -} - -export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] - -export type PtyGetResponses = { - /** - * Session info - */ - 200: Pty -} - -export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] - -export type PtyUpdateData = { - body?: { - title?: string - size?: { - rows: number - cols: number - } - } - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}" -} - -export type PtyUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] - -export type PtyUpdateResponses = { - /** - * Updated session - */ - 200: Pty -} - -export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] - -export type PtyConnectData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - } - url: "/pty/{id}/connect" -} - -export type PtyConnectErrors = { - /** - * Session not found - */ - 404: boolean -} - -export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] - -export type PtyConnectResponses = { - /** - * Connected session - */ - 200: boolean -} - -export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] - export type ConfigGetData = { body?: never path?: never diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 9e4f00a0de..306d796498 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -153,10 +153,10 @@ const newIcons = { stop: ``, enter: ``, "layout-left": ``, - "layout-left-partial": ``, + "layout-left-partial": ``, "layout-left-full": ``, "layout-right": ``, - "layout-right-partial": ``, + "layout-right-partial": ``, "layout-right-full": ``, "speech-bubble": ``, "align-right": ``, @@ -167,9 +167,6 @@ const newIcons = { "bubble-5": ``, github: ``, discord: ``, - "layout-bottom": ``, - "layout-bottom-partial": ``, - "layout-bottom-full": ``, } export interface IconProps extends ComponentProps<"svg"> { diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 96ddf174cd..421215a78c 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -20,7 +20,6 @@ [data-component="select-content"] { min-width: 4rem; - max-width: 23rem; overflow: hidden; border-radius: var(--radius-md); border-width: 1px; @@ -40,7 +39,6 @@ } [data-slot="select-select-content-list"] { - min-height: 2rem; overflow-y: auto; max-height: 12rem; white-space: nowrap; diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 9ba1f177b5..464900ef97 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -1,10 +1,10 @@ import { Select as Kobalte } from "@kobalte/core/select" -import { createMemo, splitProps, type ComponentProps } from "solid-js" +import { createMemo, type ComponentProps } from "solid-js" import { pipe, groupBy, entries, map } from "remeda" import { Button, ButtonProps } from "./button" import { Icon } from "./icon" -export type SelectProps = Omit>, "value" | "onSelect"> & { +export interface SelectProps { placeholder?: string options: T[] current?: T @@ -17,21 +17,10 @@ export type SelectProps = Omit>, "value" | " } export function Select(props: SelectProps & ButtonProps) { - const [local, others] = splitProps(props, [ - "class", - "classList", - "placeholder", - "options", - "current", - "value", - "label", - "groupBy", - "onSelect", - ]) const grouped = createMemo(() => { const result = pipe( - local.options, - groupBy((x) => (local.groupBy ? local.groupBy(x) : "")), + props.options, + groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))), entries(), map(([k, v]) => ({ category: k, options: v })), @@ -40,30 +29,28 @@ export function Select(props: SelectProps & ButtonProps) { }) return ( - // @ts-ignore - {...others} data-component="select" - value={local.current} + value={props.current} options={grouped()} - optionValue={(x) => (local.value ? local.value(x) : (x as string))} - optionTextValue={(x) => (local.label ? local.label(x) : (x as string))} + optionValue={(x) => (props.value ? props.value(x) : (x as string))} + optionTextValue={(x) => (props.label ? props.label(x) : (x as string))} optionGroupChildren="options" - placeholder={local.placeholder} - sectionComponent={(local) => ( - {local.section.rawValue.category} + placeholder={props.placeholder} + sectionComponent={(props) => ( + {props.section.rawValue.category} )} itemComponent={(itemProps) => ( - {local.label ? local.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)} + {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)} @@ -71,25 +58,24 @@ export function Select(props: SelectProps & ButtonProps) { )} onChange={(v) => { - local.onSelect?.(v ?? undefined) + props.onSelect?.(v ?? undefined) }} > data-slot="select-select-trigger-value"> {(state) => { - const selected = state.selectedOption() ?? local.current - if (!selected) return local.placeholder || "" - if (local.label) return local.label(selected) + const selected = state.selectedOption() ?? props.current + if (!selected) return props.placeholder || "" + if (props.label) return props.label(selected) return selected as string }} @@ -100,8 +86,8 @@ export function Select(props: SelectProps & ButtonProps) { diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index d60edc5c50..d03e57320f 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -6,7 +6,7 @@ background-color: var(--background-stronger); overflow: clip; - [data-slot="tabs-list"] { + [data-slot="tabs-tabs-list"] { height: 48px; width: 100%; position: relative; @@ -36,7 +36,7 @@ } } - [data-slot="tabs-trigger-wrapper"] { + [data-slot="tabs-tabs-trigger-wrapper"] { position: relative; height: 100%; display: flex; @@ -58,14 +58,14 @@ border-right: 1px solid var(--border-weak-base); background-color: var(--background-base); - [data-slot="tabs-trigger"] { + [data-slot="tabs-tabs-trigger"] { display: flex; align-items: center; justify-content: center; padding: 14px 24px; } - [data-slot="tabs-trigger-close-button"] { + [data-slot="tabs-tabs-trigger-close-button"] { display: flex; align-items: center; justify-content: center; @@ -84,12 +84,12 @@ box-shadow: 0 0 0 2px var(--border-focus); } &:has([data-hidden]) { - [data-slot="tabs-trigger-close-button"] { + [data-slot="tabs-tabs-trigger-close-button"] { opacity: 0; } &:hover { - [data-slot="tabs-trigger-close-button"] { + [data-slot="tabs-tabs-trigger-close-button"] { opacity: 1; } } @@ -98,23 +98,23 @@ color: var(--text-strong); background-color: transparent; border-bottom-color: transparent; - [data-slot="tabs-trigger-close-button"] { + [data-slot="tabs-tabs-trigger-close-button"] { opacity: 1; } } &:hover:not(:disabled):not([data-selected]) { color: var(--text-strong); } - &:has([data-slot="tabs-trigger-close-button"]) { + &:has([data-slot="tabs-tabs-trigger-close-button"]) { padding-right: 12px; - [data-slot="tabs-trigger"] { + [data-slot="tabs-tabs-trigger"] { padding-right: 0; } } } - [data-slot="tabs-content"] { + [data-slot="tabs-tabs-content"] { overflow-y: auto; flex: 1; @@ -129,80 +129,4 @@ outline: none; } } - - &[data-variant="alt"] { - [data-slot="tabs-list"] { - padding-left: 24px; - padding-right: 24px; - gap: 12px; - border-bottom: 1px solid var(--border-weak-base); - background-color: transparent; - - &::after { - border: none; - background-color: transparent; - } - &:empty::after { - display: none; - } - } - - [data-slot="tabs-trigger-wrapper"] { - border: none; - color: var(--text-base); - background-color: transparent; - border-bottom-width: 2px; - border-bottom-style: solid; - border-bottom-color: transparent; - gap: 4px; - - /* text-14-regular */ - font-family: var(--font-family-sans); - font-size: var(--font-size-base); - font-style: normal; - font-weight: var(--font-weight-regular); - line-height: var(--line-height-x-large); /* 171.429% */ - letter-spacing: var(--letter-spacing-normal); - - [data-slot="tabs-trigger"] { - height: 100%; - padding: 4px; - background-color: transparent; - border-bottom-width: 2px; - border-bottom-color: transparent; - } - - [data-slot="tabs-trigger-close-button"] { - display: flex; - align-items: center; - justify-content: center; - } - - [data-component="icon-button"] { - width: 16px; - height: 16px; - margin: 0; - } - - &:has([data-selected]) { - color: var(--text-strong); - background-color: transparent; - border-bottom-color: var(--icon-strong-base); - } - - &:hover:not(:disabled):not([data-selected]) { - color: var(--text-strong); - } - - &:has([data-slot="tabs-trigger-close-button"]) { - padding-right: 0; - [data-slot="tabs-trigger"] { - padding-right: 0; - } - } - } - - /* [data-slot="tabs-content"] { */ - /* } */ - } } diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index d91ad3c415..68acd88d4e 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -2,9 +2,7 @@ import { Tabs as Kobalte } from "@kobalte/core/tabs" import { Show, splitProps, type JSX } from "solid-js" import type { ComponentProps, ParentProps } from "solid-js" -export interface TabsProps extends ComponentProps { - variant?: "normal" | "alt" -} +export interface TabsProps extends ComponentProps {} export interface TabsListProps extends ComponentProps {} export interface TabsTriggerProps extends ComponentProps { classes?: { @@ -16,12 +14,11 @@ export interface TabsTriggerProps extends ComponentProps export interface TabsContentProps extends ComponentProps {} function TabsRoot(props: TabsProps) { - const [split, rest] = splitProps(props, ["class", "classList", "variant"]) + const [split, rest] = splitProps(props, ["class", "classList"]) return ( ) { ]) return (
) { > {split.children} {(closeButton) => ( -
+
{closeButton()}
)} @@ -84,7 +81,7 @@ function TabsContent(props: ParentProps) { return (