mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-24 05:35:15 +00:00
first pass
This commit is contained in:
parent
375444a149
commit
38129f3663
9 changed files with 556 additions and 272 deletions
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {},
|
||||
"plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"],
|
||||
"permission": {
|
||||
"edit": {
|
||||
"packages/opencode/migration/*": "deny",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useDialog } from "@tui/ui/dialog"
|
|||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||
import { createMemo, createResource, createSignal, onMount, type JSX } from "solid-js"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
|
|
@ -10,15 +10,13 @@ import { useTheme } from "../context/theme"
|
|||
import { useSDK } from "../context/sdk"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { createDebouncedSignal } from "../util/signal"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
|
||||
import { DialogWorkspaceCreate, restoreWorkspaceSession } from "./dialog-workspace-create"
|
||||
import { Spinner } from "./spinner"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
|
||||
|
||||
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
||||
import { WorkspaceLabel } from "./workspace-label"
|
||||
|
||||
export function DialogSessionList() {
|
||||
const dialog = useDialog()
|
||||
|
|
@ -44,23 +42,6 @@ export function DialogSessionList() {
|
|||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||
const sessions = createMemo(() => searchResults() ?? sync.data.session)
|
||||
|
||||
function createWorkspace() {
|
||||
dialog.replace(() => (
|
||||
<DialogWorkspaceCreate
|
||||
onSelect={(workspaceID) =>
|
||||
openWorkspaceSession({
|
||||
dialog,
|
||||
route,
|
||||
sdk,
|
||||
sync,
|
||||
toast,
|
||||
workspaceID,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
|
||||
const workspace = project.workspace.get(session.workspaceID!)
|
||||
const list = () => dialog.replace(() => <DialogSessionList />)
|
||||
|
|
@ -124,30 +105,18 @@ export function DialogSessionList() {
|
|||
.map((x) => {
|
||||
const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
|
||||
|
||||
let workspaceStatus: WorkspaceStatus | null = null
|
||||
if (x.workspaceID) {
|
||||
workspaceStatus = project.workspace.status(x.workspaceID) || "error"
|
||||
}
|
||||
|
||||
let footer = ""
|
||||
let footer: JSX.Element | string = ""
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||
if (x.workspaceID) {
|
||||
let desc = "unknown"
|
||||
if (workspace) {
|
||||
desc = `${workspace.type}: ${workspace.name}`
|
||||
}
|
||||
|
||||
footer = (
|
||||
<>
|
||||
{desc}{" "}
|
||||
<span
|
||||
style={{
|
||||
fg: workspaceStatus === "connected" ? theme.success : theme.error,
|
||||
}}
|
||||
>
|
||||
●
|
||||
</span>
|
||||
</>
|
||||
footer = workspace ? (
|
||||
<WorkspaceLabel
|
||||
type={workspace.type}
|
||||
name={workspace.name}
|
||||
status={project.workspace.status(x.workspaceID) ?? "error"}
|
||||
icon
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceLabel type="unknown" name={x.workspaceID} status="error" icon />
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -250,15 +219,6 @@ export function DialogSessionList() {
|
|||
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+w")[0],
|
||||
title: "new workspace",
|
||||
side: "right",
|
||||
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||
onTrigger: () => {
|
||||
createWorkspace()
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import type { Workspace } from "@opencode-ai/sdk/v2"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { createMemo, createSignal, onMount } from "solid-js"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { WorkspaceLabel } from "./workspace-label"
|
||||
|
||||
type Adaptor = {
|
||||
type: string
|
||||
|
|
@ -16,54 +15,24 @@ type Adaptor = {
|
|||
description: string
|
||||
}
|
||||
|
||||
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
||||
return createOpencodeClient({
|
||||
baseUrl: sdk.url,
|
||||
fetch: sdk.fetch,
|
||||
directory: sync.path.directory || sdk.directory,
|
||||
experimental_workspaceID: workspaceID,
|
||||
})
|
||||
}
|
||||
|
||||
export async function openWorkspaceSession(input: {
|
||||
dialog: ReturnType<typeof useDialog>
|
||||
route: ReturnType<typeof useRoute>
|
||||
sdk: ReturnType<typeof useSDK>
|
||||
sync: ReturnType<typeof useSync>
|
||||
toast: ReturnType<typeof useToast>
|
||||
workspaceID: string
|
||||
}) {
|
||||
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
||||
|
||||
while (true) {
|
||||
const result = await client.session.create({ workspace: input.workspaceID }).catch(() => undefined)
|
||||
if (!result) {
|
||||
input.toast.show({
|
||||
message: "Failed to create workspace session",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
export type WorkspaceSelection =
|
||||
| {
|
||||
type: "none"
|
||||
}
|
||||
if (result.response?.status && result.response.status >= 500 && result.response.status < 600) {
|
||||
await sleep(1000)
|
||||
continue
|
||||
| {
|
||||
type: "new"
|
||||
workspaceType: string
|
||||
workspaceName: string
|
||||
}
|
||||
if (!result.data) {
|
||||
input.toast.show({
|
||||
message: "Failed to create workspace session",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
| {
|
||||
type: "existing"
|
||||
workspaceID: string
|
||||
workspaceType: string
|
||||
workspaceName: string
|
||||
}
|
||||
|
||||
input.route.navigate({
|
||||
type: "session",
|
||||
sessionID: result.data.id,
|
||||
})
|
||||
input.dialog.clear()
|
||||
return
|
||||
}
|
||||
}
|
||||
type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" } | { type: "loading" }
|
||||
type ExistingWorkspaceSelectValue = { workspace: Workspace }
|
||||
|
||||
export async function restoreWorkspaceSession(input: {
|
||||
dialog: ReturnType<typeof useDialog>
|
||||
|
|
@ -101,13 +70,11 @@ export async function restoreWorkspaceSession(input: {
|
|||
input.dialog.clear()
|
||||
}
|
||||
|
||||
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
||||
function DialogWorkspaceTypeSelect(props: { onSelect: (adaptor: Adaptor) => Promise<void> | void }) {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const project = useProject()
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const [creating, setCreating] = createSignal<string>()
|
||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -131,6 +98,185 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||
})()
|
||||
})
|
||||
|
||||
const options = createMemo(() => {
|
||||
const list = adaptors()
|
||||
if (!list) {
|
||||
return [
|
||||
{
|
||||
title: "Loading workspaces...",
|
||||
value: undefined,
|
||||
description: "Fetching available workspace adaptors",
|
||||
},
|
||||
]
|
||||
}
|
||||
return list.map((item) => ({
|
||||
title: item.name,
|
||||
value: item,
|
||||
description: item.description,
|
||||
}))
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title="New Workspace"
|
||||
skipFilter={true}
|
||||
renderFilter={false}
|
||||
options={options()}
|
||||
onSelect={async (option) => {
|
||||
if (!option.value) return
|
||||
void props.onSelect(option.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DialogWorkspaceSelect(props: {
|
||||
current?: WorkspaceSelection
|
||||
onSelect: (selection: WorkspaceSelection) => Promise<void> | void
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
void (async () => {
|
||||
const dir = sync.path.directory || sdk.directory
|
||||
const url = new URL("/experimental/workspace/adaptor", sdk.url)
|
||||
if (dir) url.searchParams.set("directory", dir)
|
||||
const res = await sdk
|
||||
.fetch(url)
|
||||
.then((x) => x.json() as Promise<Adaptor[]>)
|
||||
.catch(() => undefined)
|
||||
if (!res) {
|
||||
toast.show({
|
||||
message: "Failed to load workspace adaptors",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
setAdaptors(res)
|
||||
})()
|
||||
})
|
||||
|
||||
const options = createMemo<DialogSelectOption<WorkspaceSelectValue>[]>(() => {
|
||||
const list = adaptors()
|
||||
if (!list) {
|
||||
return [
|
||||
{
|
||||
title: "Loading workspaces...",
|
||||
value: { type: "loading" as const },
|
||||
description: "Fetching available workspace adaptors",
|
||||
category: "New workspace",
|
||||
},
|
||||
]
|
||||
}
|
||||
const workspaces = project.workspace.list()
|
||||
return [
|
||||
...list.map((adaptor) => ({
|
||||
title: adaptor.name,
|
||||
value: { type: "new" as const, workspaceType: adaptor.type, workspaceName: adaptor.name },
|
||||
description: adaptor.description,
|
||||
category: "New workspace",
|
||||
})),
|
||||
{
|
||||
title: "None",
|
||||
value: { type: "none" as const },
|
||||
description: "Use the local project",
|
||||
category: "Choose workspace",
|
||||
},
|
||||
...workspaces.slice(0, 3).map((workspace: Workspace) => ({
|
||||
title: workspace.name,
|
||||
description: `(${workspace.type})`,
|
||||
value: {
|
||||
type: "existing" as const,
|
||||
workspaceID: workspace.id,
|
||||
workspaceType: workspace.type,
|
||||
workspaceName: workspace.name,
|
||||
},
|
||||
category: "Choose workspace",
|
||||
})),
|
||||
{
|
||||
title: "View all workspaces",
|
||||
value: { type: "existing-list" as const },
|
||||
description: "Choose from all workspaces",
|
||||
category: "Choose workspace",
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect<WorkspaceSelectValue>
|
||||
title="Warp"
|
||||
skipFilter={true}
|
||||
renderFilter={false}
|
||||
options={options()}
|
||||
current={props.current}
|
||||
onSelect={(option) => {
|
||||
if (!option.value) return
|
||||
if (option.value.type === "none") {
|
||||
void props.onSelect(option.value)
|
||||
return
|
||||
}
|
||||
if (option.value.type === "new") {
|
||||
void props.onSelect(option.value)
|
||||
return
|
||||
}
|
||||
if (option.value.type === "existing") {
|
||||
void props.onSelect(option.value)
|
||||
return
|
||||
}
|
||||
|
||||
dialog.replace(() => <DialogExistingWorkspaceSelect onSelect={props.onSelect} />)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogExistingWorkspaceSelect(props: { onSelect: (selection: WorkspaceSelection) => Promise<void> | void }) {
|
||||
const project = useProject()
|
||||
|
||||
const options = createMemo<DialogSelectOption<ExistingWorkspaceSelectValue>[]>(() =>
|
||||
project.workspace
|
||||
.list()
|
||||
.filter((workspace) => project.workspace.status(workspace.id) === "connected")
|
||||
.map((workspace: Workspace) => ({
|
||||
title: workspace.name,
|
||||
description: `(${workspace.type})`,
|
||||
value: { workspace },
|
||||
})),
|
||||
)
|
||||
|
||||
return (
|
||||
<DialogSelect<ExistingWorkspaceSelectValue>
|
||||
title="Existing Workspace"
|
||||
options={options()}
|
||||
onSelect={(option) => {
|
||||
void props.onSelect({
|
||||
type: "existing",
|
||||
workspaceID: option.value.workspace.id,
|
||||
workspaceType: option.value.workspace.type,
|
||||
workspaceName: option.value.workspace.name,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
||||
const dialog = useDialog()
|
||||
const project = useProject()
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const [creating, setCreating] = createSignal<string>()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
})
|
||||
|
||||
const options = createMemo(() => {
|
||||
const type = creating()
|
||||
if (type) {
|
||||
|
|
@ -142,21 +288,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||
},
|
||||
]
|
||||
}
|
||||
const list = adaptors()
|
||||
if (!list) {
|
||||
return [
|
||||
{
|
||||
title: "Loading workspaces...",
|
||||
value: "loading" as const,
|
||||
description: "Fetching available workspace adaptors",
|
||||
},
|
||||
]
|
||||
}
|
||||
return list.map((item) => ({
|
||||
title: item.name,
|
||||
value: item.type,
|
||||
description: item.description,
|
||||
}))
|
||||
return []
|
||||
})
|
||||
|
||||
const create = async (type: string) => {
|
||||
|
|
@ -186,14 +318,12 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||
setCreating(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title={creating() ? "Creating Workspace" : "New Workspace"}
|
||||
skipFilter={true}
|
||||
options={options()}
|
||||
onSelect={(option) => {
|
||||
if (option.value === "creating" || option.value === "loading") return
|
||||
void create(option.value)
|
||||
return creating() ? (
|
||||
<DialogSelect title="Creating Workspace" skipFilter={true} renderFilter={false} options={options()} />
|
||||
) : (
|
||||
<DialogWorkspaceTypeSelect
|
||||
onSelect={(adaptor) => {
|
||||
void create(adaptor.type)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Filesystem } from "@/util/filesystem"
|
|||
import { useLocal } from "@tui/context/local"
|
||||
import { tint, useTheme } from "@tui/context/theme"
|
||||
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
||||
import { Spinner } from "@tui/component/spinner"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useProject } from "@tui/context/project"
|
||||
|
|
@ -40,13 +41,23 @@ import { useKV } from "../../context/kv"
|
|||
import { createFadeIn } from "../../util/signal"
|
||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create"
|
||||
import {
|
||||
DialogWorkspaceCreate,
|
||||
DialogWorkspaceSelect,
|
||||
restoreWorkspaceSession,
|
||||
type WorkspaceSelection,
|
||||
} from "../dialog-workspace-create"
|
||||
import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
|
||||
import { useArgs } from "@tui/context/args"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { WorkspaceLabel, type WorkspaceStatus } from "../workspace-label"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
workspaceID?: string
|
||||
workspaceSelection?: WorkspaceSelection
|
||||
onWorkspaceSelectionChange?: (selection: WorkspaceSelection | undefined) => void
|
||||
onWorkspaceCreatingChange?: (creating: boolean) => void
|
||||
visible?: boolean
|
||||
disabled?: boolean
|
||||
onSubmit?: () => void
|
||||
|
|
@ -149,8 +160,31 @@ export function Prompt(props: PromptProps) {
|
|||
})
|
||||
let lastSubmittedEditorSelectionKey: string | undefined
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const [workspaceSelection, setWorkspaceSelection] = createSignal<WorkspaceSelection>()
|
||||
const [workspaceCreating, setWorkspaceCreating] = createSignal(false)
|
||||
const [workspaceCreatingDots, setWorkspaceCreatingDots] = createSignal(3)
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
const selectedWorkspace = () => props.workspaceSelection ?? workspaceSelection()
|
||||
|
||||
function selectWorkspace(selection: WorkspaceSelection | undefined) {
|
||||
setWorkspaceSelection(selection)
|
||||
props.onWorkspaceSelectionChange?.(selection)
|
||||
}
|
||||
|
||||
function setCreatingWorkspace(creating: boolean) {
|
||||
setWorkspaceCreating(creating)
|
||||
props.onWorkspaceCreatingChange?.(creating)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!workspaceCreating()) {
|
||||
setWorkspaceCreatingDots(3)
|
||||
return
|
||||
}
|
||||
const timer = setInterval(() => setWorkspaceCreatingDots((dots) => (dots % 3) + 1), 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
function promptModelWarning() {
|
||||
toast.show({
|
||||
|
|
@ -184,8 +218,9 @@ export function Prompt(props: PromptProps) {
|
|||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (props.disabled) input.cursorColor = theme.backgroundElement
|
||||
if (!props.disabled) input.cursorColor = theme.text
|
||||
if (!input || input.isDestroyed) return
|
||||
if (props.disabled || workspaceCreating()) input.cursorColor = theme.backgroundElement
|
||||
if (!props.disabled && !workspaceCreating()) input.cursorColor = theme.text
|
||||
})
|
||||
|
||||
const lastUserMessage = createMemo(() => {
|
||||
|
|
@ -450,6 +485,27 @@ export function Prompt(props: PromptProps) {
|
|||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Warp",
|
||||
description: "Change the workspace for the session",
|
||||
value: "workspace.set",
|
||||
category: "Session",
|
||||
enabled: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||
slash: {
|
||||
name: "warp",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
dialog.replace(() => (
|
||||
<DialogWorkspaceSelect
|
||||
current={selectedWorkspace()}
|
||||
onSelect={(selection) => {
|
||||
selectWorkspace(selection)
|
||||
dialog.clear()
|
||||
}}
|
||||
/>
|
||||
))
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
|
@ -507,7 +563,7 @@ export function Prompt(props: PromptProps) {
|
|||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
if (props.visible === false || dialog.stack.length > 0) {
|
||||
if (props.visible === false || dialog.stack.length > 0 || workspaceCreating()) {
|
||||
if (input.focused) input.blur()
|
||||
return
|
||||
}
|
||||
|
|
@ -527,13 +583,13 @@ export function Prompt(props: PromptProps) {
|
|||
: undefined
|
||||
input.traits = {
|
||||
capture,
|
||||
suspend: !!props.disabled || store.mode === "shell",
|
||||
suspend: !!props.disabled || workspaceCreating() || store.mode === "shell",
|
||||
status: store.mode === "shell" ? "SHELL" : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
||||
input.extmarks.clear()
|
||||
if (!input.isDestroyed) input.extmarks.clear()
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
|
||||
parts.forEach((part, partIndex) => {
|
||||
|
|
@ -673,7 +729,7 @@ export function Prompt(props: PromptProps) {
|
|||
setStore("prompt", "input", input.plainText)
|
||||
syncExtmarksWithPromptParts()
|
||||
}
|
||||
if (props.disabled) return false
|
||||
if (props.disabled || workspaceCreating()) return false
|
||||
if (autocomplete?.visible) return false
|
||||
if (!store.prompt.input) return false
|
||||
const agent = local.agent.current()
|
||||
|
|
@ -717,24 +773,6 @@ export function Prompt(props: PromptProps) {
|
|||
return false
|
||||
}
|
||||
|
||||
let sessionID = props.sessionID
|
||||
if (sessionID == null) {
|
||||
const res = await sdk.client.session.create({ workspace: props.workspaceID })
|
||||
|
||||
if (res.error) {
|
||||
console.log("Creating a session failed:", res.error)
|
||||
|
||||
toast.show({
|
||||
message: "Creating a session failed. Open console for more details.",
|
||||
variant: "error",
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
sessionID = res.data.id
|
||||
}
|
||||
|
||||
const messageID = MessageID.ascending()
|
||||
let inputText = store.prompt.input
|
||||
|
||||
|
|
@ -759,6 +797,7 @@ export function Prompt(props: PromptProps) {
|
|||
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const submittedPrompt = unwrap(store.prompt)
|
||||
const variant = local.model.variant.current()
|
||||
const editorSelection = fileContextEnabled() ? editor.selection() : undefined
|
||||
const editorSelectionKey = editorSelection ? getEditorSelectionKey(editorSelection) : undefined
|
||||
|
|
@ -794,6 +833,53 @@ export function Prompt(props: PromptProps) {
|
|||
]
|
||||
: []
|
||||
|
||||
let sessionID = props.sessionID
|
||||
if (sessionID == null) {
|
||||
const workspace = selectedWorkspace()
|
||||
if (workspace?.type === "new") {
|
||||
setCreatingWorkspace(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 10_000))
|
||||
}
|
||||
const workspaceID = await iife(async () => {
|
||||
if (!workspace) return undefined
|
||||
if (workspace.type === "none") return undefined
|
||||
if (workspace.type === "existing") return workspace.workspaceID
|
||||
|
||||
const result = await sdk.client.experimental.workspace
|
||||
.create({ type: workspace.workspaceType, branch: null })
|
||||
.catch(() => undefined)
|
||||
if (result == undefined || result.error || !result.data) return undefined
|
||||
|
||||
await project.workspace.sync()
|
||||
return result.data.id
|
||||
})
|
||||
|
||||
if (workspace?.type === "new" && !workspaceID) {
|
||||
setCreatingWorkspace(false)
|
||||
toast.show({
|
||||
message: "Creating workspace failed",
|
||||
variant: "error",
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
const res = await sdk.client.session.create({ workspace: workspaceID })
|
||||
|
||||
if (res.error) {
|
||||
setCreatingWorkspace(false)
|
||||
console.log("Creating a session failed:", res.error)
|
||||
|
||||
toast.show({
|
||||
message: "Creating a session failed. Open console for more details.",
|
||||
variant: "error",
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
sessionID = res.data.id
|
||||
}
|
||||
|
||||
if (store.mode === "shell") {
|
||||
void sdk.client.session.shell({
|
||||
sessionID,
|
||||
|
|
@ -858,7 +944,7 @@ export function Prompt(props: PromptProps) {
|
|||
lastSubmittedEditorSelectionKey = editorSelectionKey
|
||||
}
|
||||
history.append({
|
||||
...store.prompt,
|
||||
...submittedPrompt,
|
||||
mode: currentMode,
|
||||
})
|
||||
input.extmarks.clear()
|
||||
|
|
@ -877,7 +963,7 @@ export function Prompt(props: PromptProps) {
|
|||
sessionID,
|
||||
})
|
||||
}, 50)
|
||||
input.clear()
|
||||
if (!input.isDestroyed) input.clear()
|
||||
return true
|
||||
}
|
||||
const exit = useExit()
|
||||
|
|
@ -998,6 +1084,28 @@ export function Prompt(props: PromptProps) {
|
|||
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
||||
})
|
||||
|
||||
const workspaceLabel = createMemo<
|
||||
| { type: "new"; workspaceType: string }
|
||||
| { type: "existing"; workspaceType: string; workspaceName: string; status?: WorkspaceStatus }
|
||||
| undefined
|
||||
>(() => {
|
||||
const selected = selectedWorkspace()
|
||||
if (!selected) return
|
||||
if (selected.type === "none") return
|
||||
if (selected.type === "new") {
|
||||
return {
|
||||
type: "new",
|
||||
workspaceType: selected.workspaceType,
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "existing",
|
||||
workspaceType: selected.workspaceType,
|
||||
workspaceName: selected.workspaceName,
|
||||
status: selected.type === "existing" ? "connected" : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const spinnerDef = createMemo(() => {
|
||||
const agent = local.agent.current()
|
||||
const color = agent ? local.agent.color(agent.name) : theme.border
|
||||
|
|
@ -1076,7 +1184,7 @@ export function Prompt(props: PromptProps) {
|
|||
}}
|
||||
keyBindings={textareaKeybindings()}
|
||||
onKeyDown={async (e) => {
|
||||
if (props.disabled) {
|
||||
if (props.disabled || workspaceCreating()) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
|
@ -1159,7 +1267,7 @@ export function Prompt(props: PromptProps) {
|
|||
setTimeout(() => setTimeout(() => submit(), 0), 0)
|
||||
}}
|
||||
onPaste={async (event: PasteEvent) => {
|
||||
if (props.disabled) {
|
||||
if (props.disabled || workspaceCreating()) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
|
@ -1254,7 +1362,7 @@ export function Prompt(props: PromptProps) {
|
|||
}}
|
||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||
focusedBackgroundColor={theme.backgroundElement}
|
||||
cursorColor={theme.text}
|
||||
cursorColor={workspaceCreating() ? theme.backgroundElement : theme.text}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
|
||||
|
|
@ -1324,86 +1432,117 @@ export function Prompt(props: PromptProps) {
|
|||
/>
|
||||
</box>
|
||||
<box width="100%" flexDirection="row" justifyContent="space-between">
|
||||
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
flexGrow={1}
|
||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||
>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<box marginLeft={1}>
|
||||
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
<Switch>
|
||||
<Match when={workspaceLabel()}>
|
||||
{(workspace) => (
|
||||
<box paddingLeft={3} flexDirection="row" gap={1}>
|
||||
<Show when={workspaceCreating()}>
|
||||
<Spinner color={theme.accent} />
|
||||
</Show>
|
||||
</box>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
{(() => {
|
||||
const retry = createMemo(() => {
|
||||
const s = status()
|
||||
if (s.type !== "retry") return
|
||||
return s
|
||||
})
|
||||
const message = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
||||
return "gemini is way too hot right now"
|
||||
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
|
||||
return r.message
|
||||
})
|
||||
const isTruncated = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return false
|
||||
return r.message.length > 120
|
||||
})
|
||||
const [seconds, setSeconds] = createSignal(0)
|
||||
onMount(() => {
|
||||
const timer = setInterval(() => {
|
||||
const next = retry()?.next
|
||||
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
||||
}, 1000)
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
const handleMessageClick = () => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (isTruncated()) {
|
||||
void DialogAlert.show(dialog, "Retry Error", r.message)
|
||||
<text fg={workspaceCreating() ? theme.accent : theme.text}>
|
||||
{(() => {
|
||||
const item = workspace()
|
||||
if (item.type === "new") {
|
||||
if (workspaceCreating()) return `Creating ${item.workspaceType}${".".repeat(workspaceCreatingDots())}`
|
||||
return (
|
||||
<>
|
||||
Workspace <span style={{ fg: theme.textMuted }}>(new {item.workspaceType})</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const retryText = () => {
|
||||
const r = retry()
|
||||
if (!r) return ""
|
||||
const baseMessage = message()
|
||||
const truncatedHint = isTruncated() ? " (click to expand)" : ""
|
||||
const duration = formatDuration(seconds())
|
||||
const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
|
||||
return baseMessage + truncatedHint + retryInfo
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={retry()}>
|
||||
<box onMouseUp={handleMessageClick}>
|
||||
<text fg={theme.error}>{retryText()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
})()}
|
||||
return (
|
||||
<>
|
||||
Workspace <span style={{ fg: theme.textMuted }}>{item.workspaceName}</span>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
flexGrow={1}
|
||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||
>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<box marginLeft={1}>
|
||||
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
</Show>
|
||||
</box>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
{(() => {
|
||||
const retry = createMemo(() => {
|
||||
const s = status()
|
||||
if (s.type !== "retry") return
|
||||
return s
|
||||
})
|
||||
const message = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
||||
return "gemini is way too hot right now"
|
||||
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
|
||||
return r.message
|
||||
})
|
||||
const isTruncated = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return false
|
||||
return r.message.length > 120
|
||||
})
|
||||
const [seconds, setSeconds] = createSignal(0)
|
||||
onMount(() => {
|
||||
const timer = setInterval(() => {
|
||||
const next = retry()?.next
|
||||
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
||||
}, 1000)
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
const handleMessageClick = () => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (isTruncated()) {
|
||||
void DialogAlert.show(dialog, "Retry Error", r.message)
|
||||
}
|
||||
}
|
||||
|
||||
const retryText = () => {
|
||||
const r = retry()
|
||||
if (!r) return ""
|
||||
const baseMessage = message()
|
||||
const truncatedHint = isTruncated() ? " (click to expand)" : ""
|
||||
const duration = formatDuration(seconds())
|
||||
const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
|
||||
return baseMessage + truncatedHint + retryInfo
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={retry()}>
|
||||
<box onMouseUp={handleMessageClick}>
|
||||
<text fg={theme.error}>{retryText()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
})()}
|
||||
</box>
|
||||
</box>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={status().type !== "retry"}>
|
||||
<box gap={2} flexDirection="row">
|
||||
<Show when={editorFileLabelDisplay()}>{(file) => <text fg={theme.secondary}>{file()}</text>}</Show>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { useTheme } from "@tui/context/theme"
|
||||
|
||||
export type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
||||
|
||||
export function WorkspaceLabel(props: { type: string; name: string; status?: WorkspaceStatus; icon?: boolean }) {
|
||||
const { theme } = useTheme()
|
||||
const color = () => {
|
||||
if (props.status === "connected") return theme.success
|
||||
if (props.status === "error") return theme.error
|
||||
return theme.textMuted
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.icon ? <span style={{ fg: color() }}>● </span> : undefined}
|
||||
<span style={{ fg: theme.text }}>{props.name}</span> <span style={{ fg: theme.textMuted }}>({props.type})</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { useRouteData } from "@tui/context/route"
|
|||
import { usePromptRef } from "../context/prompt"
|
||||
import { useLocal } from "../context/local"
|
||||
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
||||
import type { WorkspaceSelection } from "../component/dialog-workspace-create"
|
||||
|
||||
let once = false
|
||||
const placeholder = {
|
||||
|
|
@ -22,10 +23,28 @@ export function Home() {
|
|||
const route = useRouteData("home")
|
||||
const promptRef = usePromptRef()
|
||||
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
||||
const [workspaceSelection, setWorkspaceSelection] = createSignal<WorkspaceSelection>()
|
||||
const args = useArgs()
|
||||
const local = useLocal()
|
||||
let sent = false
|
||||
|
||||
const currentWorkspaceSelection = (): WorkspaceSelection | undefined => {
|
||||
const workspaceID = project.workspace.current()
|
||||
if (!workspaceID) return { type: "none" }
|
||||
const workspace = project.workspace.get(workspaceID)
|
||||
return {
|
||||
type: "existing",
|
||||
workspaceID,
|
||||
workspaceType: workspace?.type ?? "unknown",
|
||||
workspaceName: workspace?.name ?? workspaceID,
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (workspaceSelection()) return
|
||||
setWorkspaceSelection(currentWorkspaceSelection())
|
||||
})
|
||||
|
||||
const bind = (r: PromptRef | undefined) => {
|
||||
setRef(r)
|
||||
promptRef.set(r)
|
||||
|
|
@ -73,6 +92,8 @@ export function Home() {
|
|||
<Prompt
|
||||
ref={bind}
|
||||
workspaceID={project.workspace.current()}
|
||||
workspaceSelection={workspaceSelection()}
|
||||
onWorkspaceSelectionChange={setWorkspaceSelection}
|
||||
right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={project.workspace.current()} />}
|
||||
placeholders={placeholder}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/inst
|
|||
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
||||
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
import { WorkspaceLabel } from "../../component/workspace-label"
|
||||
|
||||
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
const project = useProject()
|
||||
|
|
@ -14,17 +15,10 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
|||
const { theme } = useTheme()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const session = createMemo(() => sync.session.get(props.sessionID))
|
||||
const workspaceStatus = () => {
|
||||
const workspace = () => {
|
||||
const workspaceID = session()?.workspaceID
|
||||
if (!workspaceID) return "error"
|
||||
return project.workspace.status(workspaceID) ?? "error"
|
||||
}
|
||||
const workspaceLabel = () => {
|
||||
const workspaceID = session()?.workspaceID
|
||||
if (!workspaceID) return "unknown"
|
||||
const info = project.workspace.get(workspaceID)
|
||||
if (!info) return "unknown"
|
||||
return `${info.type}: ${info.name}`
|
||||
if (!workspaceID) return
|
||||
return project.workspace.get(workspaceID)
|
||||
}
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
||||
|
||||
|
|
@ -67,8 +61,19 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
|||
</Show>
|
||||
<Show when={session()!.workspaceID}>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: workspaceStatus() === "connected" ? theme.success : theme.error }}>●</span>{" "}
|
||||
{workspaceLabel()}
|
||||
<Show
|
||||
when={workspace()}
|
||||
fallback={<WorkspaceLabel type="unknown" name={session()!.workspaceID!} status="error" icon />}
|
||||
>
|
||||
{(item) => (
|
||||
<WorkspaceLabel
|
||||
type={item().type}
|
||||
name={item().name}
|
||||
status={project.workspace.status(item().id) ?? "error"}
|
||||
icon
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={session()!.share?.url}>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface DialogSelectProps<T> {
|
|||
onFilter?: (query: string) => void
|
||||
onSelect?: (option: DialogSelectOption<T>) => void
|
||||
skipFilter?: boolean
|
||||
renderFilter?: boolean
|
||||
keybind?: {
|
||||
keybind?: Keybind.Info
|
||||
title: string
|
||||
|
|
@ -35,6 +36,7 @@ export interface DialogSelectProps<T> {
|
|||
|
||||
export interface DialogSelectOption<T = any> {
|
||||
title: string
|
||||
titleView?: JSX.Element
|
||||
value: T
|
||||
description?: string
|
||||
footer?: JSX.Element | string
|
||||
|
|
@ -81,7 +83,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
let input: InputRenderable
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
if (props.skipFilter) return props.options.filter((x) => x.disabled !== true)
|
||||
if (props.skipFilter || props.renderFilter === false) return props.options.filter((x) => x.disabled !== true)
|
||||
const needle = store.filter.toLowerCase()
|
||||
const options = pipe(
|
||||
props.options,
|
||||
|
|
@ -250,30 +252,32 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
esc
|
||||
</text>
|
||||
</box>
|
||||
<box paddingTop={1}>
|
||||
<input
|
||||
onInput={(e) => {
|
||||
batch(() => {
|
||||
setStore("filter", e)
|
||||
props.onFilter?.(e)
|
||||
})
|
||||
}}
|
||||
focusedBackgroundColor={theme.backgroundPanel}
|
||||
cursorColor={theme.primary}
|
||||
focusedTextColor={theme.textMuted}
|
||||
ref={(r) => {
|
||||
input = r
|
||||
input.traits = { status: "FILTER" }
|
||||
setTimeout(() => {
|
||||
if (!input) return
|
||||
if (input.isDestroyed) return
|
||||
input.focus()
|
||||
}, 1)
|
||||
}}
|
||||
placeholder={props.placeholder ?? "Search"}
|
||||
placeholderColor={theme.textMuted}
|
||||
/>
|
||||
</box>
|
||||
<Show when={props.renderFilter !== false}>
|
||||
<box paddingTop={1}>
|
||||
<input
|
||||
onInput={(e) => {
|
||||
batch(() => {
|
||||
setStore("filter", e)
|
||||
props.onFilter?.(e)
|
||||
})
|
||||
}}
|
||||
focusedBackgroundColor={theme.backgroundPanel}
|
||||
cursorColor={theme.primary}
|
||||
focusedTextColor={theme.textMuted}
|
||||
ref={(r) => {
|
||||
input = r
|
||||
input.traits = { status: "FILTER" }
|
||||
setTimeout(() => {
|
||||
if (!input) return
|
||||
if (input.isDestroyed) return
|
||||
input.focus()
|
||||
}, 1)
|
||||
}}
|
||||
placeholder={props.placeholder ?? "Search"}
|
||||
placeholderColor={theme.textMuted}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
<Show
|
||||
when={grouped().length > 0}
|
||||
|
|
@ -347,6 +351,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
</Show>
|
||||
<Option
|
||||
title={option.title}
|
||||
titleView={option.titleView}
|
||||
footer={flatten() ? (option.category ?? option.footer) : option.footer}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
|
|
@ -403,6 +408,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
|
||||
function Option(props: {
|
||||
title: string
|
||||
titleView?: JSX.Element
|
||||
description?: string
|
||||
active?: boolean
|
||||
current?: boolean
|
||||
|
|
@ -433,7 +439,9 @@ function Option(props: {
|
|||
wrapMode="none"
|
||||
paddingLeft={3}
|
||||
>
|
||||
{Locale.truncate(props.title, 61)}
|
||||
<Show when={props.titleView} fallback={Locale.truncate(props.title, 61)}>
|
||||
{(titleView) => titleView()}
|
||||
</Show>
|
||||
<Show when={props.description}>
|
||||
<span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -518,6 +518,7 @@ export const layer = Layer.effect(
|
|||
const adaptor = getAdaptor(space.projectID, space.type)
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
||||
|
||||
// Update the local copy to point to the new workspace
|
||||
yield* Effect.sync(() =>
|
||||
SyncEvent.run(Session.Event.Updated, {
|
||||
sessionID: input.sessionID,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue