feat(tui): minor UX improvements for workspaces (#23146)
This commit is contained in:
@@ -139,15 +139,10 @@ export function DialogSessionList() {
|
|||||||
{desc}{" "}
|
{desc}{" "}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fg:
|
fg: workspaceStatus === "connected" ? theme.success : theme.error,
|
||||||
workspaceStatus === "error"
|
|
||||||
? theme.error
|
|
||||||
: workspaceStatus === "disconnected"
|
|
||||||
? theme.textMuted
|
|
||||||
: theme.success,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
■
|
●
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -139,7 +139,16 @@ export async function restoreWorkspaceSession(input: {
|
|||||||
total: result.data.total,
|
total: result.data.total,
|
||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
|
input.project.workspace.set(input.workspaceID)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await input.sync.bootstrap({ fatal: false })
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
input.project.workspace.sync(),
|
||||||
|
input.sync.session.sync(input.sessionID),
|
||||||
|
]).catch((err) => {
|
||||||
log.error("session restore refresh failed", {
|
log.error("session restore refresh failed", {
|
||||||
workspaceID: input.workspaceID,
|
workspaceID: input.workspaceID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { TextAttributes } from "@opentui/core"
|
||||||
|
import { useKeyboard } from "@opentui/solid"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { For } from "solid-js"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { useDialog } from "../ui/dialog"
|
||||||
|
|
||||||
|
export function DialogWorkspaceUnavailable(props: {
|
||||||
|
onRestore?: () => boolean | void | Promise<boolean | void>
|
||||||
|
}) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
active: "restore" as "cancel" | "restore",
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = ["cancel", "restore"] as const
|
||||||
|
|
||||||
|
async function confirm() {
|
||||||
|
if (store.active === "cancel") {
|
||||||
|
dialog.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await props.onRestore?.()
|
||||||
|
if (result === false) return
|
||||||
|
}
|
||||||
|
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (evt.name === "return") {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
void confirm()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (evt.name === "left") {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
setStore("active", "cancel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (evt.name === "right") {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
setStore("active", "restore")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||||
|
Workspace Unavailable
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||||
|
esc
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
|
This session is attached to a workspace that is no longer available.
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
|
Would you like to restore this session into a new workspace?
|
||||||
|
</text>
|
||||||
|
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1} gap={1}>
|
||||||
|
<For each={options}>
|
||||||
|
{(item) => (
|
||||||
|
<box
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
backgroundColor={item === store.active ? theme.primary : undefined}
|
||||||
|
onMouseUp={() => {
|
||||||
|
setStore("active", item)
|
||||||
|
void confirm()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { tint, useTheme } from "@tui/context/theme"
|
|||||||
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
|
import { useProject } from "@tui/context/project"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { useEvent } from "@tui/context/event"
|
import { useEvent } from "@tui/context/event"
|
||||||
import { MessageID, PartID } from "@/session/schema"
|
import { MessageID, PartID } from "@/session/schema"
|
||||||
@@ -38,6 +39,8 @@ import { useKV } from "../../context/kv"
|
|||||||
import { createFadeIn } from "../../util/signal"
|
import { createFadeIn } from "../../util/signal"
|
||||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||||
import { DialogSkill } from "../dialog-skill"
|
import { DialogSkill } from "../dialog-skill"
|
||||||
|
import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create"
|
||||||
|
import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
|
||||||
import { useArgs } from "@tui/context/args"
|
import { useArgs } from "@tui/context/args"
|
||||||
|
|
||||||
export type PromptProps = {
|
export type PromptProps = {
|
||||||
@@ -92,6 +95,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const args = useArgs()
|
const args = useArgs()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const project = useProject()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -241,9 +245,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
keybind: "input_submit",
|
keybind: "input_submit",
|
||||||
category: "Prompt",
|
category: "Prompt",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
onSelect: (dialog) => {
|
onSelect: async (dialog) => {
|
||||||
if (!input.focused) return
|
if (!input.focused) return
|
||||||
void submit()
|
const handled = await submit()
|
||||||
|
if (!handled) return
|
||||||
|
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -628,20 +634,48 @@ export function Prompt(props: PromptProps) {
|
|||||||
setStore("prompt", "input", input.plainText)
|
setStore("prompt", "input", input.plainText)
|
||||||
syncExtmarksWithPromptParts()
|
syncExtmarksWithPromptParts()
|
||||||
}
|
}
|
||||||
if (props.disabled) return
|
if (props.disabled) return false
|
||||||
if (autocomplete?.visible) return
|
if (autocomplete?.visible) return false
|
||||||
if (!store.prompt.input) return
|
if (!store.prompt.input) return false
|
||||||
const agent = local.agent.current()
|
const agent = local.agent.current()
|
||||||
if (!agent) return
|
if (!agent) return false
|
||||||
const trimmed = store.prompt.input.trim()
|
const trimmed = store.prompt.input.trim()
|
||||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||||
void exit()
|
void exit()
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
const selectedModel = local.model.current()
|
const selectedModel = local.model.current()
|
||||||
if (!selectedModel) {
|
if (!selectedModel) {
|
||||||
void promptModelWarning()
|
void promptModelWarning()
|
||||||
return
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined
|
||||||
|
const workspaceID = workspaceSession?.workspaceID
|
||||||
|
const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined
|
||||||
|
if (props.sessionID && workspaceID && workspaceStatus !== "connected") {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogWorkspaceUnavailable
|
||||||
|
onRestore={() => {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogWorkspaceCreate
|
||||||
|
onSelect={(nextWorkspaceID) =>
|
||||||
|
restoreWorkspaceSession({
|
||||||
|
dialog,
|
||||||
|
sdk,
|
||||||
|
sync,
|
||||||
|
project,
|
||||||
|
toast,
|
||||||
|
workspaceID: nextWorkspaceID,
|
||||||
|
sessionID: props.sessionID!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let sessionID = props.sessionID
|
let sessionID = props.sessionID
|
||||||
@@ -656,7 +690,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID = res.data.id
|
sessionID = res.data.id
|
||||||
@@ -770,6 +804,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
})
|
})
|
||||||
}, 50)
|
}, 50)
|
||||||
input.clear()
|
input.clear()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
const exit = useExit()
|
const exit = useExit()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useProject } from "@tui/context/project"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { useTheme } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
@@ -8,10 +9,23 @@ import { TuiPluginRuntime } from "../../plugin"
|
|||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
|
|
||||||
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||||
|
const project = useProject()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const tuiConfig = useTuiConfig()
|
const tuiConfig = useTuiConfig()
|
||||||
const session = createMemo(() => sync.session.get(props.sessionID))
|
const session = createMemo(() => sync.session.get(props.sessionID))
|
||||||
|
const workspaceStatus = () => {
|
||||||
|
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}`
|
||||||
|
}
|
||||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,6 +62,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
|||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
<b>{session()!.title}</b>
|
<b>{session()!.title}</b>
|
||||||
</text>
|
</text>
|
||||||
|
<Show when={session()!.workspaceID}>
|
||||||
|
<text fg={theme.textMuted}>
|
||||||
|
<span style={{ fg: workspaceStatus() === "connected" ? theme.success : theme.error }}>●</span>{" "}
|
||||||
|
{workspaceLabel()}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
<Show when={session()!.share?.url}>
|
<Show when={session()!.share?.url}>
|
||||||
<text fg={theme.textMuted}>{session()!.share!.url}</text>
|
<text fg={theme.textMuted}>{session()!.share!.url}</text>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user