fix: preserve full OpenCode provider cost shape in auth.loader
The normalizeProviderModelCosts function was only setting { input: 0, output: 0 }
which doesn't match the full OpenCode provider cost shape that includes cache fields.
This fix:
- Preserves existing cost fields (input, output, cache.read, cache.write) if valid
- Adds missing cache: { read: 0, write: 0 } structure when not present
- Validates provider and model objects before modification to avoid poisoning
invalid provider metadata
- Updates ProviderModel type to include optional cache field
Fixes provider cost normalization to maintain cache field compatibility.
This commit is contained in:
parent
6fea0178eb
commit
47d6fc7382
7 changed files with 429 additions and 1 deletions
240
denotational-design-tutorial.md
Normal file
240
denotational-design-tutorial.md
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
# Denotational Design: Part 1
|
||||||
|
|
||||||
|
## The Basic Idea
|
||||||
|
|
||||||
|
Denotational design means designing an abstraction by first asking:
|
||||||
|
|
||||||
|
> What does this thing mean?
|
||||||
|
|
||||||
|
Before choosing data structures, algorithms, callbacks, queues, caches, or runtime behavior, we give each core type a simple meaning. Then we define operations in terms of that meaning.
|
||||||
|
|
||||||
|
The implementation can be clever later. The meaning should be simple now.
|
||||||
|
|
||||||
|
## API vs Implementation vs Meaning
|
||||||
|
|
||||||
|
When designing a library, there are three related but different things:
|
||||||
|
|
||||||
|
| Layer | Question | Example |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| API | What can users call? | `map(signal, f)` |
|
||||||
|
| Implementation | How does it run? | subscriptions, graphs, caching |
|
||||||
|
| Denotation | What does it mean? | a value changing over time |
|
||||||
|
|
||||||
|
Most designs jump between API and implementation. Denotational design adds the missing middle: a precise meaning.
|
||||||
|
|
||||||
|
## A Tiny FRP Example
|
||||||
|
|
||||||
|
Functional Reactive Programming is a good example because it starts with a very simple denotation.
|
||||||
|
|
||||||
|
A time-varying value, often called a `Behavior`, can be understood as:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Behavior<A> = Time -> A
|
||||||
|
```
|
||||||
|
|
||||||
|
That says:
|
||||||
|
|
||||||
|
> A `Behavior<A>` means: give me a time, and I can tell you the `A` value at that time.
|
||||||
|
|
||||||
|
This does not mean the implementation literally stores an infinite function. It means this is the specification. The implementation may use events, subscriptions, incremental recomputation, dependency graphs, or caching.
|
||||||
|
|
||||||
|
## Operations Follow From Meaning
|
||||||
|
|
||||||
|
Once we know what `Behavior<A>` means, operations become easier to define.
|
||||||
|
|
||||||
|
For example, `map` transforms the value inside a behavior:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
map: (A -> B) -> Behavior<A> -> Behavior<B>
|
||||||
|
```
|
||||||
|
|
||||||
|
Its meaning is:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
map(f, behavior)(time) = f(behavior(time))
|
||||||
|
```
|
||||||
|
|
||||||
|
That is the whole specification.
|
||||||
|
|
||||||
|
Similarly, a constant behavior:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
constant: A -> Behavior<A>
|
||||||
|
```
|
||||||
|
|
||||||
|
means:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
constant(value)(time) = value
|
||||||
|
```
|
||||||
|
|
||||||
|
The definitions are simple because the denotation is simple.
|
||||||
|
|
||||||
|
## Why This Helps
|
||||||
|
|
||||||
|
Denotational design is useful because it separates essence from machinery.
|
||||||
|
|
||||||
|
It helps library users because the abstraction has a clear mental model.
|
||||||
|
|
||||||
|
It helps implementers because correctness has a target independent of implementation details.
|
||||||
|
|
||||||
|
It helps API design because awkward operations become easier to spot. If an operation has no clean meaning, it may be exposing implementation machinery instead of domain meaning.
|
||||||
|
|
||||||
|
## The Design Loop
|
||||||
|
|
||||||
|
A practical denotational design loop looks like this:
|
||||||
|
|
||||||
|
1. Name the core type.
|
||||||
|
2. Write down what values of that type mean.
|
||||||
|
3. Define each operation by how it transforms meanings.
|
||||||
|
4. Notice what laws naturally follow.
|
||||||
|
5. Choose an implementation that preserves the meaning.
|
||||||
|
|
||||||
|
The key discipline is to delay implementation concerns until after the meaning is clear.
|
||||||
|
|
||||||
|
## Denotation Is Not Implementation
|
||||||
|
|
||||||
|
A denotation can look like an implementation because it is concrete enough to write down:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Behavior<A> = Time -> A
|
||||||
|
```
|
||||||
|
|
||||||
|
But this equation is not saying that a real FRP system must store a function from every possible time to an `A` value.
|
||||||
|
|
||||||
|
It is saying that this is the model we use to understand a behavior.
|
||||||
|
|
||||||
|
The implementation might use callbacks, mutable cells, event queues, dependency graphs, caching, sampling, or incremental recomputation. Those choices are representation. The denotation is the meaning those representations must preserve.
|
||||||
|
|
||||||
|
So there are two different questions:
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
| --- | --- |
|
||||||
|
| What does a behavior mean? | A function from time to value |
|
||||||
|
| How do we run it efficiently? | Some concrete representation and algorithm |
|
||||||
|
|
||||||
|
A denotation should be simple and precise. An implementation should be executable and efficient. They do not have to be the same thing.
|
||||||
|
|
||||||
|
## Operations At Two Levels
|
||||||
|
|
||||||
|
Conal Elliott describes a useful pattern:
|
||||||
|
|
||||||
|
> The meaning of each method corresponds to the same method for the meaning.
|
||||||
|
|
||||||
|
This sentence is subtle because there are three things in play:
|
||||||
|
|
||||||
|
1. An abstract type, like `Behavior<A>`.
|
||||||
|
2. A meaning type, like `Time -> A`.
|
||||||
|
3. A meaning function that translates from the abstract type to the meaning type.
|
||||||
|
|
||||||
|
For behaviors, Conal often calls the meaning function `at`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
at: Behavior<A> -> (Time -> A)
|
||||||
|
```
|
||||||
|
|
||||||
|
Read this as:
|
||||||
|
|
||||||
|
> `at(behavior)` gives the meaning of `behavior`.
|
||||||
|
|
||||||
|
So `at` is just the specific FRP name for the more general idea:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
meaning: Abstract<A> -> Model<A>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can talk about operations.
|
||||||
|
|
||||||
|
There are two versions of "the same" operation.
|
||||||
|
|
||||||
|
One operation belongs to the abstract API:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
mapBehavior: (A -> B) -> Behavior<A> -> Behavior<B>
|
||||||
|
```
|
||||||
|
|
||||||
|
The other operation belongs to the semantic model:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
mapFunction: (A -> B) -> (Time -> A) -> (Time -> B)
|
||||||
|
```
|
||||||
|
|
||||||
|
They are not literally the same function. They live at different levels. But they are the same conceptual operation: mapping a pure function over a value inside some structure.
|
||||||
|
|
||||||
|
The homomorphism law says that translating meanings should not care which order we take these steps.
|
||||||
|
|
||||||
|
Path 1: operate first, then take the meaning.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
behavior
|
||||||
|
-> mapBehavior(f, behavior)
|
||||||
|
-> at(mapBehavior(f, behavior))
|
||||||
|
```
|
||||||
|
|
||||||
|
Path 2: take the meaning first, then operate on the meaning.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
behavior
|
||||||
|
-> at(behavior)
|
||||||
|
-> mapFunction(f, at(behavior))
|
||||||
|
```
|
||||||
|
|
||||||
|
The law says both paths produce the same meaning:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
at(mapBehavior(f, behavior))
|
||||||
|
=
|
||||||
|
mapFunction(f, at(behavior))
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the generic word `meaning`, the same law is:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
meaning(operationOnAbstraction(x))
|
||||||
|
=
|
||||||
|
operationOnMeaning(meaning(x))
|
||||||
|
```
|
||||||
|
|
||||||
|
In pointwise form:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
at(mapBehavior(f, behavior))(time)
|
||||||
|
=
|
||||||
|
f(at(behavior)(time))
|
||||||
|
```
|
||||||
|
|
||||||
|
So the operation on the abstraction must mean the corresponding operation on the model.
|
||||||
|
|
||||||
|
That is the homomorphism idea: the meaning function preserves structure. It translates the abstract operation into the corresponding model operation.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Same shape, different levels:
|
||||||
|
|
||||||
|
mapBehavior(f, behavior) // abstract level
|
||||||
|
|
||||||
|
mapFunction(f, at(behavior)) // meaning/model level
|
||||||
|
|
||||||
|
// Connected by the meaning function:
|
||||||
|
|
||||||
|
at(mapBehavior(f, behavior))
|
||||||
|
=
|
||||||
|
mapFunction(f, at(behavior))
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives us a correctness rule. If `Behavior` claims to support `map`, its `map` should behave like `map` on its denotation, which is a function of time.
|
||||||
|
|
||||||
|
## The Main Lesson
|
||||||
|
|
||||||
|
Denotational design does not ask, "How do we build this?" first.
|
||||||
|
|
||||||
|
It asks:
|
||||||
|
|
||||||
|
> What are we talking about?
|
||||||
|
|
||||||
|
For FRP, the answer begins with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Behavior<A> = Time -> A
|
||||||
|
```
|
||||||
|
|
||||||
|
That small equation gives the abstraction a center of gravity. Everything else can be designed around it.
|
||||||
|
|
@ -275,6 +275,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = createTuiApi({
|
const api = createTuiApi({
|
||||||
|
command,
|
||||||
tuiConfig,
|
tuiConfig,
|
||||||
dialog,
|
dialog,
|
||||||
keymap,
|
keymap,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
import type {
|
||||||
|
TuiCommand,
|
||||||
|
TuiDialogSelectOption,
|
||||||
|
TuiPluginApi,
|
||||||
|
TuiRouteDefinition,
|
||||||
|
TuiSlotProps,
|
||||||
|
} from "@opencode-ai/plugin/tui"
|
||||||
import type { useEvent } from "@tui/context/event"
|
import type { useEvent } from "@tui/context/event"
|
||||||
import type { useRoute } from "@tui/context/route"
|
import type { useRoute } from "@tui/context/route"
|
||||||
import type { useSDK } from "@tui/context/sdk"
|
import type { useSDK } from "@tui/context/sdk"
|
||||||
|
|
@ -17,6 +23,7 @@ import { Slot as HostSlot } from "./slots"
|
||||||
import type { useToast } from "../ui/toast"
|
import type { useToast } from "../ui/toast"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import * as Keymap from "../keymap"
|
import * as Keymap from "../keymap"
|
||||||
|
import type { useCommandPalette } from "../context/command-palette"
|
||||||
|
|
||||||
type RouteEntry = {
|
type RouteEntry = {
|
||||||
key: symbol
|
key: symbol
|
||||||
|
|
@ -26,6 +33,7 @@ type RouteEntry = {
|
||||||
export type RouteMap = Map<string, RouteEntry[]>
|
export type RouteMap = Map<string, RouteEntry[]>
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
|
command: ReturnType<typeof useCommandPalette>
|
||||||
tuiConfig: TuiConfig.Resolved
|
tuiConfig: TuiConfig.Resolved
|
||||||
dialog: ReturnType<typeof useDialog>
|
dialog: ReturnType<typeof useDialog>
|
||||||
keymap: ReturnType<typeof useOpencodeKeymap>
|
keymap: ReturnType<typeof useOpencodeKeymap>
|
||||||
|
|
@ -41,6 +49,54 @@ type Input = {
|
||||||
renderer: TuiPluginApi["renderer"]
|
renderer: TuiPluginApi["renderer"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let warnedLegacyCommand = false
|
||||||
|
|
||||||
|
function warnLegacyCommandApi() {
|
||||||
|
if (warnedLegacyCommand) return
|
||||||
|
warnedLegacyCommand = true
|
||||||
|
console.warn("[tui.plugin] api.command is deprecated; use api.keymap.registerLayer({ commands, bindings }) instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandBinding(command: TuiCommand, input: Input) {
|
||||||
|
if (!command.keybind) return []
|
||||||
|
return input.tuiConfig.keybinds.get(command.keybind).map((binding) => ({ ...binding, cmd: command.value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyCommandApi(input: Input): TuiPluginApi["command"] {
|
||||||
|
return {
|
||||||
|
register(cb) {
|
||||||
|
warnLegacyCommandApi()
|
||||||
|
const list = cb()
|
||||||
|
return input.keymap.registerLayer({
|
||||||
|
commands: list.map((command) => ({
|
||||||
|
name: command.value,
|
||||||
|
title: command.title,
|
||||||
|
desc: command.description,
|
||||||
|
category: command.category,
|
||||||
|
namespace: "palette",
|
||||||
|
suggested: command.suggested,
|
||||||
|
hidden: command.hidden,
|
||||||
|
enabled: command.enabled,
|
||||||
|
slashName: command.slash?.name,
|
||||||
|
slashAliases: command.slash?.aliases,
|
||||||
|
run() {
|
||||||
|
command.onSelect?.()
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
bindings: list.flatMap((command) => commandBinding(command, input)),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
trigger(value) {
|
||||||
|
warnLegacyCommandApi()
|
||||||
|
input.keymap.dispatchCommand(value)
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
warnLegacyCommandApi()
|
||||||
|
input.command.show()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
|
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
|
||||||
const key = Symbol()
|
const key = Symbol()
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
|
|
@ -209,6 +265,7 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keymap: input.keymap,
|
keymap: input.keymap,
|
||||||
|
command: legacyCommandApi(input),
|
||||||
route: {
|
route: {
|
||||||
register(list) {
|
register(list) {
|
||||||
return routeRegister(input.routes, list, input.bump)
|
return routeRegister(input.routes, list, input.bump)
|
||||||
|
|
|
||||||
|
|
@ -563,6 +563,18 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||||
|
|
||||||
const keymap = createScopedKeymap(api.keymap, scope)
|
const keymap = createScopedKeymap(api.keymap, scope)
|
||||||
|
|
||||||
|
const command: TuiPluginApi["command"] = {
|
||||||
|
register(cb) {
|
||||||
|
return scope.track(api.command.register(cb))
|
||||||
|
},
|
||||||
|
trigger(value) {
|
||||||
|
api.command.trigger(value)
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
api.command.show()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
let count = 0
|
let count = 0
|
||||||
|
|
||||||
const slots: TuiPluginApi["slots"] = {
|
const slots: TuiPluginApi["slots"] = {
|
||||||
|
|
@ -578,6 +590,7 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||||
app: api.app,
|
app: api.app,
|
||||||
keys: api.keys,
|
keys: api.keys,
|
||||||
keymap,
|
keymap,
|
||||||
|
command,
|
||||||
route,
|
route,
|
||||||
ui: api.ui,
|
ui: api.ui,
|
||||||
tuiConfig: api.tuiConfig,
|
tuiConfig: api.tuiConfig,
|
||||||
|
|
|
||||||
|
|
@ -776,6 +776,79 @@ test("auto-disposes plugin keymap layers", async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("supports legacy plugin command API", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
const file = path.join(dir, "legacy-command-plugin.ts")
|
||||||
|
const spec = pathToFileURL(file).href
|
||||||
|
const marker = path.join(dir, "legacy-command.txt")
|
||||||
|
|
||||||
|
await Bun.write(
|
||||||
|
file,
|
||||||
|
`export default {
|
||||||
|
id: "demo.command.legacy",
|
||||||
|
tui: async (api) => {
|
||||||
|
api.command.register(() => [{
|
||||||
|
title: "Legacy command",
|
||||||
|
value: "demo.command.legacy.run",
|
||||||
|
onSelect() {
|
||||||
|
Bun.write(${JSON.stringify(marker)}, "called")
|
||||||
|
},
|
||||||
|
}])
|
||||||
|
api.command.trigger("demo.command.legacy.run")
|
||||||
|
api.command.show()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return { spec, marker }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let add = 0
|
||||||
|
let drop = 0
|
||||||
|
let show = 0
|
||||||
|
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||||
|
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await TuiPluginRuntime.init({
|
||||||
|
api: createTuiPluginApi({
|
||||||
|
command: {
|
||||||
|
register(cb) {
|
||||||
|
add += 1
|
||||||
|
const list = cb()
|
||||||
|
return () => {
|
||||||
|
drop += list.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger(value) {
|
||||||
|
expect(value).toBe("demo.command.legacy.run")
|
||||||
|
Bun.write(tmp.extra.marker, "called")
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
show += 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
config: createTuiResolvedConfig({
|
||||||
|
plugin: [tmp.extra.spec],
|
||||||
|
plugin_origins: [{ spec: tmp.extra.spec, scope: "local", source: path.join(tmp.path, "tui.json") }],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(add).toBe(1)
|
||||||
|
expect(show).toBe(1)
|
||||||
|
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||||
|
} finally {
|
||||||
|
await TuiPluginRuntime.dispose()
|
||||||
|
expect(drop).toBe(1)
|
||||||
|
cwd.mockRestore()
|
||||||
|
wait.mockRestore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("plugin keymap proxy preserves real keymap receiver", async () => {
|
test("plugin keymap proxy preserves real keymap receiver", async () => {
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
init: async (dir) => {
|
init: async (dir) => {
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ function themeCurrent(): HostPluginApi["theme"]["current"] {
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
client?: HostPluginApi["client"] | (() => HostPluginApi["client"])
|
client?: HostPluginApi["client"] | (() => HostPluginApi["client"])
|
||||||
|
command?: Partial<HostPluginApi["command"]>
|
||||||
renderer?: HostPluginApi["renderer"]
|
renderer?: HostPluginApi["renderer"]
|
||||||
count?: Count
|
count?: Count
|
||||||
keymap?: HostPluginApi["keymap"]
|
keymap?: HostPluginApi["keymap"]
|
||||||
|
|
@ -131,6 +132,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
|
||||||
? () => opts.client as HostPluginApi["client"]
|
? () => opts.client as HostPluginApi["client"]
|
||||||
: fallback
|
: fallback
|
||||||
const client = () => read()
|
const client = () => read()
|
||||||
|
const commands: ReturnType<Parameters<HostPluginApi["command"]["register"]>[0]> = []
|
||||||
let depth = 0
|
let depth = 0
|
||||||
let size: "medium" | "large" | "xlarge" = "medium"
|
let size: "medium" | "large" | "xlarge" = "medium"
|
||||||
const has = opts.theme?.has ?? (() => false)
|
const has = opts.theme?.has ?? (() => false)
|
||||||
|
|
@ -187,6 +189,25 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
|
||||||
formatSequence: () => "",
|
formatSequence: () => "",
|
||||||
formatBindings: () => undefined,
|
formatBindings: () => undefined,
|
||||||
},
|
},
|
||||||
|
command: {
|
||||||
|
register(cb) {
|
||||||
|
if (opts.command?.register) return opts.command.register(cb)
|
||||||
|
const list = cb()
|
||||||
|
commands.push(...list)
|
||||||
|
if (count) count.command_add += 1
|
||||||
|
return () => {
|
||||||
|
if (count) count.command_drop += 1
|
||||||
|
commands.splice(0, commands.length, ...commands.filter((command) => !list.includes(command)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger(value) {
|
||||||
|
if (opts.command?.trigger) return opts.command.trigger(value)
|
||||||
|
commands.find((command) => command.value === value)?.onSelect?.()
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
opts.command?.show?.()
|
||||||
|
},
|
||||||
|
},
|
||||||
get client() {
|
get client() {
|
||||||
return client()
|
return client()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,23 @@ export type TuiRouteDefinition = {
|
||||||
render: (input: { params?: Record<string, unknown> }) => JSX.Element
|
render: (input: { params?: Record<string, unknown> }) => JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use api.keymap.registerLayer({ commands, bindings }) instead. */
|
||||||
|
export type TuiCommand = {
|
||||||
|
title: string
|
||||||
|
value: string
|
||||||
|
description?: string
|
||||||
|
category?: string
|
||||||
|
keybind?: string
|
||||||
|
suggested?: boolean
|
||||||
|
hidden?: boolean
|
||||||
|
enabled?: boolean
|
||||||
|
slash?: {
|
||||||
|
name: string
|
||||||
|
aliases?: string[]
|
||||||
|
}
|
||||||
|
onSelect?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export type TuiKeys = {
|
export type TuiKeys = {
|
||||||
formatSequence: (parts: readonly KeySequenceFormatPart[] | undefined) => string
|
formatSequence: (parts: readonly KeySequenceFormatPart[] | undefined) => string
|
||||||
formatBindings: (bindings: readonly SequenceBindingLike[] | undefined) => string | undefined
|
formatBindings: (bindings: readonly SequenceBindingLike[] | undefined) => string | undefined
|
||||||
|
|
@ -463,6 +480,12 @@ export type TuiPluginApi = {
|
||||||
app: TuiApp
|
app: TuiApp
|
||||||
keys: TuiKeys
|
keys: TuiKeys
|
||||||
keymap: TuiKeymap
|
keymap: TuiKeymap
|
||||||
|
/** @deprecated Use api.keymap.registerLayer({ commands, bindings }) instead. */
|
||||||
|
command: {
|
||||||
|
register: (cb: () => TuiCommand[]) => () => void
|
||||||
|
trigger: (value: string) => void
|
||||||
|
show: () => void
|
||||||
|
}
|
||||||
route: {
|
route: {
|
||||||
register: (routes: TuiRouteDefinition[]) => () => void
|
register: (routes: TuiRouteDefinition[]) => () => void
|
||||||
navigate: (name: string, params?: Record<string, unknown>) => void
|
navigate: (name: string, params?: Record<string, unknown>) => void
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue