feat(tui): switch console orgs in app
parent
f5b30cdf39
commit
e972353d59
|
|
@ -442,6 +442,14 @@ export namespace Account {
|
|||
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> {
|
||||
const t = await runPromise((service) => service.token(accountID))
|
||||
return Option.getOrUndefined(t)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command
|
|||
import { DialogAgent } from "@tui/component/dialog-agent"
|
||||
import { DialogSessionList } from "@tui/component/dialog-session-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 { ThemeProvider, useTheme } from "@tui/context/theme"
|
||||
import { Home } from "@tui/routes/home"
|
||||
|
|
@ -629,6 +630,19 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||
},
|
||||
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",
|
||||
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">
|
||||
{props.right}
|
||||
<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>
|
||||
</box>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -10,11 +10,30 @@ import { MCP } from "../../mcp"
|
|||
import { Session } from "../../session"
|
||||
import { Config } from "../../config/config"
|
||||
import { ConsoleState } from "../../config/console-state"
|
||||
import { Account, AccountID, OrgID } from "../../account"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
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(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
|
|
@ -38,6 +57,62 @@ export const ExperimentalRoutes = lazy(() =>
|
|||
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(
|
||||
"/tool/ids",
|
||||
describeRoute({
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import type {
|
|||
EventTuiSessionSelect,
|
||||
EventTuiToastShow,
|
||||
ExperimentalConsoleGetResponses,
|
||||
ExperimentalConsoleListOrgsResponses,
|
||||
ExperimentalConsoleSwitchOrgResponses,
|
||||
ExperimentalResourceListResponses,
|
||||
ExperimentalSessionListResponses,
|
||||
ExperimentalWorkspaceCreateErrors,
|
||||
|
|
@ -1012,6 +1014,75 @@ export class Console extends HeyApiClient {
|
|||
...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 {
|
||||
|
|
|
|||
|
|
@ -2675,6 +2675,58 @@ export type 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 = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
|
|
|||
Loading…
Reference in New Issue