chore: merge dev into v2 (#37370)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com> Co-authored-by: Jay <air@live.ca> Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com>
This commit is contained in:
parent
88b8883122
commit
e916b99742
79 changed files with 871 additions and 432 deletions
54
bun.lock
54
bun.lock
|
|
@ -53,7 +53,7 @@
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode2": "./bin/opencode2.cjs",
|
"opencode2": "./bin/opencode2.cjs",
|
||||||
},
|
},
|
||||||
|
|
@ -179,7 +179,7 @@
|
||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -193,7 +193,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
|
@ -229,7 +229,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
|
|
@ -256,7 +256,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
|
|
@ -278,7 +278,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
@ -302,7 +302,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
|
|
@ -322,7 +322,7 @@
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
|
|
@ -418,7 +418,7 @@
|
||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -478,7 +478,7 @@
|
||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -492,7 +492,7 @@
|
||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -504,7 +504,7 @@
|
||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -536,7 +536,7 @@
|
||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
|
|
@ -552,7 +552,7 @@
|
||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
},
|
},
|
||||||
|
|
@ -584,7 +584,7 @@
|
||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
|
|
@ -717,7 +717,7 @@
|
||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/ai": "workspace:*",
|
"@opencode-ai/ai": "workspace:*",
|
||||||
|
|
@ -806,7 +806,7 @@
|
||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -821,7 +821,7 @@
|
||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -838,7 +838,7 @@
|
||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -899,7 +899,7 @@
|
||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
|
|
@ -912,7 +912,7 @@
|
||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
|
|
@ -946,7 +946,7 @@
|
||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
|
|
@ -965,7 +965,7 @@
|
||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
|
@ -1006,7 +1006,7 @@
|
||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -1036,7 +1036,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
|
|
@ -1087,7 +1087,7 @@
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { createMemo, onCleanup } from "solid-js"
|
import { createMemo, onCleanup } from "solid-js"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { commandPaletteOptions, useCommand, type CommandOption } from "@/context/command"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout, type LocalProject } from "@/context/layout"
|
||||||
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useTabs } from "@/context/tabs"
|
||||||
|
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
|
|
||||||
export type CommandPaletteEntry = {
|
export type CommandPaletteEntry = {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -24,6 +25,8 @@ export type CommandPaletteEntry = {
|
||||||
path?: string
|
path?: string
|
||||||
directory?: string
|
directory?: string
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
|
server?: ServerConnection.Key
|
||||||
|
project?: LocalProject
|
||||||
archived?: number
|
archived?: number
|
||||||
updated?: number
|
updated?: number
|
||||||
}
|
}
|
||||||
|
|
@ -75,28 +78,25 @@ export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => vo
|
||||||
|
|
||||||
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
const global = useGlobal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const layout = useLayout()
|
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const navigate = useNavigate()
|
|
||||||
const serverSDK = useServerSDK()()
|
const serverSDK = useServerSDK()()
|
||||||
const serverSync = useServerSync()
|
const serverCtx = global.ensureServerCtx(serverSDK.server)
|
||||||
const { params, tabs } = useSessionLayout()
|
const appTabs = useTabs()
|
||||||
|
const { tabs: sessionTabs } = useSessionLayout()
|
||||||
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
const filesOnly = () => props.filesOnly?.() ?? false
|
const filesOnly = () => props.filesOnly?.() ?? false
|
||||||
|
|
||||||
const allowedCommands = createMemo(() => {
|
const allowedCommands = createMemo(() => {
|
||||||
if (filesOnly()) return []
|
if (filesOnly()) return []
|
||||||
return command.options.filter(
|
return commandPaletteOptions(command.options)
|
||||||
(option) =>
|
|
||||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
const commandEntries = createMemo(() => {
|
const commandEntries = createMemo(() => {
|
||||||
const category = language.t("palette.group.commands")
|
const category = language.t("palette.group.commands")
|
||||||
return allowedCommands().map((option) => createCommandEntry(option, category))
|
return allowedCommands().map((option) => createCommandPaletteCommandEntry(option, category))
|
||||||
})
|
})
|
||||||
const preferredCommandEntries = createMemo(() => {
|
const preferredCommandEntries = createMemo(() => {
|
||||||
const all = allowedCommands()
|
const all = allowedCommands()
|
||||||
|
|
@ -105,11 +105,11 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||||
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
||||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||||
const category = language.t("palette.group.commands")
|
const category = language.t("palette.group.commands")
|
||||||
return sorted.map((option) => createCommandEntry(option, category))
|
return sorted.map((option) => createCommandPaletteCommandEntry(option, category))
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabState = createSessionTabs({
|
const tabState = createSessionTabs({
|
||||||
tabs,
|
tabs: sessionTabs,
|
||||||
pathFromTab: file.pathFromTab,
|
pathFromTab: file.pathFromTab,
|
||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
})
|
})
|
||||||
|
|
@ -140,36 +140,12 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||||
.map((path) => createCommandPaletteFileEntry(path, category))
|
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
const sessions = createServerSessionEntries({
|
||||||
const project = createMemo(() => {
|
server: ServerConnection.key(serverSDK.server),
|
||||||
const directory = projectDirectory()
|
opened: serverCtx.projects.list,
|
||||||
if (!directory) return undefined
|
stored: () => serverCtx.sync.data.project,
|
||||||
return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
load: (search, signal) =>
|
||||||
})
|
serverSDK.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }),
|
||||||
const workspaces = createMemo(() => {
|
|
||||||
const directory = projectDirectory()
|
|
||||||
const current = project()
|
|
||||||
if (!current) return directory ? [directory] : []
|
|
||||||
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
|
||||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
|
||||||
return dirs
|
|
||||||
})
|
|
||||||
const homedir = createMemo(() => serverSync().data.path.home)
|
|
||||||
const sessions = createSessionEntries({
|
|
||||||
workspaces,
|
|
||||||
label: (directory) => {
|
|
||||||
const current = project()
|
|
||||||
const kind =
|
|
||||||
current && directory === current.worktree
|
|
||||||
? language.t("workspace.type.local")
|
|
||||||
: language.t("workspace.type.sandbox")
|
|
||||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
|
||||||
const home = homedir()
|
|
||||||
const path = home ? directory.replace(home, "~") : directory
|
|
||||||
const name = store.vcs?.branch ?? getFilename(directory)
|
|
||||||
return `${kind} : ${name || path}`
|
|
||||||
},
|
|
||||||
load: (directory) => serverSDK.client.session.list({ directory, roots: true }),
|
|
||||||
untitled: () => language.t("command.session.new"),
|
untitled: () => language.t("command.session.new"),
|
||||||
category: () => language.t("command.category.session"),
|
category: () => language.t("command.category.session"),
|
||||||
})
|
})
|
||||||
|
|
@ -191,8 +167,17 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.type === "session") {
|
if (item.type === "session") {
|
||||||
if (!item.directory || !item.sessionID) return
|
if (!item.sessionID || !item.server) return
|
||||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
const directory = item.project?.worktree ?? item.directory
|
||||||
|
if (directory) {
|
||||||
|
serverCtx.projects.open(directory)
|
||||||
|
serverCtx.projects.touch(directory)
|
||||||
|
}
|
||||||
|
const tab = appTabs.addSessionTab({
|
||||||
|
server: item.server,
|
||||||
|
sessionId: item.sessionID,
|
||||||
|
})
|
||||||
|
appTabs.select(tab)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!item.path) return
|
if (!item.path) return
|
||||||
|
|
@ -218,7 +203,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
export function createCommandPaletteCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
||||||
return {
|
return {
|
||||||
id: "command:" + option.id,
|
id: "command:" + option.id,
|
||||||
type: "command",
|
type: "command",
|
||||||
|
|
@ -230,96 +215,65 @@ function createCommandEntry(option: CommandOption, category: string): CommandPal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSessionEntries(props: {
|
export function createServerSessionEntries(props: {
|
||||||
workspaces: () => string[]
|
server: ServerConnection.Key
|
||||||
label: (directory: string) => string
|
opened: () => LocalProject[]
|
||||||
load: (directory: string) => ReturnType<ServerSDK["client"]["session"]["list"]>
|
stored: () => Project[]
|
||||||
|
load: (search: string, signal: AbortSignal) => Promise<{ data?: GlobalSession[] }>
|
||||||
untitled: () => string
|
untitled: () => string
|
||||||
category: () => string
|
category: () => string
|
||||||
}) {
|
}) {
|
||||||
const state: {
|
let abort: AbortController | undefined
|
||||||
token: number
|
|
||||||
inflight: Promise<CommandPaletteEntry[]> | undefined
|
|
||||||
cached: CommandPaletteEntry[] | undefined
|
|
||||||
} = { token: 0, inflight: undefined, cached: undefined }
|
|
||||||
|
|
||||||
return (text: string) => {
|
onCleanup(() => abort?.abort())
|
||||||
if (!text.trim()) {
|
|
||||||
state.token += 1
|
return async (text: string): Promise<CommandPaletteEntry[]> => {
|
||||||
state.inflight = undefined
|
const search = text.trim()
|
||||||
state.cached = undefined
|
if (!search) {
|
||||||
return [] as CommandPaletteEntry[]
|
abort?.abort()
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
if (state.cached) return state.cached
|
abort?.abort()
|
||||||
if (state.inflight) return state.inflight
|
const current = new AbortController()
|
||||||
|
abort = current
|
||||||
const current = state.token
|
await new Promise<void>((resolve) => {
|
||||||
const dirs = props.workspaces()
|
const timer = setTimeout(resolve, 100)
|
||||||
if (dirs.length === 0) return [] as CommandPaletteEntry[]
|
current.signal.addEventListener(
|
||||||
|
"abort",
|
||||||
state.inflight = Promise.all(
|
() => {
|
||||||
dirs.map((directory) => {
|
clearTimeout(timer)
|
||||||
const description = props.label(directory)
|
resolve()
|
||||||
return props
|
},
|
||||||
.load(directory)
|
{ once: true },
|
||||||
.then((result) =>
|
)
|
||||||
(result.data ?? [])
|
})
|
||||||
.filter((session) => !!session?.id)
|
if (current.signal.aborted) return []
|
||||||
.map((session) => ({
|
const opened = props.opened()
|
||||||
id: session.id,
|
const openedByID = new Map(opened.flatMap((project) => (project.id ? [[project.id, project] as const] : [])))
|
||||||
title: session.title ?? props.untitled(),
|
const stored = props.stored().map((project) => ({ ...project, expanded: false }))
|
||||||
description,
|
const storedByID = new Map(stored.map((project) => [project.id, project] as const))
|
||||||
directory,
|
return props
|
||||||
archived: session.time?.archived,
|
.load(search, current.signal)
|
||||||
updated: session.time?.updated,
|
.then((result) =>
|
||||||
})),
|
(result.data ?? [])
|
||||||
)
|
.filter((session) => !session.time.archived)
|
||||||
.catch(() => [] as SessionEntryInput[])
|
.map((session) => {
|
||||||
}),
|
const project =
|
||||||
)
|
projectForSession(session, opened, openedByID) ?? projectForSession(session, stored, storedByID)
|
||||||
.then((results) => {
|
return {
|
||||||
if (state.token !== current) return [] as CommandPaletteEntry[]
|
id: `session:${props.server}:${session.id}`,
|
||||||
const seen = new Set<string>()
|
type: "session" as const,
|
||||||
const next = results
|
title: session.title || props.untitled(),
|
||||||
.flat()
|
description: project ? displayName(project) : session.project?.name || getFilename(session.directory),
|
||||||
.filter((item) => {
|
category: props.category(),
|
||||||
const key = `${item.directory}:${item.id}`
|
directory: session.directory,
|
||||||
if (seen.has(key)) return false
|
sessionID: session.id,
|
||||||
seen.add(key)
|
server: props.server,
|
||||||
return true
|
project,
|
||||||
})
|
updated: session.time.updated,
|
||||||
.map((item) => createSessionEntry(item, props.category()))
|
}
|
||||||
state.cached = next
|
}),
|
||||||
return next
|
)
|
||||||
})
|
|
||||||
.catch(() => [] as CommandPaletteEntry[])
|
.catch(() => [] as CommandPaletteEntry[])
|
||||||
.finally(() => {
|
|
||||||
state.inflight = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
return state.inflight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionEntryInput = {
|
|
||||||
directory: string
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
archived?: number
|
|
||||||
updated?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSessionEntry(input: SessionEntryInput, category: string): CommandPaletteEntry {
|
|
||||||
return {
|
|
||||||
id: `session:${input.directory}:${input.id}`,
|
|
||||||
type: "session",
|
|
||||||
title: input.title,
|
|
||||||
description: input.description,
|
|
||||||
category,
|
|
||||||
directory: input.directory,
|
|
||||||
sessionID: input.id,
|
|
||||||
archived: input.archived,
|
|
||||||
updated: input.updated,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,20 @@ import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { formatKeybindParts } from "@/context/command"
|
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
|
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
|
||||||
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
import { useTabs } from "@/context/tabs"
|
||||||
|
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||||
import { getRelativeTime } from "@/utils/time"
|
import { getRelativeTime } from "@/utils/time"
|
||||||
import {
|
import {
|
||||||
|
createCommandPaletteCommandEntry,
|
||||||
createCommandPaletteFileEntry,
|
createCommandPaletteFileEntry,
|
||||||
createCommandPaletteModel,
|
createCommandPaletteModel,
|
||||||
|
createServerSessionEntries,
|
||||||
uniqueCommandPaletteEntries,
|
uniqueCommandPaletteEntries,
|
||||||
type CommandPaletteEntry,
|
type CommandPaletteEntry,
|
||||||
} from "./command-palette"
|
} from "./command-palette"
|
||||||
|
|
@ -30,9 +37,6 @@ function matchesEntry(entry: CommandPaletteEntry, query: string) {
|
||||||
|
|
||||||
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
||||||
const palette = createCommandPaletteModel(props)
|
const palette = createCommandPaletteModel(props)
|
||||||
const [query, setQuery] = createSignal("")
|
|
||||||
const [active, setActive] = createSignal(0)
|
|
||||||
|
|
||||||
const loadItems = async (text: string) => {
|
const loadItems = async (text: string) => {
|
||||||
const q = text.trim()
|
const q = text.trim()
|
||||||
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||||
|
|
@ -41,16 +45,105 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||||
const category = palette.language.t("palette.group.files")
|
const category = palette.language.t("palette.group.files")
|
||||||
return [
|
return [
|
||||||
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
||||||
...nextSessions.filter((entry) => matchesEntry(entry, q)),
|
...nextSessions,
|
||||||
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
return (
|
||||||
|
<CommandPaletteView
|
||||||
|
placeholder={palette.language.t("palette.search.placeholder")}
|
||||||
|
loadItems={loadItems}
|
||||||
|
highlight={palette.highlight}
|
||||||
|
select={palette.select}
|
||||||
|
close={palette.close}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogHomeCommandPaletteV2(props: {
|
||||||
|
server: ServerConnection.Any
|
||||||
|
onSelectSession: (entry: CommandPaletteEntry) => void
|
||||||
|
}) {
|
||||||
|
const command = useCommand()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const global = useGlobal()
|
||||||
|
const language = useLanguage()
|
||||||
|
const serverCtx = global.ensureServerCtx(props.server)
|
||||||
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
|
const commandEntries = createMemo(() => {
|
||||||
|
const category = language.t("palette.group.commands")
|
||||||
|
return commandPaletteOptions(command.options).map((option) => createCommandPaletteCommandEntry(option, category))
|
||||||
|
})
|
||||||
|
const sessions = createServerSessionEntries({
|
||||||
|
server: ServerConnection.key(props.server),
|
||||||
|
opened: serverCtx.projects.list,
|
||||||
|
stored: () => serverCtx.sync.data.project,
|
||||||
|
load: (search, signal) =>
|
||||||
|
serverCtx.sdk.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }),
|
||||||
|
untitled: () => language.t("command.session.new"),
|
||||||
|
category: () => language.t("command.category.session"),
|
||||||
|
})
|
||||||
|
|
||||||
|
const highlight = (item: CommandPaletteEntry | undefined) => {
|
||||||
|
state.cleanup?.()
|
||||||
|
state.cleanup = undefined
|
||||||
|
if (item?.type !== "command") return
|
||||||
|
state.cleanup = item.option?.onHighlight?.()
|
||||||
|
}
|
||||||
|
const select = (item: CommandPaletteEntry | undefined) => {
|
||||||
|
if (!item) return
|
||||||
|
state.committed = true
|
||||||
|
state.cleanup = undefined
|
||||||
|
dialog.close()
|
||||||
|
if (item.type === "command") {
|
||||||
|
item.option?.onSelect?.("palette")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.type === "session") props.onSelectSession(item)
|
||||||
|
}
|
||||||
|
const loadItems = async (text: string) => {
|
||||||
|
const query = text.trim()
|
||||||
|
if (!query) return commandEntries().slice(0, 5)
|
||||||
|
return [...commandEntries().filter((entry) => matchesEntry(entry, query)), ...(await sessions(query))]
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (state.committed) return
|
||||||
|
state.cleanup?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandPaletteView
|
||||||
|
placeholder={language.t("palette.search.placeholder.home")}
|
||||||
|
loadItems={loadItems}
|
||||||
|
highlight={highlight}
|
||||||
|
select={select}
|
||||||
|
close={() => dialog.close()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandPaletteView(props: {
|
||||||
|
placeholder: string
|
||||||
|
loadItems: (text: string) => CommandPaletteEntry[] | Promise<CommandPaletteEntry[]>
|
||||||
|
highlight: (item: CommandPaletteEntry | undefined) => void
|
||||||
|
select: (item: CommandPaletteEntry | undefined) => void
|
||||||
|
close: () => void
|
||||||
|
}) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const tabs = useTabs()
|
||||||
|
const [query, setQuery] = createSignal("")
|
||||||
|
const [active, setActive] = createSignal(0)
|
||||||
|
|
||||||
|
const [entries] = createResource(query, props.loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
||||||
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
||||||
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
||||||
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
||||||
const activeEntry = createMemo(() => visibleEntries()[active()])
|
const activeEntry = createMemo(() => visibleEntries()[active()])
|
||||||
|
const openSessions = createMemo(
|
||||||
|
() => new Set(tabs.store.flatMap((tab) => (tab.type === "session" ? [`${tab.server}\0${tab.sessionId}`] : []))),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
query()
|
query()
|
||||||
|
|
@ -59,7 +152,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
palette.highlight(activeEntry())
|
props.highlight(activeEntry())
|
||||||
})
|
})
|
||||||
|
|
||||||
let resultsRef: HTMLDivElement | undefined
|
let resultsRef: HTMLDivElement | undefined
|
||||||
|
|
@ -86,12 +179,12 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
palette.select(activeEntry())
|
props.select(activeEntry())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
palette.close()
|
props.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +198,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
appearance="large"
|
appearance="large"
|
||||||
placeholder={palette.language.t("palette.search.placeholder")}
|
placeholder={props.placeholder}
|
||||||
leadingIcon={<Icon name="magnifying-glass" />}
|
leadingIcon={<Icon name="magnifying-glass" />}
|
||||||
onInput={(event) => setQuery(event.currentTarget.value)}
|
onInput={(event) => setQuery(event.currentTarget.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
|
@ -117,7 +210,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||||
when={visibleEntries().length > 0}
|
when={visibleEntries().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="command-palette-v2-state">
|
<div class="command-palette-v2-state">
|
||||||
{entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")}
|
{entries.loading ? language.t("common.loading") : language.t("palette.empty")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -132,9 +225,14 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||||
<PaletteRow
|
<PaletteRow
|
||||||
item={item}
|
item={item}
|
||||||
active={activeEntry()?.id === item.id}
|
active={activeEntry()?.id === item.id}
|
||||||
language={palette.language}
|
language={language}
|
||||||
|
sessionOpen={
|
||||||
|
item.server && item.sessionID
|
||||||
|
? openSessions().has(`${item.server}\0${item.sessionID}`)
|
||||||
|
: false
|
||||||
|
}
|
||||||
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
||||||
onSelect={() => palette.select(item)}
|
onSelect={() => props.select(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -153,13 +251,19 @@ function PaletteRow(props: {
|
||||||
item: CommandPaletteEntry
|
item: CommandPaletteEntry
|
||||||
active: boolean
|
active: boolean
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
|
sessionOpen: boolean
|
||||||
onActive: () => void
|
onActive: () => void
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const session = () =>
|
||||||
|
props.item.server && props.item.directory && props.item.sessionID
|
||||||
|
? { server: props.item.server, directory: props.item.directory, sessionID: props.item.sessionID }
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="command-palette-v2-row"
|
class="command-palette-v2-row group"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={props.active}
|
aria-selected={props.active}
|
||||||
data-active={props.active ? "" : undefined}
|
data-active={props.active ? "" : undefined}
|
||||||
|
|
@ -197,7 +301,25 @@ function PaletteRow(props: {
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.item.type === "session"}>
|
<Match when={props.item.type === "session"}>
|
||||||
<div class="command-palette-v2-row-main">
|
<div class="command-palette-v2-row-main">
|
||||||
<Icon name="status" class="command-palette-v2-row-icon" />
|
<div class="relative shrink-0">
|
||||||
|
<Show when={props.sessionOpen}>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pointer-events-none absolute top-1/2 h-3 w-0.5 -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
|
||||||
|
style={{ right: "calc(100% + 4px)" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Show when={session()}>
|
||||||
|
{(session) => (
|
||||||
|
<SessionTabAvatar
|
||||||
|
project={props.item.project}
|
||||||
|
directory={session().directory}
|
||||||
|
sessionId={session().sessionID}
|
||||||
|
server={session().server}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
<div class="command-palette-v2-row-text">
|
<div class="command-palette-v2-row-text">
|
||||||
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
||||||
{props.item.title}
|
{props.item.title}
|
||||||
|
|
|
||||||
|
|
@ -8,49 +8,9 @@ import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4"
|
||||||
import homeImage from "@/assets/help/home.png"
|
import homeImage from "@/assets/help/home.png"
|
||||||
import tabsImage from "@/assets/help/tabs.png"
|
import tabsImage from "@/assets/help/tabs.png"
|
||||||
|
|
||||||
const helpIcon = (
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
data-slot="icon-svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.94235 10.5714V10.4854C6.94617 9.76302 7.01879 9.18777 7.16022 8.75968C7.30546 8.33158 7.50804 7.98567 7.76796 7.72193C8.02787 7.45819 8.34321 7.21548 8.71397 6.99379C8.93948 6.85619 9.14206 6.69374 9.32171 6.50645C9.50518 6.31916 9.64851 6.10511 9.75171 5.86431C9.85874 5.62351 9.91225 5.35404 9.91225 5.0559C9.91225 4.69661 9.82625 4.38509 9.65424 4.12136C9.48607 3.85762 9.26055 3.65504 8.9777 3.51362C8.69486 3.36837 8.38143 3.29575 8.03743 3.29575C7.73165 3.29575 7.43733 3.35882 7.15448 3.48495C6.87546 3.61108 6.6423 3.80984 6.45501 4.08122C6.26772 4.3526 6.15878 4.70425 6.12821 5.13617H4.56299C4.59357 4.47109 4.76557 3.9054 5.07899 3.43908C5.39242 2.96894 5.80522 2.61156 6.31741 2.36694C6.83341 2.12231 7.40675 2 8.03743 2C8.72161 2 9.31789 2.13378 9.82625 2.40134C10.3384 2.66507 10.734 3.0301 11.0131 3.49642C11.2959 3.96273 11.4373 4.49976 11.4373 5.1075C11.4373 5.53177 11.3724 5.914 11.2424 6.25418C11.1124 6.59436 10.9251 6.89823 10.6805 7.16579C10.4397 7.43335 10.1492 7.67033 9.80905 7.87673C9.48033 8.08313 9.21468 8.301 9.0121 8.53034C8.80952 8.75585 8.66237 9.02341 8.57063 9.33302C8.4789 9.64262 8.42921 10.0268 8.42156 10.4854V10.5714H6.94235ZM7.72782 14C7.43351 14 7.17933 13.8949 6.96528 13.6847C6.75506 13.4744 6.64994 13.2203 6.64994 12.9221C6.64994 12.6278 6.75506 12.3755 6.96528 12.1653C7.17933 11.9551 7.43351 11.85 7.72782 11.85C8.02214 11.85 8.27441 11.9551 8.48463 12.1653C8.69868 12.3755 8.8057 12.6278 8.8057 12.9221C8.8057 13.1209 8.75601 13.3024 8.65663 13.4668C8.55726 13.6273 8.4273 13.7573 8.26676 13.8567C8.10623 13.9522 7.92658 14 7.72782 14Z"
|
|
||||||
fill="var(--v2-icon-icon-base)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const triggerClass =
|
|
||||||
"size-7 !rounded-full shrink-0 bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]"
|
|
||||||
|
|
||||||
// TODO: wire to changelog / seen-state when available
|
// TODO: wire to changelog / seen-state when available
|
||||||
const showPopover = () => true
|
const showPopover = () => true
|
||||||
|
|
||||||
export function HelpButton() {
|
|
||||||
const platform = usePlatform()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="https://opencode.ai"
|
|
||||||
aria-label="Open the OpenCode website"
|
|
||||||
data-component="icon-button-v2"
|
|
||||||
data-size="large"
|
|
||||||
class={`${triggerClass} fixed bottom-5 right-5 z-50 flex items-center justify-center`}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
platform.openLink(event.currentTarget.href)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{helpIcon}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// can remove this after the tabs rollout has been out for a while
|
// can remove this after the tabs rollout has been out for a while
|
||||||
export function TabsInfoPopup() {
|
export function TabsInfoPopup() {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
@ -61,7 +21,7 @@ export function TabsInfoPopup() {
|
||||||
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
|
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
|
||||||
<Show when={settings.general.shouldDisplayTabsToast()}>
|
<Show when={settings.general.shouldDisplayTabsToast()}>
|
||||||
<div
|
<div
|
||||||
class="fixed bottom-14 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
|
class="fixed bottom-5 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
|
||||||
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
@ -20,7 +20,6 @@ const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({
|
||||||
|
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
|
||||||
|
|
||||||
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,19 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
import { commandPaletteOptions, resolveKeybindOption, upsertCommandRegistration, type CommandOption } from "./command"
|
||||||
|
|
||||||
|
const paletteOptions: CommandOption[] = [
|
||||||
|
{ id: "settings.open", title: "Open settings" },
|
||||||
|
{ id: "session.undo", title: "Undo" },
|
||||||
|
{ id: "file.open", title: "Open file" },
|
||||||
|
{ id: "hidden", title: "Hidden", hidden: true },
|
||||||
|
{ id: "disabled", title: "Disabled", disabled: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("commandPaletteOptions", () => {
|
||||||
|
test("keeps visible enabled commands", () => {
|
||||||
|
expect(commandPaletteOptions(paletteOptions).map((option) => option.id)).toEqual(["settings.open", "session.undo"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("upsertCommandRegistration", () => {
|
describe("upsertCommandRegistration", () => {
|
||||||
test("replaces keyed registrations", () => {
|
test("replaces keyed registrations", () => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Persist, persisted } from "@/utils/persist"
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
|
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
export const DEFAULT_PALETTE_KEYBIND = "mod+k,mod+shift+p"
|
||||||
const SUGGESTED_PREFIX = "suggested."
|
const SUGGESTED_PREFIX = "suggested."
|
||||||
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
||||||
|
|
||||||
|
|
@ -87,6 +87,13 @@ export interface CommandOption {
|
||||||
onHighlight?: () => (() => void) | void
|
onHighlight?: () => (() => void) | void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function commandPaletteOptions(options: CommandOption[]) {
|
||||||
|
return options.filter(
|
||||||
|
(option) =>
|
||||||
|
!option.disabled && !option.hidden && !option.id.startsWith(SUGGESTED_PREFIX) && option.id !== "file.open",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
||||||
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
||||||
}
|
}
|
||||||
|
|
@ -375,7 +382,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPalette = () => {
|
const showPalette = () => {
|
||||||
run("file.open", "palette")
|
run(PALETTE_ID, "palette")
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
|
||||||
29
packages/app/src/context/local-agent.test.ts
Normal file
29
packages/app/src/context/local-agent.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||||
|
|
||||||
|
describe("hasCustomAgent", () => {
|
||||||
|
test("detects explicitly custom agents", () => {
|
||||||
|
expect(hasCustomAgent([{ native: true }, { native: false }])).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores built-in and unclassified agents", () => {
|
||||||
|
expect(hasCustomAgent([{ native: true }, {}])).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveAgent", () => {
|
||||||
|
const agents = [{ name: "plan" }, { name: "build" }, { name: "custom" }]
|
||||||
|
|
||||||
|
test("uses the requested available agent", () => {
|
||||||
|
expect(resolveAgent(agents, "custom")?.name).toBe("custom")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("defaults to build", () => {
|
||||||
|
expect(resolveAgent(agents)?.name).toBe("build")
|
||||||
|
expect(resolveAgent(agents, "missing")?.name).toBe("build")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the first agent when build is unavailable", () => {
|
||||||
|
expect(resolveAgent([{ name: "custom" }], "missing")?.name).toBe("custom")
|
||||||
|
})
|
||||||
|
})
|
||||||
7
packages/app/src/context/local-agent.ts
Normal file
7
packages/app/src/context/local-agent.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function hasCustomAgent(items: Array<{ native?: boolean }>) {
|
||||||
|
return items.some((item) => item.native === false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAgent<T extends { name: string }>(items: T[], name?: string) {
|
||||||
|
return items.find((item) => item.name === name) ?? items.find((item) => item.name === "build") ?? items[0]
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,10 @@ import { useParams } from "@solidjs/router"
|
||||||
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
|
|
@ -62,9 +64,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const providers = useProviders(() => sdk().directory)
|
const providers = useProviders(() => sdk().directory)
|
||||||
const models = useModels()
|
const models = useModels()
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
const id = createMemo(() => params.id || undefined)
|
const id = createMemo(() => params.id || undefined)
|
||||||
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
||||||
|
const agentsVisible = createMemo(() => settings.visibility.customAgents() || hasCustomAgent(list()))
|
||||||
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
||||||
|
|
||||||
const [saved, setSaved, , savedReady] = persisted(
|
const [saved, setSaved, , savedReady] = persisted(
|
||||||
|
|
@ -107,9 +111,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickAgent = (name: string | undefined) => {
|
const pickAgent = (name: string | undefined) => {
|
||||||
const items = list()
|
return resolveAgent(list(), name)
|
||||||
if (items.length === 0) return
|
|
||||||
return items.find((item) => item.name === name) ?? items[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -180,8 +182,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
|
|
||||||
const agent = {
|
const agent = {
|
||||||
list,
|
list,
|
||||||
|
visible: agentsVisible,
|
||||||
current() {
|
current() {
|
||||||
return pickAgent(scope()?.agent ?? store.current)
|
return pickAgent(agentsVisible() ? (scope()?.agent ?? store.current) : "build")
|
||||||
},
|
},
|
||||||
set(name: string | undefined) {
|
set(name: string | undefined) {
|
||||||
const item = pickAgent(name)
|
const item = pickAgent(name)
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export const dict = {
|
||||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||||
|
"palette.search.placeholder.home": "البحث في الأوامر والجلسات",
|
||||||
"palette.empty": "لا توجد نتائج",
|
"palette.empty": "لا توجد نتائج",
|
||||||
"palette.group.commands": "الأوامر",
|
"palette.group.commands": "الأوامر",
|
||||||
"palette.group.files": "الملفات",
|
"palette.group.files": "الملفات",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export const dict = {
|
||||||
"command.session.unshare": "Parar de compartilhar sessão",
|
"command.session.unshare": "Parar de compartilhar sessão",
|
||||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||||
|
"palette.search.placeholder.home": "Buscar comandos e sessões",
|
||||||
"palette.empty": "Nenhum resultado encontrado",
|
"palette.empty": "Nenhum resultado encontrado",
|
||||||
"palette.group.commands": "Comandos",
|
"palette.group.commands": "Comandos",
|
||||||
"palette.group.files": "Arquivos",
|
"palette.group.files": "Arquivos",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||||
|
|
||||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||||
|
"palette.search.placeholder.home": "Pretraži komande i sesije",
|
||||||
"palette.empty": "Nema rezultata",
|
"palette.empty": "Nema rezultata",
|
||||||
"palette.group.commands": "Komande",
|
"palette.group.commands": "Komande",
|
||||||
"palette.group.files": "Datoteke",
|
"palette.group.files": "Datoteke",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Stop med at dele denne session",
|
"command.session.unshare.description": "Stop med at dele denne session",
|
||||||
|
|
||||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
||||||
|
"palette.search.placeholder.home": "Søg i kommandoer og sessioner",
|
||||||
"palette.empty": "Ingen resultater fundet",
|
"palette.empty": "Ingen resultater fundet",
|
||||||
"palette.group.commands": "Kommandoer",
|
"palette.group.commands": "Kommandoer",
|
||||||
"palette.group.files": "Filer",
|
"palette.group.files": "Filer",
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ export const dict = {
|
||||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||||
|
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
|
||||||
"palette.empty": "Keine Ergebnisse gefunden",
|
"palette.empty": "Keine Ergebnisse gefunden",
|
||||||
"palette.group.commands": "Befehle",
|
"palette.group.commands": "Befehle",
|
||||||
"palette.group.files": "Dateien",
|
"palette.group.files": "Dateien",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Stop sharing this session",
|
"command.session.unshare.description": "Stop sharing this session",
|
||||||
|
|
||||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||||
|
"palette.search.placeholder.home": "Search commands and sessions",
|
||||||
"palette.empty": "No results found",
|
"palette.empty": "No results found",
|
||||||
"palette.group.commands": "Commands",
|
"palette.group.commands": "Commands",
|
||||||
"palette.group.files": "Files",
|
"palette.group.files": "Files",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||||
|
|
||||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||||
|
"palette.search.placeholder.home": "Buscar comandos y sesiones",
|
||||||
"palette.empty": "No se encontraron resultados",
|
"palette.empty": "No se encontraron resultados",
|
||||||
"palette.group.commands": "Comandos",
|
"palette.group.commands": "Comandos",
|
||||||
"palette.group.files": "Archivos",
|
"palette.group.files": "Archivos",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export const dict = {
|
||||||
"command.session.unshare": "Ne plus partager la session",
|
"command.session.unshare": "Ne plus partager la session",
|
||||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||||
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
||||||
|
"palette.search.placeholder.home": "Rechercher des commandes et des sessions",
|
||||||
"palette.empty": "Aucun résultat trouvé",
|
"palette.empty": "Aucun résultat trouvé",
|
||||||
"palette.group.commands": "Commandes",
|
"palette.group.commands": "Commandes",
|
||||||
"palette.group.files": "Fichiers",
|
"palette.group.files": "Fichiers",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export const dict = {
|
||||||
"command.session.unshare": "セッションの共有を停止",
|
"command.session.unshare": "セッションの共有を停止",
|
||||||
"command.session.unshare.description": "このセッションの共有を停止",
|
"command.session.unshare.description": "このセッションの共有を停止",
|
||||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||||
|
"palette.search.placeholder.home": "コマンドとセッションを検索",
|
||||||
"palette.empty": "結果が見つかりません",
|
"palette.empty": "結果が見つかりません",
|
||||||
"palette.group.commands": "コマンド",
|
"palette.group.commands": "コマンド",
|
||||||
"palette.group.files": "ファイル",
|
"palette.group.files": "ファイル",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ export const dict = {
|
||||||
"command.session.unshare": "세션 공유 중지",
|
"command.session.unshare": "세션 공유 중지",
|
||||||
"command.session.unshare.description": "이 세션 공유 중지",
|
"command.session.unshare.description": "이 세션 공유 중지",
|
||||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||||
|
"palette.search.placeholder.home": "명령어 및 세션 검색",
|
||||||
"palette.empty": "결과 없음",
|
"palette.empty": "결과 없음",
|
||||||
"palette.group.commands": "명령어",
|
"palette.group.commands": "명령어",
|
||||||
"palette.group.files": "파일",
|
"palette.group.files": "파일",
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||||
|
|
||||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
||||||
|
"palette.search.placeholder.home": "Søk i kommandoer og sesjoner",
|
||||||
"palette.empty": "Ingen resultater funnet",
|
"palette.empty": "Ingen resultater funnet",
|
||||||
"palette.group.commands": "Kommandoer",
|
"palette.group.commands": "Kommandoer",
|
||||||
"palette.group.files": "Filer",
|
"palette.group.files": "Filer",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export const dict = {
|
||||||
"command.session.unshare": "Przestań udostępniać sesję",
|
"command.session.unshare": "Przestań udostępniać sesję",
|
||||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||||
|
"palette.search.placeholder.home": "Szukaj poleceń i sesji",
|
||||||
"palette.empty": "Brak wyników",
|
"palette.empty": "Brak wyników",
|
||||||
"palette.group.commands": "Polecenia",
|
"palette.group.commands": "Polecenia",
|
||||||
"palette.group.files": "Pliki",
|
"palette.group.files": "Pliki",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||||
|
|
||||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||||
|
"palette.search.placeholder.home": "Поиск команд и сессий",
|
||||||
"palette.empty": "Ничего не найдено",
|
"palette.empty": "Ничего не найдено",
|
||||||
"palette.group.commands": "Команды",
|
"palette.group.commands": "Команды",
|
||||||
"palette.group.files": "Файлы",
|
"palette.group.files": "Файлы",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||||
|
|
||||||
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||||
|
"palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน",
|
||||||
"palette.empty": "ไม่พบผลลัพธ์",
|
"palette.empty": "ไม่พบผลลัพธ์",
|
||||||
"palette.group.commands": "คำสั่ง",
|
"palette.group.commands": "คำสั่ง",
|
||||||
"palette.group.files": "ไฟล์",
|
"palette.group.files": "ไฟล์",
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
||||||
|
|
||||||
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
||||||
|
"palette.search.placeholder.home": "Komut ve oturum ara",
|
||||||
"palette.empty": "Sonuç bulunamadı",
|
"palette.empty": "Sonuç bulunamadı",
|
||||||
"palette.group.commands": "Komutlar",
|
"palette.group.commands": "Komutlar",
|
||||||
"palette.group.files": "Dosyalar",
|
"palette.group.files": "Dosyalar",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
||||||
|
|
||||||
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
||||||
|
"palette.search.placeholder.home": "Пошук команд і сесій",
|
||||||
"palette.empty": "Результатів не знайдено",
|
"palette.empty": "Результатів не знайдено",
|
||||||
"palette.group.commands": "Команди",
|
"palette.group.commands": "Команди",
|
||||||
"palette.group.files": "Файли",
|
"palette.group.files": "Файли",
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "停止分享此会话",
|
"command.session.unshare.description": "停止分享此会话",
|
||||||
|
|
||||||
"palette.search.placeholder": "搜索文件、命令和会话",
|
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||||
|
"palette.search.placeholder.home": "搜索命令和会话",
|
||||||
"palette.empty": "未找到结果",
|
"palette.empty": "未找到结果",
|
||||||
"palette.group.commands": "命令",
|
"palette.group.commands": "命令",
|
||||||
"palette.group.files": "文件",
|
"palette.group.files": "文件",
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ export const dict = {
|
||||||
"command.session.unshare.description": "停止分享此工作階段",
|
"command.session.unshare.description": "停止分享此工作階段",
|
||||||
|
|
||||||
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||||
|
"palette.search.placeholder.home": "搜尋命令和工作階段",
|
||||||
"palette.empty": "找不到結果",
|
"palette.empty": "找不到結果",
|
||||||
"palette.group.commands": "命令",
|
"palette.group.commands": "命令",
|
||||||
"palette.group.files": "檔案",
|
"palette.group.files": "檔案",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createRoot,
|
createRoot,
|
||||||
|
createSignal,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
on,
|
on,
|
||||||
|
|
@ -74,6 +75,23 @@ const HOME_SESSION_LIMIT = 64
|
||||||
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
||||||
const HOME_SESSION_HEADER_TEXT_HEIGHT = 16
|
const HOME_SESSION_HEADER_TEXT_HEIGHT = 16
|
||||||
const HOME_SESSION_HEADER_FADE_DISTANCE = 16
|
const HOME_SESSION_HEADER_FADE_DISTANCE = 16
|
||||||
|
|
||||||
|
function containHomeWheel(event: WheelEvent, viewport: HTMLElement) {
|
||||||
|
if (event.defaultPrevented || event.ctrlKey || !event.deltaY) return
|
||||||
|
if (!(event.target instanceof Element)) return
|
||||||
|
|
||||||
|
const scrollable = event.target.closest<HTMLElement>("[data-scrollable]")
|
||||||
|
if (
|
||||||
|
scrollable !== viewport &&
|
||||||
|
scrollable &&
|
||||||
|
(event.deltaY < 0
|
||||||
|
? scrollable.scrollTop > 0
|
||||||
|
: scrollable.scrollTop < scrollable.scrollHeight - scrollable.clientHeight)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
const SHOW_HOME_SESSION_ARCHIVE = false
|
const SHOW_HOME_SESSION_ARCHIVE = false
|
||||||
const HOME_ROW_LAYOUT =
|
const HOME_ROW_LAYOUT =
|
||||||
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
|
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
|
||||||
|
|
@ -144,6 +162,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||||
let content: HTMLDivElement | undefined
|
let content: HTMLDivElement | undefined
|
||||||
let positionFrame: number | undefined
|
let positionFrame: number | undefined
|
||||||
let resizeObserver: ResizeObserver | undefined
|
let resizeObserver: ResizeObserver | undefined
|
||||||
|
let stickyTop = HOME_SESSION_HEADER_STICKY_TOP
|
||||||
const headerRefs = new Map<HomeSessionGroup["id"], HTMLDivElement>()
|
const headerRefs = new Map<HomeSessionGroup["id"], HTMLDivElement>()
|
||||||
const headerOffsets = new Map<HomeSessionGroup["id"], number>()
|
const headerOffsets = new Map<HomeSessionGroup["id"], number>()
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
|
|
@ -202,6 +221,13 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||||
|
|
||||||
function updatePositionCache() {
|
function updatePositionCache() {
|
||||||
if (!viewport) return
|
if (!viewport) return
|
||||||
|
const header = groups()
|
||||||
|
.map((group) => headerRefs.get(group.id))
|
||||||
|
.find((el) => el !== undefined)
|
||||||
|
if (header && typeof getComputedStyle === "function") {
|
||||||
|
const top = Number.parseFloat(getComputedStyle(header).top)
|
||||||
|
if (Number.isFinite(top)) stickyTop = top
|
||||||
|
}
|
||||||
groups().forEach((group) => {
|
groups().forEach((group) => {
|
||||||
const el = headerRefs.get(group.id)
|
const el = headerRefs.get(group.id)
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|
@ -217,7 +243,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||||
.slice(index + 1)
|
.slice(index + 1)
|
||||||
.map((item) => headerOffsets.get(item.id))
|
.map((item) => headerOffsets.get(item.id))
|
||||||
.find((offset) => offset !== undefined)
|
.find((offset) => offset !== undefined)
|
||||||
const fadeEnd = HOME_SESSION_HEADER_STICKY_TOP + HOME_SESSION_HEADER_TEXT_HEIGHT
|
const fadeEnd = stickyTop + HOME_SESSION_HEADER_TEXT_HEIGHT
|
||||||
const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop
|
const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop
|
||||||
const opacity =
|
const opacity =
|
||||||
nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE))
|
nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE))
|
||||||
|
|
@ -271,6 +297,9 @@ export function NewHome() {
|
||||||
const marked = useMarked()
|
const marked = useMarked()
|
||||||
const openSettings = useSettingsCommand()
|
const openSettings = useSettingsCommand()
|
||||||
let focusSessionSearch: (() => void) | undefined
|
let focusSessionSearch: (() => void) | undefined
|
||||||
|
let sessionViewport: HTMLDivElement | undefined
|
||||||
|
const [sessionThumbTrack, setSessionThumbTrack] = createSignal<HTMLDivElement>()
|
||||||
|
const [sessionHoverTarget, setSessionHoverTarget] = createSignal<HTMLElement>()
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
search: "",
|
search: "",
|
||||||
searchFocused: false,
|
searchFocused: false,
|
||||||
|
|
@ -402,6 +431,34 @@ export function NewHome() {
|
||||||
}
|
}
|
||||||
|
|
||||||
command.register("home", () => [
|
command.register("home", () => [
|
||||||
|
{
|
||||||
|
id: "command.palette",
|
||||||
|
title: language.t("command.palette"),
|
||||||
|
hidden: true,
|
||||||
|
onSelect: async () => {
|
||||||
|
const conn = focusedServer()
|
||||||
|
if (!conn) return
|
||||||
|
const ctx = global.ensureServerCtx(conn)
|
||||||
|
const { DialogHomeCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2")
|
||||||
|
void dialog.show(() => (
|
||||||
|
<DialogHomeCommandPaletteV2
|
||||||
|
server={conn}
|
||||||
|
onSelectSession={(entry) => {
|
||||||
|
if (!entry.sessionID || !entry.directory || !entry.server) return
|
||||||
|
const sessionID = entry.sessionID
|
||||||
|
const server = entry.server
|
||||||
|
const directory = entry.project?.worktree ?? entry.directory
|
||||||
|
ctx.projects.open(directory)
|
||||||
|
ctx.projects.touch(directory)
|
||||||
|
void startTransition(() => {
|
||||||
|
const tab = tabs.addSessionTab({ server, sessionId: sessionID })
|
||||||
|
tabs.select(tab)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "home.sessions.search.focus",
|
id: "home.sessions.search.focus",
|
||||||
title: searchPlaceholder(),
|
title: searchPlaceholder(),
|
||||||
|
|
@ -541,127 +598,158 @@ export function NewHome() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 lg:overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
|
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
|
||||||
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
<ScrollView
|
||||||
<HomeProjectColumn
|
class="h-full [container-type:size]"
|
||||||
projects={projects()}
|
thumbContainer={sessionThumbTrack}
|
||||||
recentlyClosed={recentlyClosed()}
|
thumbHoverTarget={sessionHoverTarget}
|
||||||
homedir={homedir()}
|
viewportRef={(el) => {
|
||||||
selected={selection()}
|
sessionViewport = el
|
||||||
focusServer={focusServer}
|
sessionHeaderOpacity.setViewport(el)
|
||||||
selectProject={selectProject}
|
}}
|
||||||
openNewSession={openProjectNewSession}
|
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
||||||
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
onWheel={(event) => {
|
||||||
chooseProject={(conn) => void chooseProject(conn)}
|
if (!sessionViewport) return
|
||||||
editProject={editProject}
|
if (event.target instanceof Node && sessionViewport.contains(event.target)) return
|
||||||
closeProject={(conn, directory) => {
|
containHomeWheel(event, sessionViewport)
|
||||||
const next = closeHomeProject(
|
}}
|
||||||
selection(),
|
>
|
||||||
ServerConnection.key(conn),
|
<div class="mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
||||||
global.ensureServerCtx(conn).projects,
|
<HomeProjectColumn
|
||||||
directory,
|
projects={projects()}
|
||||||
)
|
recentlyClosed={recentlyClosed()}
|
||||||
if (next) setSelection(next)
|
homedir={homedir()}
|
||||||
}}
|
selected={selection()}
|
||||||
clearNotifications={clearNotifications}
|
focusServer={focusServer}
|
||||||
unseenCount={unseenCount}
|
selectProject={selectProject}
|
||||||
openSettings={openSettings}
|
openNewSession={openProjectNewSession}
|
||||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
||||||
language={language}
|
chooseProject={(conn) => void chooseProject(conn)}
|
||||||
/>
|
editProject={editProject}
|
||||||
|
closeProject={(conn, directory) => {
|
||||||
<section
|
const next = closeHomeProject(
|
||||||
class="min-h-0 min-w-0 flex-1 flex flex-col pt-6 lg:pt-12 relative"
|
selection(),
|
||||||
aria-label={language.t("sidebar.project.recentSessions")}
|
ServerConnection.key(conn),
|
||||||
>
|
global.ensureServerCtx(conn).projects,
|
||||||
<HomeSessionSearch
|
directory,
|
||||||
value={state.search}
|
)
|
||||||
placeholder={searchPlaceholder()}
|
if (next) setSelection(next)
|
||||||
open={searchOpen()}
|
}}
|
||||||
loading={sessionLoad.isLoading}
|
clearNotifications={clearNotifications}
|
||||||
results={searchResults()}
|
unseenCount={unseenCount}
|
||||||
showProjectName={!selectedProject()}
|
openSettings={openSettings}
|
||||||
server={selection().server}
|
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||||
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
language={language}
|
||||||
bindFocus={(focus) => {
|
onWheel={(event) => {
|
||||||
focusSessionSearch = focus
|
if (sessionViewport) containHomeWheel(event, sessionViewport)
|
||||||
}}
|
}}
|
||||||
onInput={(value) => setState("search", value)}
|
|
||||||
onFocus={() => setState("searchFocused", true)}
|
|
||||||
onClose={closeSearch}
|
|
||||||
onSelect={selectSearchSession}
|
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
|
||||||
class="mt-3 -mr-3 min-h-0 flex-1 relative"
|
<section
|
||||||
viewportRef={sessionHeaderOpacity.setViewport}
|
ref={setSessionHoverTarget}
|
||||||
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
class="min-h-0 min-w-0 flex-1 flex flex-col"
|
||||||
|
aria-label={language.t("sidebar.project.recentSessions")}
|
||||||
>
|
>
|
||||||
<Show when={groups().length > 0 && newSessionProject()}>
|
<div
|
||||||
<div class="pointer-events-none absolute top-3 right-3 z-20 flex">
|
class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12"
|
||||||
<ButtonV2
|
onWheel={(event) => {
|
||||||
data-action="home-new-session"
|
if (sessionViewport) containHomeWheel(event, sessionViewport)
|
||||||
variant="ghost-muted"
|
}}
|
||||||
size="normal"
|
|
||||||
icon="edit"
|
|
||||||
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
|
||||||
onClick={openNewSession}
|
|
||||||
>
|
|
||||||
{language.t("command.session.new")}
|
|
||||||
</ButtonV2>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show
|
|
||||||
when={!sessionLoad.isLoading}
|
|
||||||
fallback={
|
|
||||||
<div class="pt-3">
|
|
||||||
<HomeSessionSkeleton label={language.t("common.loading")} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Show
|
<HomeSessionSearch
|
||||||
when={groups().length > 0}
|
value={state.search}
|
||||||
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
placeholder={searchPlaceholder()}
|
||||||
>
|
open={searchOpen()}
|
||||||
<div ref={sessionHeaderOpacity.setContentRef} class="flex flex-col pt-3 pr-3 pb-16">
|
loading={sessionLoad.isLoading}
|
||||||
<For each={groups()}>
|
results={searchResults()}
|
||||||
{(group, index) => (
|
showProjectName={!selectedProject()}
|
||||||
<>
|
server={selection().server}
|
||||||
<HomeSessionGroupHeader
|
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
||||||
title={group.title}
|
bindFocus={(focus) => {
|
||||||
titleOpacity={sessionHeaderOpacity.titleOpacity(group.id)}
|
focusSessionSearch = focus
|
||||||
ref={(el) => sessionHeaderOpacity.setHeaderRef(group.id, el)}
|
}}
|
||||||
elevated={index() === 0}
|
onInput={(value) => setState("search", value)}
|
||||||
/>
|
onFocus={() => setState("searchFocused", true)}
|
||||||
<div
|
onClose={closeSearch}
|
||||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === groups().length - 1 ? "" : "mb-6"}`}
|
onSelect={selectSearchSession}
|
||||||
>
|
/>
|
||||||
<For each={group.sessions}>
|
<Show when={groups().length > 0 && newSessionProject()}>
|
||||||
{(record) => (
|
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
||||||
<HomeSessionRow
|
<ButtonV2
|
||||||
record={record}
|
data-action="home-new-session"
|
||||||
showProjectName={!selectedProject()}
|
variant="ghost-muted"
|
||||||
server={selection().server}
|
size="normal"
|
||||||
openSession={openSession}
|
icon="edit"
|
||||||
archiveSession={archiveSession}
|
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
||||||
/>
|
onClick={openNewSession}
|
||||||
)}
|
>
|
||||||
</For>
|
{language.t("command.session.new")}
|
||||||
</div>
|
</ButtonV2>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
</ScrollView>
|
{/* Sticky chrome for the portaled session scrollbar — matches old sessions ScrollView bounds */}
|
||||||
</section>
|
<div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
|
||||||
<HomeUtilityNav
|
<div
|
||||||
class="flex lg:hidden"
|
ref={setSessionThumbTrack}
|
||||||
openSettings={openSettings}
|
data-component="home-session-scroll-track"
|
||||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
|
||||||
language={language}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div class="-mr-3 min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
|
||||||
|
<Show
|
||||||
|
when={!sessionLoad.isLoading}
|
||||||
|
fallback={
|
||||||
|
<div class="pt-3">
|
||||||
|
<HomeSessionSkeleton label={language.t("common.loading")} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={groups().length > 0}
|
||||||
|
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
||||||
|
>
|
||||||
|
<div ref={sessionHeaderOpacity.setContentRef} class="flex flex-col pt-3 pr-3 pb-16">
|
||||||
|
<For each={groups()}>
|
||||||
|
{(group, index) => (
|
||||||
|
<>
|
||||||
|
<HomeSessionGroupHeader
|
||||||
|
title={group.title}
|
||||||
|
titleOpacity={sessionHeaderOpacity.titleOpacity(group.id)}
|
||||||
|
ref={(el) => sessionHeaderOpacity.setHeaderRef(group.id, el)}
|
||||||
|
elevated={index() === 0}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === groups().length - 1 ? "" : "mb-6"}`}
|
||||||
|
>
|
||||||
|
<For each={group.sessions}>
|
||||||
|
{(record) => (
|
||||||
|
<HomeSessionRow
|
||||||
|
record={record}
|
||||||
|
showProjectName={!selectedProject()}
|
||||||
|
server={selection().server}
|
||||||
|
openSession={openSession}
|
||||||
|
archiveSession={archiveSession}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<HomeUtilityNav
|
||||||
|
class="flex lg:hidden"
|
||||||
|
openSettings={openSettings}
|
||||||
|
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -683,6 +771,7 @@ function HomeProjectColumn(props: {
|
||||||
openSettings: () => void
|
openSettings: () => void
|
||||||
openHelp: () => void
|
openHelp: () => void
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
|
onWheel: (event: WheelEvent) => void
|
||||||
}) {
|
}) {
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
@ -699,8 +788,12 @@ function HomeProjectColumn(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:mt-14 lg:pt-[52px]"
|
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:sticky lg:top-14 lg:mt-14 lg:h-[calc(100cqh-56px)] lg:self-start lg:pt-[52px]"
|
||||||
aria-label={props.language.t("home.projects")}
|
aria-label={props.language.t("home.projects")}
|
||||||
|
onWheel={(event) => {
|
||||||
|
if (event.target === event.currentTarget) return
|
||||||
|
props.onWheel(event)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
||||||
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
||||||
|
|
@ -1418,7 +1511,7 @@ function HomeSessionGroupHeader(props: {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={props.ref}
|
ref={props.ref}
|
||||||
class={`pointer-events-none sticky top-3 flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
|
class={`pointer-events-none sticky top-[84px] lg:top-[108px] flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
|
||||||
>
|
>
|
||||||
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
||||||
{props.title}
|
{props.title}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { HelpButton, TabsInfoPopup } from "@/components/help-button"
|
import { TabsInfoPopup } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
|
|
@ -38,7 +38,6 @@ export default function NewLayout(props: ParentProps) {
|
||||||
</main>
|
</main>
|
||||||
{import.meta.env.DEV && <DebugBar inline />}
|
{import.meta.env.DEV && <DebugBar inline />}
|
||||||
<TabsInfoPopup />
|
<TabsInfoPopup />
|
||||||
<HelpButton />
|
|
||||||
<ToastRegion v2 />
|
<ToastRegion v2 />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { HelpButton, TabsInfoPopup } from "@/components/help-button"
|
import { TabsInfoPopup } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
import { useDirectoryPicker } from "@/components/directory-picker"
|
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
|
|
@ -2396,7 +2396,6 @@ export default function LegacyLayout(props: ParentProps) {
|
||||||
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
|
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
|
||||||
</div>
|
</div>
|
||||||
<TabsInfoPopup />
|
<TabsInfoPopup />
|
||||||
<HelpButton />
|
|
||||||
<ToastRegion v2={false} />
|
<ToastRegion v2={false} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useSearchParams } from "@solidjs/router"
|
import { useSearchParams } from "@solidjs/router"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { NewSessionDesignView } from "@/components/session"
|
import { NewSessionDesignView } from "@/components/session"
|
||||||
|
|
@ -51,6 +52,7 @@ export default function NewSessionPage() {
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
const dialog = useDialog()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const providers = useProviders(() => sdk().directory)
|
const providers = useProviders(() => sdk().directory)
|
||||||
const openProviderSettings = useSettingsDialog("providers")
|
const openProviderSettings = useSettingsDialog("providers")
|
||||||
|
|
@ -77,6 +79,15 @@ export default function NewSessionPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
command.register("new-session", () => [
|
command.register("new-session", () => [
|
||||||
|
{
|
||||||
|
id: "command.palette",
|
||||||
|
title: language.t("command.palette"),
|
||||||
|
hidden: true,
|
||||||
|
onSelect: async () => {
|
||||||
|
const { DialogSelectFile } = await import("@/components/dialog-select-file")
|
||||||
|
void dialog.show(() => <DialogSelectFile />)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "input.focus",
|
id: "input.focus",
|
||||||
title: language.t("command.input.focus"),
|
title: language.t("command.input.focus"),
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import { useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/r
|
||||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { ErrorPage } from "@/pages/error"
|
import { ErrorPage } from "@/pages/error"
|
||||||
import { CommentsProvider, useComments } from "@/context/comments"
|
import { CommentsProvider, useComments } from "@/context/comments"
|
||||||
|
import { useCommand } from "@/context/command"
|
||||||
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
@ -362,6 +363,7 @@ export default function Page() {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
|
const command = useCommand()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
@ -1098,6 +1100,14 @@ export default function Page() {
|
||||||
review: reviewTab,
|
review: reviewTab,
|
||||||
fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id,
|
fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id,
|
||||||
})
|
})
|
||||||
|
command.register("session-palette", () => [
|
||||||
|
{
|
||||||
|
id: "command.palette",
|
||||||
|
title: language.t("command.palette"),
|
||||||
|
hidden: true,
|
||||||
|
onSelect: () => command.trigger("file.open", "palette"),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const openReviewFile = createOpenReviewFile({
|
const openReviewFile = createOpenReviewFile({
|
||||||
showAllFiles,
|
showAllFiles,
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export function createPromptInputController(input: {
|
||||||
options: local.agent.list().map((agent) => agent.name),
|
options: local.agent.list().map((agent) => agent.name),
|
||||||
current: local.agent.current()?.name ?? "",
|
current: local.agent.current()?.name ?? "",
|
||||||
loading: agentsQuery.isLoading,
|
loading: agentsQuery.isLoading,
|
||||||
visible: settings.visibility.customAgents(),
|
visible: local.agent.visible(),
|
||||||
select: local.agent.set,
|
select: local.agent.set,
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLocal, type ModelSelection } from "@/context/local"
|
import { useLocal, type ModelSelection } from "@/context/local"
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom"
|
import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom"
|
||||||
import { useSessionLayout } from "./session-layout"
|
import { useSessionLayout } from "./session-layout"
|
||||||
|
|
@ -19,7 +18,6 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const settings = useSettings()
|
|
||||||
const { sessionKey } = useSessionLayout()
|
const { sessionKey } = useSessionLayout()
|
||||||
const sessionOwnership = createSessionOwnership(sessionKey)
|
const sessionOwnership = createSessionOwnership(sessionKey)
|
||||||
const model = input.model ?? local.model
|
const model = input.model ?? local.model
|
||||||
|
|
@ -70,7 +68,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
||||||
description: language.t("command.agent.cycle.description"),
|
description: language.t("command.agent.cycle.description"),
|
||||||
keybind: "mod+.",
|
keybind: "mod+.",
|
||||||
slash: "agent",
|
slash: "agent",
|
||||||
disabled: !settings.visibility.customAgents(),
|
disabled: !local.agent.visible(),
|
||||||
onSelect: () => local.agent.move(1),
|
onSelect: () => local.agent.move(1),
|
||||||
}),
|
}),
|
||||||
agentCommand({
|
agentCommand({
|
||||||
|
|
@ -78,7 +76,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
||||||
title: language.t("command.agent.cycle.reverse"),
|
title: language.t("command.agent.cycle.reverse"),
|
||||||
description: language.t("command.agent.cycle.reverse.description"),
|
description: language.t("command.agent.cycle.reverse.description"),
|
||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
disabled: !settings.visibility.customAgents(),
|
disabled: !local.agent.visible(),
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -468,7 +468,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
id: "file.open",
|
id: "file.open",
|
||||||
title: language.t("command.file.open"),
|
title: language.t("command.file.open"),
|
||||||
description: language.t("palette.search.placeholder"),
|
description: language.t("palette.search.placeholder"),
|
||||||
keybind: "mod+k,mod+p",
|
keybind: "mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
onSelect: openFile,
|
onSelect: openFile,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
76
packages/app/test-browser/command-palette.test.ts
Normal file
76
packages/app/test-browser/command-palette.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { createRoot } from "solid-js"
|
||||||
|
import { createServerSessionEntries } from "@/components/command-palette"
|
||||||
|
import type { LocalProject } from "@/context/layout"
|
||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
|
|
||||||
|
const stored: Project = {
|
||||||
|
id: "project-1",
|
||||||
|
name: "Palette project",
|
||||||
|
worktree: "/workspace/project",
|
||||||
|
sandboxes: [],
|
||||||
|
time: { created: 1, updated: 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: GlobalSession = {
|
||||||
|
id: "session-1",
|
||||||
|
slug: "session-1",
|
||||||
|
projectID: stored.id,
|
||||||
|
directory: stored.worktree,
|
||||||
|
title: "Palette session",
|
||||||
|
version: "1",
|
||||||
|
time: { created: 1, updated: 2 },
|
||||||
|
project: { id: stored.id, name: stored.name, worktree: stored.worktree },
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("command palette sessions", () => {
|
||||||
|
test("uses the home project avatar and cancels superseded searches", async () => {
|
||||||
|
const server = ServerConnection.Key.make("selected-server")
|
||||||
|
const opened: LocalProject = {
|
||||||
|
...stored,
|
||||||
|
icon: { override: "home-project-avatar" },
|
||||||
|
expanded: true,
|
||||||
|
}
|
||||||
|
const searches: string[] = []
|
||||||
|
const result = await new Promise<Awaited<ReturnType<ReturnType<typeof createServerSessionEntries>>>>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const search = createServerSessionEntries({
|
||||||
|
server,
|
||||||
|
opened: () => [opened],
|
||||||
|
stored: () => [{ ...stored, icon: { url: "stored-project-avatar" } }],
|
||||||
|
load: async (text) => {
|
||||||
|
searches.push(text)
|
||||||
|
return {
|
||||||
|
data: [session, { ...session, id: "archived-session", time: { ...session.time, archived: 3 } }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
untitled: () => "Untitled",
|
||||||
|
category: () => "Sessions",
|
||||||
|
})
|
||||||
|
const first = search("palette")
|
||||||
|
const second = search("palette session")
|
||||||
|
Promise.all([first, second])
|
||||||
|
.then(([cancelled, entries]) => {
|
||||||
|
expect(cancelled).toEqual([])
|
||||||
|
resolve(entries)
|
||||||
|
})
|
||||||
|
.catch(reject)
|
||||||
|
.finally(dispose)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(searches).toEqual(["palette session"])
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(getProjectAvatarSource(result[0]?.project?.id, result[0]?.project?.icon)).toBe("home-project-avatar")
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
server,
|
||||||
|
sessionID: session.id,
|
||||||
|
description: stored.name,
|
||||||
|
project: { id: stored.id, icon: opened.icon },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -633,6 +633,12 @@ export function RunSubagentSelectBody(props: {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.name.toLowerCase() === "up" && menu.selected() === 0) {
|
||||||
|
event.preventDefault()
|
||||||
|
props.onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose })
|
handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"description": "Effect-native confined code execution over schema-described tools",
|
"description": "Effect-native confined code execution over schema-described tools",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,8 @@ function DesktopRoot(props: { windowState: DesktopWindowState }) {
|
||||||
function App() {
|
function App() {
|
||||||
const wslServers = useWslServers()
|
const wslServers = useWslServers()
|
||||||
const ready = createMemo(
|
const ready = createMemo(
|
||||||
() => !defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading,
|
() =>
|
||||||
|
!defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading && !wslServers.isLoading,
|
||||||
)
|
)
|
||||||
const servers = createMemo(() => {
|
const servers = createMemo(() => {
|
||||||
const data = initializationData(sidecar)
|
const data = initializationData(sidecar)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"azure-cognitive-services": Effect.fnUntraced(function* () {
|
"azure-cognitive-services": Effect.fnUntraced(function* (provider: Info) {
|
||||||
const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
||||||
return {
|
return {
|
||||||
autoload: false,
|
autoload: false,
|
||||||
|
|
@ -285,7 +285,9 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||||
return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
|
return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
baseURL: resourceName
|
||||||
|
? `https://${resourceName}.cognitiveservices.azure.com/openai${provider.options?.useDeploymentBasedUrls ? "" : "/v1"}`
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -641,6 +641,42 @@ test("direct subagent panel renders active subagents", async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("direct subagent panel closes when moving up from the first item", async () => {
|
||||||
|
const [tabs] = createSignal([
|
||||||
|
subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" }),
|
||||||
|
subagent({ sessionID: "s-2", label: "General", description: "Write migration plan" }),
|
||||||
|
])
|
||||||
|
const [current] = createSignal<string | undefined>()
|
||||||
|
let closed = 0
|
||||||
|
|
||||||
|
const app = await testRender(
|
||||||
|
() => (
|
||||||
|
<box width={100} height={RUN_SUBAGENT_PANEL_ROWS}>
|
||||||
|
<RunSubagentSelectBody
|
||||||
|
theme={() => RUN_THEME_FALLBACK.footer}
|
||||||
|
tabs={tabs}
|
||||||
|
current={current}
|
||||||
|
onClose={() => closed++}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
),
|
||||||
|
{ width: 100, height: RUN_SUBAGENT_PANEL_ROWS },
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await app.renderOnce()
|
||||||
|
app.mockInput.pressKey("ARROW_DOWN")
|
||||||
|
app.mockInput.pressKey("ARROW_UP")
|
||||||
|
expect(closed).toBe(0)
|
||||||
|
|
||||||
|
app.mockInput.pressKey("ARROW_UP")
|
||||||
|
expect(closed).toBe(1)
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("direct queued prompt panel renders pending prompt actions", async () => {
|
test("direct queued prompt panel renders pending prompt actions", async () => {
|
||||||
const [prompts] = createSignal([
|
const [prompts] = createSignal([
|
||||||
{ messageID: "m-1", partID: "p-1", prompt: { text: "fix the auth test", parts: [] } },
|
{ messageID: "m-1", partID: "p-1", prompt: { text: "fix the auth test", parts: [] } },
|
||||||
|
|
@ -1087,7 +1123,7 @@ test("direct footer always offers backgrounding for a foreground subagent", asyn
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
|
|
||||||
expect(frame).toContain("GPT-5")
|
expect(frame).toContain("GPT-5")
|
||||||
expect(frame).toContain("xhigh · ctrl+b background · ctrl+x down subagents · ctrl+p cmd")
|
expect(frame).toContain("xhigh · ctrl+b background · ↓ subagents · ctrl+p cmd")
|
||||||
expect(frame).toContain("ctrl+b background")
|
expect(frame).toContain("ctrl+b background")
|
||||||
expect(frame).not.toContain("queued")
|
expect(frame).not.toContain("queued")
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Suspense } from "solid-js"
|
||||||
import { I18nProvider, useI18n } from "./context/i18n"
|
import { I18nProvider, useI18n } from "./context/i18n"
|
||||||
import { LanguageProvider } from "./context/language"
|
import { LanguageProvider } from "./context/language"
|
||||||
import { strip } from "./lib/language"
|
import { strip } from "./lib/language"
|
||||||
import "./app.css"
|
import "./routes/index.css"
|
||||||
|
|
||||||
function AppMeta() {
|
function AppMeta() {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "../routes/index.css"
|
|
||||||
import { Link, Meta, Title } from "@solidjs/meta"
|
import { Link, Meta, Title } from "@solidjs/meta"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import {
|
import {
|
||||||
|
|
@ -263,9 +262,8 @@ export default function ModelCompareDetailPage(props: ModelCompareDetailPageProp
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<ComparisonRadar models={models()} catalogModels={catalog()?.models ?? []} />
|
|
||||||
<div
|
<div
|
||||||
data-component="compare-detail-table"
|
data-component="compare-detail-comparison"
|
||||||
data-model-count={models().length}
|
data-model-count={models().length}
|
||||||
style={`--compare-detail-grid: ${comparisonDetailGridTemplate(models().length)}`}
|
style={`--compare-detail-grid: ${comparisonDetailGridTemplate(models().length)}`}
|
||||||
>
|
>
|
||||||
|
|
@ -276,6 +274,7 @@ export default function ModelCompareDetailPage(props: ModelCompareDetailPageProp
|
||||||
>
|
>
|
||||||
<ComparisonPairSelector catalogModels={selectorModels()} models={models()} />
|
<ComparisonPairSelector catalogModels={selectorModels()} models={models()} />
|
||||||
</div>
|
</div>
|
||||||
|
<ComparisonRadar models={models()} catalogModels={catalog()?.models ?? []} />
|
||||||
<div
|
<div
|
||||||
data-component="compare-detail-body-scroll"
|
data-component="compare-detail-body-scroll"
|
||||||
ref={(element) => (comparisonBodyScroll = element)}
|
ref={(element) => (comparisonBodyScroll = element)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "../index.css"
|
|
||||||
import { Meta, Title } from "@solidjs/meta"
|
import { Meta, Title } from "@solidjs/meta"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "../index.css"
|
|
||||||
import { Meta, Title } from "@solidjs/meta"
|
import { Meta, Title } from "@solidjs/meta"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "../index.css"
|
|
||||||
import { Meta, Title } from "@solidjs/meta"
|
import { Meta, Title } from "@solidjs/meta"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { createAsync } from "@solidjs/router"
|
import { createAsync } from "@solidjs/router"
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,11 @@
|
||||||
U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02;
|
U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"] {
|
[data-page="stats"] {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--color-background: #ffffff;
|
--color-background: #ffffff;
|
||||||
|
|
@ -6076,6 +6081,7 @@
|
||||||
height: 800px;
|
height: 800px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-right: 1px solid var(--stats-line);
|
border-right: 1px solid var(--stats-line);
|
||||||
|
border-bottom: 1px solid var(--stats-line);
|
||||||
border-left: 1px solid var(--stats-line);
|
border-left: 1px solid var(--stats-line);
|
||||||
color: var(--stats-text);
|
color: var(--stats-text);
|
||||||
background: var(--stats-bg);
|
background: var(--stats-bg);
|
||||||
|
|
@ -6273,7 +6279,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="compare-detail-table"] {
|
[data-page="stats"] [data-component="compare-detail-comparison"] {
|
||||||
--compare-detail-label-column: 292px;
|
--compare-detail-label-column: 292px;
|
||||||
--compare-detail-model-column-min: 360px;
|
--compare-detail-model-column-min: 360px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -6330,10 +6336,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="3"]
|
[data-component="compare-detail-comparison"][data-model-count="3"]
|
||||||
[data-section="compare-detail-selector"],
|
[data-section="compare-detail-selector"],
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="3"]
|
[data-component="compare-detail-comparison"][data-model-count="3"]
|
||||||
[data-section="compare-detail-matrix"] {
|
[data-section="compare-detail-matrix"] {
|
||||||
min-width: calc(
|
min-width: calc(
|
||||||
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
||||||
|
|
@ -6342,10 +6348,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="4"]
|
[data-component="compare-detail-comparison"][data-model-count="4"]
|
||||||
[data-section="compare-detail-selector"],
|
[data-section="compare-detail-selector"],
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="4"]
|
[data-component="compare-detail-comparison"][data-model-count="4"]
|
||||||
[data-section="compare-detail-matrix"] {
|
[data-section="compare-detail-matrix"] {
|
||||||
min-width: calc(
|
min-width: calc(
|
||||||
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
||||||
|
|
@ -6355,10 +6361,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="5"]
|
[data-component="compare-detail-comparison"][data-model-count="5"]
|
||||||
[data-section="compare-detail-selector"],
|
[data-section="compare-detail-selector"],
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="5"]
|
[data-component="compare-detail-comparison"][data-model-count="5"]
|
||||||
[data-section="compare-detail-matrix"] {
|
[data-section="compare-detail-matrix"] {
|
||||||
min-width: calc(
|
min-width: calc(
|
||||||
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
||||||
|
|
@ -6368,10 +6374,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="6"]
|
[data-component="compare-detail-comparison"][data-model-count="6"]
|
||||||
[data-section="compare-detail-selector"],
|
[data-section="compare-detail-selector"],
|
||||||
[data-page="stats"]
|
[data-page="stats"]
|
||||||
[data-component="compare-detail-table"][data-model-count="6"]
|
[data-component="compare-detail-comparison"][data-model-count="6"]
|
||||||
[data-section="compare-detail-matrix"] {
|
[data-section="compare-detail-matrix"] {
|
||||||
min-width: calc(
|
min-width: calc(
|
||||||
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
|
||||||
|
|
@ -6663,7 +6669,7 @@
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="compare-detail-table"] {
|
[data-page="stats"] [data-component="compare-detail-comparison"] {
|
||||||
--compare-detail-label-column: 220px;
|
--compare-detail-label-column: 220px;
|
||||||
--compare-detail-model-column-min: 320px;
|
--compare-detail-model-column-min: 320px;
|
||||||
}
|
}
|
||||||
|
|
@ -6754,7 +6760,7 @@
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="compare-detail-table"] {
|
[data-page="stats"] [data-component="compare-detail-comparison"] {
|
||||||
--compare-detail-label-column: 188px;
|
--compare-detail-label-column: 188px;
|
||||||
--compare-detail-model-column-min: 246px;
|
--compare-detail-model-column-min: 246px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import "./index.css"
|
|
||||||
import { Link, Meta, Title } from "@solidjs/meta"
|
import { Link, Meta, Title } from "@solidjs/meta"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
transition: opacity 200ms ease;
|
transition: opacity 200ms ease;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,19 @@ describe("scrollTopFromThumbPointer", () => {
|
||||||
expect(scrollTopFromThumbPointer({ ...input, pointer: 0 })).toBe(0)
|
expect(scrollTopFromThumbPointer({ ...input, pointer: 0 })).toBe(0)
|
||||||
expect(scrollTopFromThumbPointer({ ...input, pointer: 1_000 })).toBe(5_400)
|
expect(scrollTopFromThumbPointer({ ...input, pointer: 1_000 })).toBe(5_400)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses scrollClientHeight when the thumb track differs from the viewport", () => {
|
||||||
|
const input = {
|
||||||
|
pointer: 400,
|
||||||
|
viewportTop: 100,
|
||||||
|
grabOffset: 0,
|
||||||
|
clientHeight: 400,
|
||||||
|
scrollClientHeight: 800,
|
||||||
|
scrollHeight: 8_000,
|
||||||
|
thumbHeight: 40,
|
||||||
|
}
|
||||||
|
// track usable = 400 - 16 - 40 = 344; thumbTop = 400 - 100 - 8 = 292
|
||||||
|
// maxScroll = 8000 - 800 = 7200 → 292/344 * 7200
|
||||||
|
expect(scrollTopFromThumbPointer(input)).toBeCloseTo((292 / 344) * 7200)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
import { onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
mergeProps,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
splitProps,
|
||||||
|
type Accessor,
|
||||||
|
type ComponentProps,
|
||||||
|
} from "solid-js"
|
||||||
|
import { Portal } from "solid-js/web"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useI18n } from "../context/i18n"
|
import { useI18n } from "../context/i18n"
|
||||||
|
|
@ -8,7 +19,17 @@ export type ScrollViewThumbVisibility = "hover" | "scroll"
|
||||||
export interface ScrollViewProps extends ComponentProps<"div"> {
|
export interface ScrollViewProps extends ComponentProps<"div"> {
|
||||||
viewportRef?: (el: HTMLDivElement) => void
|
viewportRef?: (el: HTMLDivElement) => void
|
||||||
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
|
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
|
||||||
|
/**
|
||||||
|
* `hover`: show while hovered or scrolling. `scroll`: show only while scrolling.
|
||||||
|
*
|
||||||
|
* In most cases, scrolling a container = hovering over it, so this change has no effect.
|
||||||
|
* This is a special case to account for the home page scroll, where scrolling a container != hovering over it
|
||||||
|
* */
|
||||||
thumbVisibility?: ScrollViewThumbVisibility
|
thumbVisibility?: ScrollViewThumbVisibility
|
||||||
|
/** Mount the thumb into an external track. Scroll metrics still come from this ScrollView. */
|
||||||
|
thumbContainer?: HTMLElement | Accessor<HTMLElement | undefined>
|
||||||
|
/** Element whose hover reveals the thumb. Defaults to the ScrollView root when unset. */
|
||||||
|
thumbHoverTarget?: HTMLElement | Accessor<HTMLElement | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
|
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
|
||||||
|
|
@ -65,12 +86,14 @@ export function scrollTopFromThumbPointer(input: {
|
||||||
clientHeight: number
|
clientHeight: number
|
||||||
scrollHeight: number
|
scrollHeight: number
|
||||||
thumbHeight: number
|
thumbHeight: number
|
||||||
|
/** Viewport height used for max scroll. Defaults to `clientHeight` (track == viewport). */
|
||||||
|
scrollClientHeight?: number
|
||||||
}) {
|
}) {
|
||||||
const padding = 8
|
const padding = 8
|
||||||
const maxThumbTop = input.clientHeight - padding * 2 - input.thumbHeight
|
const maxThumbTop = input.clientHeight - padding * 2 - input.thumbHeight
|
||||||
if (maxThumbTop <= 0) return 0
|
if (maxThumbTop <= 0) return 0
|
||||||
const thumbTop = Math.max(0, Math.min(input.pointer - input.viewportTop - padding - input.grabOffset, maxThumbTop))
|
const thumbTop = Math.max(0, Math.min(input.pointer - input.viewportTop - padding - input.grabOffset, maxThumbTop))
|
||||||
return (thumbTop / maxThumbTop) * Math.max(0, input.scrollHeight - input.clientHeight)
|
return (thumbTop / maxThumbTop) * Math.max(0, input.scrollHeight - (input.scrollClientHeight ?? input.clientHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScrollView(props: ScrollViewProps) {
|
export function ScrollView(props: ScrollViewProps) {
|
||||||
|
|
@ -78,7 +101,16 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
const merged = mergeProps({ orientation: "vertical", thumbVisibility: "hover" }, props)
|
const merged = mergeProps({ orientation: "vertical", thumbVisibility: "hover" }, props)
|
||||||
const [local, events, rest] = splitProps(
|
const [local, events, rest] = splitProps(
|
||||||
merged,
|
merged,
|
||||||
["class", "children", "viewportRef", "orientation", "thumbVisibility", "style"],
|
[
|
||||||
|
"class",
|
||||||
|
"children",
|
||||||
|
"viewportRef",
|
||||||
|
"orientation",
|
||||||
|
"thumbVisibility",
|
||||||
|
"thumbContainer",
|
||||||
|
"thumbHoverTarget",
|
||||||
|
"style",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"onScroll",
|
"onScroll",
|
||||||
"onWheel",
|
"onWheel",
|
||||||
|
|
@ -96,6 +128,15 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
let viewportRef!: HTMLDivElement
|
let viewportRef!: HTMLDivElement
|
||||||
let thumbRef!: HTMLDivElement
|
let thumbRef!: HTMLDivElement
|
||||||
|
|
||||||
|
const resolveEl = (value: HTMLElement | Accessor<HTMLElement | undefined> | undefined) => {
|
||||||
|
if (typeof value === "function") return value()
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbMount = createMemo(() => resolveEl(local.thumbContainer))
|
||||||
|
const thumbHover = createMemo(() => resolveEl(local.thumbHoverTarget))
|
||||||
|
const hoverRoot = () => !local.thumbHoverTarget && !local.thumbContainer
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
isHovered: false,
|
isHovered: false,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
|
|
@ -114,7 +155,6 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
let scrollIdleTimer: ReturnType<typeof setTimeout> | undefined
|
let scrollIdleTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
const markScrolling = () => {
|
const markScrolling = () => {
|
||||||
if (local.thumbVisibility !== "scroll") return
|
|
||||||
setState("isScrolling", true)
|
setState("isScrolling", true)
|
||||||
if (scrollIdleTimer !== undefined) clearTimeout(scrollIdleTimer)
|
if (scrollIdleTimer !== undefined) clearTimeout(scrollIdleTimer)
|
||||||
scrollIdleTimer = setTimeout(() => setState("isScrolling", false), 800)
|
scrollIdleTimer = setTimeout(() => setState("isScrolling", false), 800)
|
||||||
|
|
@ -122,8 +162,8 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
|
|
||||||
const thumbVisible = () => {
|
const thumbVisible = () => {
|
||||||
if (isDragging()) return true
|
if (isDragging()) return true
|
||||||
if (local.thumbVisibility === "scroll") return isScrolling()
|
if (isScrolling()) return true
|
||||||
return isHovered()
|
return local.thumbVisibility === "hover" && isHovered()
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|
@ -141,7 +181,8 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
|
|
||||||
setState("showThumb", true)
|
setState("showThumb", true)
|
||||||
const trackPadding = 8
|
const trackPadding = 8
|
||||||
const trackHeight = clientHeight - trackPadding * 2
|
const trackClientHeight = thumbMount()?.clientHeight || clientHeight
|
||||||
|
const trackHeight = trackClientHeight - trackPadding * 2
|
||||||
|
|
||||||
const minThumbHeight = 32
|
const minThumbHeight = 32
|
||||||
// Calculate raw thumb height based on ratio
|
// Calculate raw thumb height based on ratio
|
||||||
|
|
@ -165,16 +206,40 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
local.viewportRef(viewportRef)
|
local.viewportRef(viewportRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
createResizeObserver([viewportRef, viewportRef.firstElementChild], updateThumb)
|
createResizeObserver(
|
||||||
|
() => [viewportRef, viewportRef.firstElementChild, thumbMount()].filter(Boolean) as HTMLElement[],
|
||||||
|
updateThumb,
|
||||||
|
)
|
||||||
|
|
||||||
updateThumb()
|
updateThumb()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
thumbMount()
|
||||||
|
updateThumb()
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const target = thumbHover()
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
const enter = () => setState("isHovered", true)
|
||||||
|
const leave = () => setState("isHovered", false)
|
||||||
|
target.addEventListener("pointerenter", enter)
|
||||||
|
target.addEventListener("pointerleave", leave)
|
||||||
|
onCleanup(() => {
|
||||||
|
target.removeEventListener("pointerenter", enter)
|
||||||
|
target.removeEventListener("pointerleave", leave)
|
||||||
|
setState("isHovered", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const onThumbPointerDown = (e: PointerEvent) => {
|
const onThumbPointerDown = (e: PointerEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setState("isDragging", true)
|
setState("isDragging", true)
|
||||||
const grabOffset = e.clientY - thumbRef.getBoundingClientRect().top
|
const grabOffset = e.clientY - thumbRef.getBoundingClientRect().top
|
||||||
|
const track = thumbMount() ?? viewportRef
|
||||||
|
|
||||||
thumbRef.setPointerCapture(e.pointerId)
|
thumbRef.setPointerCapture(e.pointerId)
|
||||||
|
|
||||||
|
|
@ -182,9 +247,10 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
const { scrollHeight, clientHeight } = viewportRef
|
const { scrollHeight, clientHeight } = viewportRef
|
||||||
viewportRef.scrollTop = scrollTopFromThumbPointer({
|
viewportRef.scrollTop = scrollTopFromThumbPointer({
|
||||||
pointer: e.clientY,
|
pointer: e.clientY,
|
||||||
viewportTop: viewportRef.getBoundingClientRect().top,
|
viewportTop: track.getBoundingClientRect().top,
|
||||||
grabOffset,
|
grabOffset,
|
||||||
clientHeight,
|
clientHeight: track.clientHeight,
|
||||||
|
scrollClientHeight: clientHeight,
|
||||||
scrollHeight,
|
scrollHeight,
|
||||||
thumbHeight: thumbHeight(),
|
thumbHeight: thumbHeight(),
|
||||||
})
|
})
|
||||||
|
|
@ -203,6 +269,23 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
thumbRef.addEventListener("pointercancel", done)
|
thumbRef.addEventListener("pointercancel", done)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderThumb = () => (
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
thumbRef = el
|
||||||
|
}}
|
||||||
|
onPointerDown={onThumbPointerDown}
|
||||||
|
class="scroll-view__thumb"
|
||||||
|
data-visible={thumbVisible()}
|
||||||
|
data-dragging={isDragging()}
|
||||||
|
style={{
|
||||||
|
height: `${thumbHeight()}px`,
|
||||||
|
transform: `translateY(${thumbTop()}px)`,
|
||||||
|
"z-index": 100, // ensure it displays over content
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
// Keybinds implementation
|
// Keybinds implementation
|
||||||
// We ensure the viewport has a tabindex so it can receive focus
|
// We ensure the viewport has a tabindex so it can receive focus
|
||||||
// We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior,
|
// We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior,
|
||||||
|
|
@ -253,8 +336,12 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
class={`scroll-view ${local.class || ""}`}
|
class={`scroll-view ${local.class || ""}`}
|
||||||
style={local.style}
|
style={local.style}
|
||||||
onPointerEnter={() => setState("isHovered", true)}
|
onPointerEnter={() => {
|
||||||
onPointerLeave={() => setState("isHovered", false)}
|
if (hoverRoot()) setState("isHovered", true)
|
||||||
|
}}
|
||||||
|
onPointerLeave={() => {
|
||||||
|
if (hoverRoot()) setState("isHovered", false)
|
||||||
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{/* Viewport */}
|
{/* Viewport */}
|
||||||
|
|
@ -290,20 +377,11 @@ export function ScrollView(props: ScrollViewProps) {
|
||||||
{local.children}
|
{local.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Thumb Overlay */}
|
{/* Thumb Overlay — optionally portaled into an external track */}
|
||||||
<Show when={showThumb()}>
|
<Show when={showThumb()}>
|
||||||
<div
|
<Show when={thumbMount()} fallback={renderThumb()}>
|
||||||
ref={thumbRef}
|
{(mount) => <Portal mount={mount()}>{renderThumb()}</Portal>}
|
||||||
onPointerDown={onThumbPointerDown}
|
</Show>
|
||||||
class="scroll-view__thumb"
|
|
||||||
data-visible={thumbVisible()}
|
|
||||||
data-dragging={isDragging()}
|
|
||||||
style={{
|
|
||||||
height: `${thumbHeight()}px`,
|
|
||||||
transform: `translateY(${thumbTop()}px)`,
|
|
||||||
"z-index": 100, // ensure it displays over content
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue