refactor(tui): reduce legacy sync usage
This commit is contained in:
@@ -476,18 +476,16 @@ export interface IntegrationApi<E = never> {
|
|||||||
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||||
export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
||||||
export type Endpoint11_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
export type Endpoint11_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
||||||
export type ServerMcpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E>
|
export type McpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E>
|
||||||
|
|
||||||
type Endpoint11_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
type Endpoint11_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||||
export type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] }
|
export type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] }
|
||||||
export type Endpoint11_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
|
export type Endpoint11_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
|
||||||
export type ServerMcpResourceCatalogOperation<E = never> = (
|
export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_1Input) => Effect.Effect<Endpoint11_1Output, E>
|
||||||
input?: Endpoint11_1Input,
|
|
||||||
) => Effect.Effect<Endpoint11_1Output, E>
|
|
||||||
|
|
||||||
export interface ServerMcpApi<E = never> {
|
export interface McpApi<E = never> {
|
||||||
readonly list: ServerMcpListOperation<E>
|
readonly list: McpListOperation<E>
|
||||||
readonly resource: { readonly catalog: ServerMcpResourceCatalogOperation<E> }
|
readonly resource: { readonly catalog: McpResourceCatalogOperation<E> }
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoint12_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
type Endpoint12_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
@@ -955,7 +953,7 @@ export interface AppApi<E = never> {
|
|||||||
readonly generate: GenerateApi<E>
|
readonly generate: GenerateApi<E>
|
||||||
readonly provider: ProviderApi<E>
|
readonly provider: ProviderApi<E>
|
||||||
readonly integration: IntegrationApi<E>
|
readonly integration: IntegrationApi<E>
|
||||||
readonly "server.mcp": ServerMcpApi<E>
|
readonly mcp: McpApi<E>
|
||||||
readonly credential: CredentialApi<E>
|
readonly credential: CredentialApi<E>
|
||||||
readonly project: ProjectApi<E>
|
readonly project: ProjectApi<E>
|
||||||
readonly form: FormApi<E>
|
readonly form: FormApi<E>
|
||||||
|
|||||||
@@ -1134,7 +1134,7 @@ const adaptClient = (raw: RawClient) => ({
|
|||||||
generate: adaptGroup8(raw["server.generate"]),
|
generate: adaptGroup8(raw["server.generate"]),
|
||||||
provider: adaptGroup9(raw["server.provider"]),
|
provider: adaptGroup9(raw["server.provider"]),
|
||||||
integration: adaptGroup10(raw["server.integration"]),
|
integration: adaptGroup10(raw["server.integration"]),
|
||||||
"server.mcp": adaptGroup11(raw["server.mcp"]),
|
mcp: adaptGroup11(raw["server.mcp"]),
|
||||||
credential: adaptGroup12(raw["server.credential"]),
|
credential: adaptGroup12(raw["server.credential"]),
|
||||||
project: adaptGroup13(raw["server.project"]),
|
project: adaptGroup13(raw["server.project"]),
|
||||||
form: adaptGroup14(raw["server.form"]),
|
form: adaptGroup14(raw["server.form"]),
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ import type {
|
|||||||
IntegrationAttemptCompleteOutput,
|
IntegrationAttemptCompleteOutput,
|
||||||
IntegrationAttemptCancelInput,
|
IntegrationAttemptCancelInput,
|
||||||
IntegrationAttemptCancelOutput,
|
IntegrationAttemptCancelOutput,
|
||||||
ServerMcpListInput,
|
McpListInput,
|
||||||
ServerMcpListOutput,
|
McpListOutput,
|
||||||
ServerMcpResourceCatalogInput,
|
McpResourceCatalogInput,
|
||||||
ServerMcpResourceCatalogOutput,
|
McpResourceCatalogOutput,
|
||||||
CredentialUpdateInput,
|
CredentialUpdateInput,
|
||||||
CredentialUpdateOutput,
|
CredentialUpdateOutput,
|
||||||
CredentialRemoveInput,
|
CredentialRemoveInput,
|
||||||
@@ -941,9 +941,9 @@ export function make(options: ClientOptions) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"server.mcp": {
|
mcp: {
|
||||||
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
|
list: (input?: McpListInput, requestOptions?: RequestOptions) =>
|
||||||
request<ServerMcpListOutput>(
|
request<McpListOutput>(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: `/api/mcp`,
|
path: `/api/mcp`,
|
||||||
@@ -955,8 +955,8 @@ export function make(options: ClientOptions) {
|
|||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
resource: {
|
resource: {
|
||||||
catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
|
catalog: (input?: McpResourceCatalogInput, requestOptions?: RequestOptions) =>
|
||||||
request<ServerMcpResourceCatalogOutput>(
|
request<McpResourceCatalogOutput>(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: `/api/mcp/resource`,
|
path: `/api/mcp/resource`,
|
||||||
|
|||||||
@@ -3346,24 +3346,24 @@ export type IntegrationAttemptCancelInput = {
|
|||||||
|
|
||||||
export type IntegrationAttemptCancelOutput = void
|
export type IntegrationAttemptCancelOutput = void
|
||||||
|
|
||||||
export type ServerMcpListInput = {
|
export type McpListInput = {
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
}["location"]
|
}["location"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerMcpListOutput = {
|
export type McpListOutput = {
|
||||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
|
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
|
||||||
data: Array<McpServer>
|
data: Array<McpServer>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerMcpResourceCatalogInput = {
|
export type McpResourceCatalogInput = {
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
}["location"]
|
}["location"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerMcpResourceCatalogOutput = {
|
export type McpResourceCatalogOutput = {
|
||||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
|
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
|
||||||
data: McpResourceCatalog
|
data: McpResourceCatalog
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export const groupNames = {
|
|||||||
"server.event": "event",
|
"server.event": "event",
|
||||||
"server.pty": "pty",
|
"server.pty": "pty",
|
||||||
"server.shell": "shell",
|
"server.shell": "shell",
|
||||||
|
"server.mcp": "mcp",
|
||||||
"server.question": "question",
|
"server.question": "question",
|
||||||
"server.reference": "reference",
|
"server.reference": "reference",
|
||||||
"server.project": "project",
|
"server.project": "project",
|
||||||
|
|||||||
@@ -418,10 +418,10 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||||||
const keymap = useOpencodeKeymap()
|
const keymap = useOpencodeKeymap()
|
||||||
const event = useEvent()
|
const event = useEvent()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const sync = useSync()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const themeState = useTheme()
|
const themeState = useTheme()
|
||||||
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
||||||
const sync = useSync()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const project = useProject()
|
const project = useProject()
|
||||||
const exit = useExit()
|
const exit = useExit()
|
||||||
@@ -435,7 +435,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||||||
// the same problem on every refresh while still re-alerting if the state changes.
|
// the same problem on every refresh while still re-alerting if the state changes.
|
||||||
const mcpAlerted: Record<string, string> = {}
|
const mcpAlerted: Record<string, string> = {}
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
for (const server of data.location.mcp.list() ?? []) {
|
for (const server of data.location.mcp.server.list() ?? []) {
|
||||||
const status = server.status
|
const status = server.status
|
||||||
if (status.status !== "failed" && status.status !== "needs_auth") {
|
if (status.status !== "failed" && status.status !== "needs_auth") {
|
||||||
delete mcpAlerted[server.name]
|
delete mcpAlerted[server.name]
|
||||||
@@ -520,7 +520,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||||||
}
|
}
|
||||||
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
|
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
|
||||||
const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(
|
const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(
|
||||||
kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary),
|
kv.get("paste_summary_enabled", true),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update terminal window title based on current route and session
|
// Update terminal window title based on current route and session
|
||||||
@@ -574,7 +574,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||||||
|
|
||||||
let continued = false
|
let continued = false
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (continued || sync.status === "loading" || !args.continue) return
|
if (continued || !args.continue) return
|
||||||
continued = true
|
continued = true
|
||||||
const location = data.location.default()
|
const location = data.location.default()
|
||||||
void sdk.api.session
|
void sdk.api.session
|
||||||
@@ -600,12 +600,10 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||||||
.catch(toast.error)
|
.catch(toast.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle --session with --fork: wait for sync to be fully complete before forking
|
// Handle --session with --fork once.
|
||||||
// (session list loads in non-blocking phase for --session, so we must wait for "complete"
|
|
||||||
// to avoid a race where reconcile overwrites the newly forked session)
|
|
||||||
let forked = false
|
let forked = false
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
|
if (forked || !args.sessionID || !args.fork) return
|
||||||
forked = true
|
forked = true
|
||||||
void sdk.api.session
|
void sdk.api.session
|
||||||
.fork({ sessionID: args.sessionID })
|
.fork({ sessionID: args.sessionID })
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function DialogMcp() {
|
|||||||
|
|
||||||
const servers = createMemo(() =>
|
const servers = createMemo(() =>
|
||||||
pipe(
|
pipe(
|
||||||
data.location.mcp.list() ?? [],
|
data.location.mcp.server.list() ?? [],
|
||||||
sortBy(
|
sortBy(
|
||||||
(server) => statusMeta(server.status, theme).rank,
|
(server) => statusMeta(server.status, theme).rank,
|
||||||
(server) => server.name,
|
(server) => server.name,
|
||||||
|
|||||||
@@ -1,48 +1,17 @@
|
|||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { fileURLToPath } from "bun"
|
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useDialog } from "../ui/dialog"
|
import { useDialog } from "../ui/dialog"
|
||||||
import { useSync } from "../context/sync"
|
|
||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||||
|
|
||||||
export type DialogStatusProps = {}
|
export type DialogStatusProps = {}
|
||||||
|
|
||||||
export function DialogStatus() {
|
export function DialogStatus() {
|
||||||
const sync = useSync()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
const mcp = createMemo(() => data.location.mcp.list() ?? [])
|
const mcp = createMemo(() => data.location.mcp.server.list() ?? [])
|
||||||
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
|
|
||||||
|
|
||||||
const plugins = createMemo(() => {
|
|
||||||
const list = sync.data.config.plugin ?? []
|
|
||||||
const result = list.map((item) => {
|
|
||||||
const value = typeof item === "string" ? item : item[0]
|
|
||||||
if (value.startsWith("file://")) {
|
|
||||||
const path = fileURLToPath(value)
|
|
||||||
const parts = path.split("/")
|
|
||||||
const filename = parts.pop() || path
|
|
||||||
if (!filename.includes(".")) return { name: filename }
|
|
||||||
const basename = filename.split(".")[0]
|
|
||||||
if (basename === "index") {
|
|
||||||
const dirname = parts.pop()
|
|
||||||
const name = dirname || basename
|
|
||||||
return { name }
|
|
||||||
}
|
|
||||||
return { name: basename }
|
|
||||||
}
|
|
||||||
const index = value.lastIndexOf("@")
|
|
||||||
if (index <= 0) return { name: value, version: "latest" }
|
|
||||||
const name = value.substring(0, index)
|
|
||||||
const version = value.substring(index + 1)
|
|
||||||
return { name, version }
|
|
||||||
})
|
|
||||||
return result.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
@@ -94,76 +63,6 @@ export function DialogStatus() {
|
|||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
{sync.data.lsp.length > 0 && (
|
|
||||||
<box>
|
|
||||||
<text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
|
|
||||||
<For each={sync.data.lsp}>
|
|
||||||
{(item) => (
|
|
||||||
<box flexDirection="row" gap={1}>
|
|
||||||
<text
|
|
||||||
flexShrink={0}
|
|
||||||
style={{
|
|
||||||
fg: {
|
|
||||||
connected: theme.success,
|
|
||||||
error: theme.error,
|
|
||||||
}[item.status],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
•
|
|
||||||
</text>
|
|
||||||
<text fg={theme.text} wrapMode="word">
|
|
||||||
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
<Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
|
|
||||||
<box>
|
|
||||||
<text fg={theme.text}>{enabledFormatters().length} Formatters</text>
|
|
||||||
<For each={enabledFormatters()}>
|
|
||||||
{(item) => (
|
|
||||||
<box flexDirection="row" gap={1}>
|
|
||||||
<text
|
|
||||||
flexShrink={0}
|
|
||||||
style={{
|
|
||||||
fg: theme.success,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
•
|
|
||||||
</text>
|
|
||||||
<text wrapMode="word" fg={theme.text}>
|
|
||||||
<b>{item.name}</b>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
<Show when={plugins().length > 0} fallback={<text fg={theme.text}>No Plugins</text>}>
|
|
||||||
<box>
|
|
||||||
<text fg={theme.text}>{plugins().length} Plugins</text>
|
|
||||||
<For each={plugins()}>
|
|
||||||
{(item) => (
|
|
||||||
<box flexDirection="row" gap={1}>
|
|
||||||
<text
|
|
||||||
flexShrink={0}
|
|
||||||
style={{
|
|
||||||
fg: theme.success,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
•
|
|
||||||
</text>
|
|
||||||
<text wrapMode="word" fg={theme.text}>
|
|
||||||
<b>{item.name}</b>
|
|
||||||
{item.version && <span style={{ fg: theme.textMuted }}> @{item.version}</span>}
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { createStore } from "solid-js/store"
|
|||||||
import { useEditorContext } from "../../context/editor"
|
import { useEditorContext } from "../../context/editor"
|
||||||
import { useProject } from "../../context/project"
|
import { useProject } from "../../context/project"
|
||||||
import { useSDK } from "../../context/sdk"
|
import { useSDK } from "../../context/sdk"
|
||||||
import { useSync } from "../../context/sync"
|
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
import { useTuiPaths } from "../../context/runtime"
|
import { useTuiPaths } from "../../context/runtime"
|
||||||
@@ -86,7 +85,6 @@ export function Autocomplete(props: {
|
|||||||
}) {
|
}) {
|
||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const project = useProject()
|
const project = useProject()
|
||||||
const slashes = useCommandSlashes()
|
const slashes = useCommandSlashes()
|
||||||
@@ -285,7 +283,7 @@ export function Autocomplete(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function normalizeMentionPath(filePath: string) {
|
function normalizeMentionPath(filePath: string) {
|
||||||
const baseDir = location()?.directory || sync.path.directory || paths.cwd
|
const baseDir = location()?.directory || project.instance.directory() || paths.cwd
|
||||||
const absolute = path.resolve(filePath)
|
const absolute = path.resolve(filePath)
|
||||||
const relative = path.relative(baseDir, absolute)
|
const relative = path.relative(baseDir, absolute)
|
||||||
|
|
||||||
@@ -363,7 +361,7 @@ export function Autocomplete(props: {
|
|||||||
const options: AutocompleteOption[] = []
|
const options: AutocompleteOption[] = []
|
||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
|
|
||||||
for (const res of Object.values(sync.data.mcp_resource)) {
|
for (const res of data.location.mcp.resource.list(location()) ?? []) {
|
||||||
options.push({
|
options.push({
|
||||||
display: Locale.truncateMiddle(res.name, width),
|
display: Locale.truncateMiddle(res.name, width),
|
||||||
// Match the name only; matching the URI caused unrelated fuzzy hits.
|
// Match the name only; matching the URI caused unrelated fuzzy hits.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { Spinner } from "../spinner"
|
|||||||
import { useSDK } from "../../context/sdk"
|
import { useSDK } from "../../context/sdk"
|
||||||
import { useRoute } from "../../context/route"
|
import { useRoute } from "../../context/route"
|
||||||
import { useProject } from "../../context/project"
|
import { useProject } from "../../context/project"
|
||||||
import { useSync } from "../../context/sync"
|
|
||||||
import { useEvent } from "../../context/event"
|
import { useEvent } from "../../context/event"
|
||||||
import { editorSelectionKey, useEditorContext, type EditorSelection } from "../../context/editor"
|
import { editorSelectionKey, useEditorContext, type EditorSelection } from "../../context/editor"
|
||||||
import { normalizePromptContent, openEditor } from "../../editor"
|
import { normalizePromptContent, openEditor } from "../../editor"
|
||||||
@@ -153,7 +152,6 @@ export function Prompt(props: PromptProps) {
|
|||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const project = useProject()
|
const project = useProject()
|
||||||
const sync = useSync()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const currentLocation = useLocation()
|
const currentLocation = useLocation()
|
||||||
const tuiConfig = useTuiConfig()
|
const tuiConfig = useTuiConfig()
|
||||||
@@ -1193,7 +1191,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
||||||
if (
|
if (
|
||||||
(lineCount >= 3 || pastedContent.length > 150) &&
|
(lineCount >= 3 || pastedContent.length > 150) &&
|
||||||
kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary)
|
kv.get("paste_summary_enabled", true)
|
||||||
) {
|
) {
|
||||||
pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
|
pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
FormInfo,
|
FormInfo,
|
||||||
IntegrationInfo,
|
IntegrationInfo,
|
||||||
LocationRef,
|
LocationRef,
|
||||||
|
McpResource,
|
||||||
McpServer,
|
McpServer,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
PermissionSavedInfo,
|
PermissionSavedInfo,
|
||||||
@@ -43,7 +44,10 @@ type LocationData = {
|
|||||||
agent?: AgentInfo[]
|
agent?: AgentInfo[]
|
||||||
command?: CommandInfo[]
|
command?: CommandInfo[]
|
||||||
integration?: IntegrationInfo[]
|
integration?: IntegrationInfo[]
|
||||||
mcp?: McpServer[]
|
mcp?: {
|
||||||
|
server?: McpServer[]
|
||||||
|
resource?: McpResource[]
|
||||||
|
}
|
||||||
model?: ModelInfo[]
|
model?: ModelInfo[]
|
||||||
provider?: ProviderV2Info[]
|
provider?: ProviderV2Info[]
|
||||||
reference?: ReferenceInfo[]
|
reference?: ReferenceInfo[]
|
||||||
@@ -801,7 +805,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
// so the mcp list refreshes here rather than off integration.updated.
|
// so the mcp list refreshes here rather than off integration.updated.
|
||||||
case "mcp.status.changed":
|
case "mcp.status.changed":
|
||||||
if (bootstrapping) break
|
if (bootstrapping) break
|
||||||
void result.location.mcp.refresh(event.location)
|
void result.location.mcp.server.refresh(event.location)
|
||||||
|
break
|
||||||
|
case "mcp.resources.changed":
|
||||||
|
void result.location.mcp.resource.refresh(event.location)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -987,13 +994,31 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
|
server: {
|
||||||
list(location?: LocationRef) {
|
list(location?: LocationRef) {
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.mcp
|
return store.location[locationKey(location ?? defaultLocation())]?.mcp?.server
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.api["server.mcp"].list({ location: locationQuery(ref) })
|
const result = await sdk.api.mcp.list({ location: locationQuery(ref) })
|
||||||
const key = locationKey(result.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], mcp: result.data })
|
setStore("location", key, {
|
||||||
|
...store.location[key],
|
||||||
|
mcp: { ...store.location[key]?.mcp, server: result.data },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resource: {
|
||||||
|
list(location?: LocationRef) {
|
||||||
|
return store.location[locationKey(location ?? defaultLocation())]?.mcp?.resource
|
||||||
|
},
|
||||||
|
async refresh(ref?: LocationRef) {
|
||||||
|
const result = await sdk.api.mcp.resource.catalog({ location: locationQuery(ref) })
|
||||||
|
const key = locationKey(result.location)
|
||||||
|
setStore("location", key, {
|
||||||
|
...store.location[key],
|
||||||
|
mcp: { ...store.location[key]?.mcp, resource: result.data.resources },
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
@@ -1089,7 +1114,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
result.location.refresh(),
|
result.location.refresh(),
|
||||||
result.location.agent.refresh(),
|
result.location.agent.refresh(),
|
||||||
result.location.integration.refresh(),
|
result.location.integration.refresh(),
|
||||||
result.location.mcp.refresh(),
|
result.location.mcp.server.refresh(),
|
||||||
|
result.location.mcp.resource.refresh(),
|
||||||
result.location.model.refresh(),
|
result.location.model.refresh(),
|
||||||
result.location.provider.refresh(),
|
result.location.provider.refresh(),
|
||||||
result.location.reference.refresh(),
|
result.location.reference.refresh(),
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
import { useProject } from "./project"
|
import { useProject } from "./project"
|
||||||
import { useSync } from "./sync"
|
|
||||||
import { abbreviateHome } from "../runtime"
|
import { abbreviateHome } from "../runtime"
|
||||||
import { useTuiPaths } from "./runtime"
|
import { useTuiPaths } from "./runtime"
|
||||||
|
|
||||||
export function useDirectory() {
|
export function useDirectory() {
|
||||||
const project = useProject()
|
const project = useProject()
|
||||||
const sync = useSync()
|
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
return createMemo(() => {
|
return createMemo(() => {
|
||||||
const directory = project.instance.path().directory || paths.cwd
|
const directory = project.instance.path().directory || paths.cwd
|
||||||
const result = abbreviateHome(directory, paths.home)
|
return abbreviateHome(directory, paths.home)
|
||||||
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
|
||||||
return result
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { useSync } from "./sync"
|
|
||||||
import { useEvent } from "./event"
|
import { useEvent } from "./event"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { useTuiPaths } from "./runtime"
|
import { useTuiPaths } from "./runtime"
|
||||||
@@ -52,7 +51,6 @@ export function recentModels(
|
|||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
init: () => {
|
init: () => {
|
||||||
const sync = useSync()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -210,16 +208,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sync.data.config.model) {
|
|
||||||
const { providerID, modelID } = parseModel(sync.data.config.model)
|
|
||||||
if (isModelValid({ providerID, modelID })) {
|
|
||||||
return {
|
|
||||||
providerID,
|
|
||||||
modelID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of modelStore.recent) {
|
for (const item of modelStore.recent) {
|
||||||
if (isModelValid(item)) {
|
if (isModelValid(item)) {
|
||||||
return item
|
return item
|
||||||
@@ -507,12 +495,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const mcp = {
|
const mcp = {
|
||||||
isEnabled(name: string) {
|
isEnabled(name: string) {
|
||||||
const status = sync.data.mcp[name]
|
return data.location.mcp.server.list()?.find((item) => item.name === name)?.status.status === "connected"
|
||||||
return status?.status === "connected"
|
|
||||||
},
|
},
|
||||||
async toggle(name: string) {
|
async toggle(name: string) {
|
||||||
const status = sync.data.mcp[name]
|
const status = data.location.mcp.server.list()?.find((item) => item.name === name)?.status.status
|
||||||
if (status?.status === "connected") {
|
if (status === "connected") {
|
||||||
// Disable: disconnect the MCP
|
// Disable: disconnect the MCP
|
||||||
await sdk.client.mcp.disconnect({ name })
|
await sdk.client.mcp.disconnect({ name })
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function mapOptionCb<Value>(cb?: (item: TuiDialogSelectOption<Value>) => void) {
|
|||||||
function stateApi(sync: ReturnType<typeof useSync>, data: ReturnType<typeof useData>): TuiPluginApi["state"] {
|
function stateApi(sync: ReturnType<typeof useSync>, data: ReturnType<typeof useData>): TuiPluginApi["state"] {
|
||||||
return {
|
return {
|
||||||
get ready() {
|
get ready() {
|
||||||
return sync.ready
|
return true
|
||||||
},
|
},
|
||||||
get config() {
|
get config() {
|
||||||
return sync.data.config
|
return sync.data.config
|
||||||
@@ -120,7 +120,7 @@ function stateApi(sync: ReturnType<typeof useSync>, data: ReturnType<typeof useD
|
|||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
count() {
|
count() {
|
||||||
return sync.data.session.length
|
return data.session.list().length
|
||||||
},
|
},
|
||||||
get(sessionID) {
|
get(sessionID) {
|
||||||
return sync.session.get(sessionID)
|
return sync.session.get(sessionID)
|
||||||
@@ -150,13 +150,19 @@ function stateApi(sync: ReturnType<typeof useSync>, data: ReturnType<typeof useD
|
|||||||
return sync.data.lsp.map((item) => ({ id: item.id, root: item.root, status: item.status }))
|
return sync.data.lsp.map((item) => ({ id: item.id, root: item.root, status: item.status }))
|
||||||
},
|
},
|
||||||
mcp() {
|
mcp() {
|
||||||
return Object.entries(sync.data.mcp)
|
return (data.location.mcp.server.list() ?? [])
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
.map(([name, item]) => ({
|
.flatMap((item) =>
|
||||||
name,
|
item.status.status === "pending"
|
||||||
status: item.status,
|
? []
|
||||||
error: item.status === "failed" ? item.error : undefined,
|
: [
|
||||||
}))
|
{
|
||||||
|
name: item.name,
|
||||||
|
status: item.status.status,
|
||||||
|
error: item.status.status === "failed" ? item.status.error : undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Prompt, type PromptRef } from "../component/prompt"
|
import { Prompt, type PromptRef } from "../component/prompt"
|
||||||
import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
|
||||||
import { Logo } from "../component/logo"
|
import { Logo } from "../component/logo"
|
||||||
import { useSync } from "../context/sync"
|
|
||||||
import { Toast } from "../ui/toast"
|
import { Toast } from "../ui/toast"
|
||||||
import { useArgs } from "../context/args"
|
import { useArgs } from "../context/args"
|
||||||
import { useRouteData } from "../context/route"
|
import { useRouteData } from "../context/route"
|
||||||
@@ -24,7 +23,6 @@ const placeholder = {
|
|||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const pluginRuntime = usePluginRuntime()
|
const pluginRuntime = usePluginRuntime()
|
||||||
const sync = useSync()
|
|
||||||
const route = useRouteData("home")
|
const route = useRouteData("home")
|
||||||
const promptRef = usePromptRef()
|
const promptRef = usePromptRef()
|
||||||
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
||||||
@@ -61,12 +59,12 @@ export function Home() {
|
|||||||
once = true
|
once = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for sync and model store to be ready before auto-submitting --prompt
|
// Wait for the model store to be ready before auto-submitting --prompt.
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const r = ref()
|
const r = ref()
|
||||||
if (sent) return
|
if (sent) return
|
||||||
if (!r) return
|
if (!r) return
|
||||||
if (!sync.ready || !local.model.ready) return
|
if (!local.model.ready) return
|
||||||
if (!args.prompt) return
|
if (!args.prompt) return
|
||||||
if (r.current.text !== args.prompt) return
|
if (r.current.text !== args.prompt) return
|
||||||
sent = true
|
sent = true
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
type ParentProps,
|
type ParentProps,
|
||||||
type Setter,
|
type Setter,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { useSync } from "../../context/sync"
|
|
||||||
import { useTuiPaths } from "../../context/runtime"
|
import { useTuiPaths } from "../../context/runtime"
|
||||||
|
import { useProject } from "../../context/project"
|
||||||
|
|
||||||
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new"; name: string }
|
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new"; name: string }
|
||||||
|
|
||||||
@@ -21,11 +21,11 @@ type Context = {
|
|||||||
const HomeSessionDestinationContext = createContext<Context>()
|
const HomeSessionDestinationContext = createContext<Context>()
|
||||||
|
|
||||||
export function HomeSessionDestinationProvider(props: ParentProps) {
|
export function HomeSessionDestinationProvider(props: ParentProps) {
|
||||||
const sync = useSync()
|
const project = useProject()
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
const [selected, setDestination] = createSignal<HomeSessionDestination>()
|
const [selected, setDestination] = createSignal<HomeSessionDestination>()
|
||||||
const destination = createMemo<HomeSessionDestination>(
|
const destination = createMemo<HomeSessionDestination>(
|
||||||
() => selected() ?? { type: "directory", directory: sync.path.directory || paths.cwd, subdirectory: false },
|
() => selected() ?? { type: "directory", directory: project.instance.directory() || paths.cwd, subdirectory: false },
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<HomeSessionDestinationContext.Provider
|
<HomeSessionDestinationContext.Provider
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
import { createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
||||||
import { useTheme } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
import { useSync } from "../../context/sync"
|
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
import { useDirectory } from "../../context/directory"
|
import { useDirectory } from "../../context/directory"
|
||||||
import { useConnected } from "../../component/use-connected"
|
import { useConnected } from "../../component/use-connected"
|
||||||
@@ -9,12 +8,14 @@ import { useRoute } from "../../context/route"
|
|||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const sync = useSync()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const mcp = createMemo(() => (data.location.mcp.list() ?? []).filter((x) => x.status.status === "connected").length)
|
const mcp = createMemo(
|
||||||
const mcpError = createMemo(() => (data.location.mcp.list() ?? []).some((x) => x.status.status === "failed"))
|
() => (data.location.mcp.server.list() ?? []).filter((x) => x.status.status === "connected").length,
|
||||||
const lsp = createMemo(() => Object.keys(sync.data.lsp))
|
)
|
||||||
|
const mcpError = createMemo(() =>
|
||||||
|
(data.location.mcp.server.list() ?? []).some((x) => x.status.status === "failed"),
|
||||||
|
)
|
||||||
const permissions = createMemo(() => {
|
const permissions = createMemo(() => {
|
||||||
if (route.data.type !== "session") return []
|
if (route.data.type !== "session") return []
|
||||||
return data.session.permission.list(route.data.sessionID) ?? []
|
return data.session.permission.list(route.data.sessionID) ?? []
|
||||||
@@ -68,9 +69,6 @@ export function Footer() {
|
|||||||
{permissions().length > 1 ? "s" : ""}
|
{permissions().length > 1 ? "s" : ""}
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<text fg={theme.text}>
|
|
||||||
<span style={{ fg: lsp().length > 0 ? theme.success : theme.textMuted }}>•</span> {lsp().length} LSP
|
|
||||||
</text>
|
|
||||||
<Show when={mcp()}>
|
<Show when={mcp()}>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@@ -1305,6 +1305,70 @@ test("refreshes integrations after integration updates", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("refreshes MCP resources after catalog updates", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
let requests = 0
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname !== "/api/mcp/resource") return
|
||||||
|
requests++
|
||||||
|
return json({
|
||||||
|
location: { directory, project: { id: "proj_test", directory } },
|
||||||
|
data: {
|
||||||
|
resources:
|
||||||
|
requests === 1
|
||||||
|
? []
|
||||||
|
: [{ server: "docs", name: "API reference", uri: "https://example.com/api", description: "API docs" }],
|
||||||
|
templates: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, events)
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
let ready!: () => void
|
||||||
|
const mounted = new Promise<void>((resolve) => {
|
||||||
|
ready = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
onMount(ready)
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mounted
|
||||||
|
await wait(() => data.location.mcp.resource.list() !== undefined)
|
||||||
|
expect(data.location.mcp.resource.list()).toEqual([])
|
||||||
|
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_mcp_resources",
|
||||||
|
created: 0,
|
||||||
|
type: "mcp.resources.changed",
|
||||||
|
data: { server: "docs" },
|
||||||
|
})
|
||||||
|
await wait(() => data.location.mcp.resource.list()?.length === 1)
|
||||||
|
expect(data.location.mcp.resource.list()?.[0]).toEqual({
|
||||||
|
server: "docs",
|
||||||
|
name: "API reference",
|
||||||
|
uri: "https://example.com/api",
|
||||||
|
description: "API docs",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("refreshes effective catalog data after catalog updates", async () => {
|
test("refreshes effective catalog data after catalog updates", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const requests = { model: 0, provider: 0 }
|
const requests = { model: 0, provider: 0 }
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
|||||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||||
if (url.pathname === "/api/mcp")
|
if (url.pathname === "/api/mcp")
|
||||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||||
|
if (url.pathname === "/api/mcp/resource")
|
||||||
|
return json({
|
||||||
|
location: { directory, project: { id: "proj_test", directory: worktree } },
|
||||||
|
data: { resources: [], templates: [] },
|
||||||
|
})
|
||||||
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
||||||
if (url.pathname === "/api/session/active") return json({ data: {} })
|
if (url.pathname === "/api/session/active") return json({ data: {} })
|
||||||
if (url.pathname === "/api/permission/request")
|
if (url.pathname === "/api/permission/request")
|
||||||
|
|||||||
Reference in New Issue
Block a user