feat(tui): switch console orgs in app
This commit is contained in:
parent
f5b30cdf39
commit
e972353d59
7 changed files with 313 additions and 1 deletions
|
|
@ -442,6 +442,14 @@ export namespace Account {
|
||||||
return Option.getOrUndefined(await runPromise((service) => service.activeOrg()))
|
return Option.getOrUndefined(await runPromise((service) => service.activeOrg()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
|
||||||
|
return runPromise((service) => service.orgsByAccount())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
|
||||||
|
return runPromise((service) => service.use(accountID, Option.some(orgID)))
|
||||||
|
}
|
||||||
|
|
||||||
export async function token(accountID: AccountID): Promise<AccessToken | undefined> {
|
export async function token(accountID: AccountID): Promise<AccessToken | undefined> {
|
||||||
const t = await runPromise((service) => service.token(accountID))
|
const t = await runPromise((service) => service.token(accountID))
|
||||||
return Option.getOrUndefined(t)
|
return Option.getOrUndefined(t)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command
|
||||||
import { DialogAgent } from "@tui/component/dialog-agent"
|
import { DialogAgent } from "@tui/component/dialog-agent"
|
||||||
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
||||||
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
|
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
|
||||||
|
import { DialogConsoleOrg } from "@tui/component/dialog-console-org"
|
||||||
import { KeybindProvider, useKeybind } from "@tui/context/keybind"
|
import { KeybindProvider, useKeybind } from "@tui/context/keybind"
|
||||||
import { ThemeProvider, useTheme } from "@tui/context/theme"
|
import { ThemeProvider, useTheme } from "@tui/context/theme"
|
||||||
import { Home } from "@tui/routes/home"
|
import { Home } from "@tui/routes/home"
|
||||||
|
|
@ -629,6 +630,19 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||||
},
|
},
|
||||||
category: "Provider",
|
category: "Provider",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Switch org",
|
||||||
|
value: "console.org.switch",
|
||||||
|
suggested: Boolean(sync.data.console_state.activeOrgName),
|
||||||
|
slash: {
|
||||||
|
name: "org",
|
||||||
|
aliases: ["orgs", "switch-org"],
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.replace(() => <DialogConsoleOrg />)
|
||||||
|
},
|
||||||
|
category: "Provider",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "View status",
|
title: "View status",
|
||||||
keybind: "status_view",
|
keybind: "status_view",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { createResource, createMemo } from "solid-js"
|
||||||
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
|
import { useSDK } from "@tui/context/sdk"
|
||||||
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
|
import { useToast } from "@tui/ui/toast"
|
||||||
|
|
||||||
|
type OrgOption = {
|
||||||
|
accountID: string
|
||||||
|
accountEmail: string
|
||||||
|
accountUrl: string
|
||||||
|
orgID: string
|
||||||
|
orgName: string
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogConsoleOrg() {
|
||||||
|
const sdk = useSDK()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const [orgs] = createResource(async () => {
|
||||||
|
const result = await sdk.client.experimental.console.listOrgs({}, { throwOnError: true })
|
||||||
|
return result.data?.orgs ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const current = createMemo(() => orgs()?.find((item) => item.active))
|
||||||
|
|
||||||
|
const options = createMemo(() => {
|
||||||
|
const listed = orgs()
|
||||||
|
if (listed === undefined) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Loading orgs...",
|
||||||
|
value: "loading",
|
||||||
|
onSelect: () => {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listed.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "No orgs found",
|
||||||
|
value: "empty",
|
||||||
|
onSelect: () => {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return listed
|
||||||
|
.toSorted((a, b) => {
|
||||||
|
if (a.active !== b.active) return a.active ? -1 : 1
|
||||||
|
return a.orgName.localeCompare(b.orgName)
|
||||||
|
})
|
||||||
|
.map((item) => ({
|
||||||
|
title: item.orgName,
|
||||||
|
value: item,
|
||||||
|
description: `${item.accountEmail} · ${(() => {
|
||||||
|
try {
|
||||||
|
return new URL(item.accountUrl).host
|
||||||
|
} catch {
|
||||||
|
return item.accountUrl
|
||||||
|
}
|
||||||
|
})()}`,
|
||||||
|
onSelect: async () => {
|
||||||
|
if (item.active) {
|
||||||
|
dialog.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sdk.client.experimental.console.switchOrg(
|
||||||
|
{
|
||||||
|
accountID: item.accountID,
|
||||||
|
orgID: item.orgID,
|
||||||
|
},
|
||||||
|
{ throwOnError: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
await sdk.client.instance.dispose()
|
||||||
|
toast.show({
|
||||||
|
message: `Switched to ${item.orgName}`,
|
||||||
|
variant: "info",
|
||||||
|
})
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
return <DialogSelect<string | OrgOption> title="Switch org" options={options()} current={current()} />
|
||||||
|
}
|
||||||
|
|
@ -1118,7 +1118,9 @@ export function Prompt(props: PromptProps) {
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
{props.right}
|
{props.right}
|
||||||
<Show when={activeOrgName()}>
|
<Show when={activeOrgName()}>
|
||||||
<text fg={theme.textMuted}>{`${CONSOLE_MANAGED_ICON} ${activeOrgName()}`}</text>
|
<text fg={theme.textMuted} onMouseUp={() => command.trigger("console.org.switch")}>
|
||||||
|
{`${CONSOLE_MANAGED_ICON} ${activeOrgName()}`}
|
||||||
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,30 @@ import { MCP } from "../../mcp"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import { Config } from "../../config/config"
|
import { Config } from "../../config/config"
|
||||||
import { ConsoleState } from "../../config/console-state"
|
import { ConsoleState } from "../../config/console-state"
|
||||||
|
import { Account, AccountID, OrgID } from "../../account"
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { WorkspaceRoutes } from "./workspace"
|
import { WorkspaceRoutes } from "./workspace"
|
||||||
|
|
||||||
|
const ConsoleOrgOption = z.object({
|
||||||
|
accountID: z.string(),
|
||||||
|
accountEmail: z.string(),
|
||||||
|
accountUrl: z.string(),
|
||||||
|
orgID: z.string(),
|
||||||
|
orgName: z.string(),
|
||||||
|
active: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ConsoleOrgList = z.object({
|
||||||
|
orgs: z.array(ConsoleOrgOption),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ConsoleSwitchBody = z.object({
|
||||||
|
accountID: z.string(),
|
||||||
|
orgID: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
export const ExperimentalRoutes = lazy(() =>
|
export const ExperimentalRoutes = lazy(() =>
|
||||||
new Hono()
|
new Hono()
|
||||||
.get(
|
.get(
|
||||||
|
|
@ -38,6 +57,62 @@ export const ExperimentalRoutes = lazy(() =>
|
||||||
return c.json(await Config.getConsoleState())
|
return c.json(await Config.getConsoleState())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/console/orgs",
|
||||||
|
describeRoute({
|
||||||
|
summary: "List switchable Console orgs",
|
||||||
|
description: "Get the available Console orgs across logged-in accounts, including the current active org.",
|
||||||
|
operationId: "experimental.console.listOrgs",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Switchable Console orgs",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(ConsoleOrgList),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()])
|
||||||
|
const orgs = groups.flatMap((group) =>
|
||||||
|
group.orgs.map((org) => ({
|
||||||
|
accountID: group.account.id,
|
||||||
|
accountEmail: group.account.email,
|
||||||
|
accountUrl: group.account.url,
|
||||||
|
orgID: org.id,
|
||||||
|
orgName: org.name,
|
||||||
|
active: !!active && active.id === group.account.id && active.active_org_id === org.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
return c.json({ orgs })
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/console/switch",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Switch active Console org",
|
||||||
|
description: "Persist a new active Console account/org selection for the current local OpenCode state.",
|
||||||
|
operationId: "experimental.console.switchOrg",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Switch success",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator("json", ConsoleSwitchBody),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID))
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/tool/ids",
|
"/tool/ids",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import type {
|
||||||
EventTuiSessionSelect,
|
EventTuiSessionSelect,
|
||||||
EventTuiToastShow,
|
EventTuiToastShow,
|
||||||
ExperimentalConsoleGetResponses,
|
ExperimentalConsoleGetResponses,
|
||||||
|
ExperimentalConsoleListOrgsResponses,
|
||||||
|
ExperimentalConsoleSwitchOrgResponses,
|
||||||
ExperimentalResourceListResponses,
|
ExperimentalResourceListResponses,
|
||||||
ExperimentalSessionListResponses,
|
ExperimentalSessionListResponses,
|
||||||
ExperimentalWorkspaceCreateErrors,
|
ExperimentalWorkspaceCreateErrors,
|
||||||
|
|
@ -1012,6 +1014,75 @@ export class Console extends HeyApiClient {
|
||||||
...params,
|
...params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List switchable Console orgs
|
||||||
|
*
|
||||||
|
* Get the available Console orgs across logged-in accounts, including the current active org.
|
||||||
|
*/
|
||||||
|
public listOrgs<ThrowOnError extends boolean = false>(
|
||||||
|
parameters?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "query", key: "directory" },
|
||||||
|
{ in: "query", key: "workspace" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).get<ExperimentalConsoleListOrgsResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/experimental/console/orgs",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch active Console org
|
||||||
|
*
|
||||||
|
* Persist a new active Console account/org selection for the current local OpenCode state.
|
||||||
|
*/
|
||||||
|
public switchOrg<ThrowOnError extends boolean = false>(
|
||||||
|
parameters?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
accountID?: string
|
||||||
|
orgID?: string
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "query", key: "directory" },
|
||||||
|
{ in: "query", key: "workspace" },
|
||||||
|
{ in: "body", key: "accountID" },
|
||||||
|
{ in: "body", key: "orgID" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).post<ExperimentalConsoleSwitchOrgResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/experimental/console/switch",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options?.headers,
|
||||||
|
...params.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Workspace extends HeyApiClient {
|
export class Workspace extends HeyApiClient {
|
||||||
|
|
|
||||||
|
|
@ -2675,6 +2675,58 @@ export type ExperimentalConsoleGetResponses = {
|
||||||
|
|
||||||
export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses]
|
export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses]
|
||||||
|
|
||||||
|
export type ExperimentalConsoleListOrgsData = {
|
||||||
|
body?: never
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
}
|
||||||
|
url: "/experimental/console/orgs"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExperimentalConsoleListOrgsResponses = {
|
||||||
|
/**
|
||||||
|
* Switchable Console orgs
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
orgs: Array<{
|
||||||
|
accountID: string
|
||||||
|
accountEmail: string
|
||||||
|
accountUrl: string
|
||||||
|
orgID: string
|
||||||
|
orgName: string
|
||||||
|
active: boolean
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExperimentalConsoleListOrgsResponse =
|
||||||
|
ExperimentalConsoleListOrgsResponses[keyof ExperimentalConsoleListOrgsResponses]
|
||||||
|
|
||||||
|
export type ExperimentalConsoleSwitchOrgData = {
|
||||||
|
body?: {
|
||||||
|
accountID: string
|
||||||
|
orgID: string
|
||||||
|
}
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
}
|
||||||
|
url: "/experimental/console/switch"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExperimentalConsoleSwitchOrgResponses = {
|
||||||
|
/**
|
||||||
|
* Switch success
|
||||||
|
*/
|
||||||
|
200: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExperimentalConsoleSwitchOrgResponse =
|
||||||
|
ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses]
|
||||||
|
|
||||||
export type ToolIdsData = {
|
export type ToolIdsData = {
|
||||||
body?: never
|
body?: never
|
||||||
path?: never
|
path?: never
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue