@agentprojectcontext/apx 1.15.5 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { RGBA, TextAttributes, type KeyEvent, type Renderable } from "@opentui/core"
|
|
3
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
4
|
+
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
|
|
5
|
+
import { useBindings, useKeymapSelector } from "../../keymap"
|
|
6
|
+
import type { ActiveKey } from "@opentui/keymap"
|
|
7
|
+
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
8
|
+
import type { InternalTuiPlugin } from "../../plugin/internal"
|
|
9
|
+
|
|
10
|
+
const command = {
|
|
11
|
+
toggle: "which-key.toggle",
|
|
12
|
+
toggleLayout: "which-key.layout.toggle",
|
|
13
|
+
togglePending: "which-key.pending.toggle",
|
|
14
|
+
groupPrevious: "which-key.group.previous",
|
|
15
|
+
groupNext: "which-key.group.next",
|
|
16
|
+
scrollUp: "which-key.scroll.up",
|
|
17
|
+
scrollDown: "which-key.scroll.down",
|
|
18
|
+
pageUp: "which-key.page.up",
|
|
19
|
+
pageDown: "which-key.page.down",
|
|
20
|
+
home: "which-key.home",
|
|
21
|
+
end: "which-key.end",
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
const LAYER_PRIORITY = 900
|
|
25
|
+
const KV_LAYOUT = "which_key_layout"
|
|
26
|
+
const KV_PENDING_PREVIEW = "which_key_pending_preview"
|
|
27
|
+
const toggleCommands = [command.toggle, command.toggleLayout, command.togglePending] as const
|
|
28
|
+
const scrollCommands = [
|
|
29
|
+
command.scrollUp,
|
|
30
|
+
command.scrollDown,
|
|
31
|
+
command.pageUp,
|
|
32
|
+
command.pageDown,
|
|
33
|
+
command.home,
|
|
34
|
+
command.end,
|
|
35
|
+
] as const
|
|
36
|
+
const panelCommands = [command.groupPrevious, command.groupNext, ...scrollCommands] as const
|
|
37
|
+
const COLUMN_GAP = 4
|
|
38
|
+
const TAB_GAP = 3
|
|
39
|
+
const MIN_TAB_GAP = 1
|
|
40
|
+
const TAB_CONTENT_GAP = 1
|
|
41
|
+
const MIN_COLUMN_WIDTH = 28
|
|
42
|
+
const MAX_COLUMN_WIDTH = 44
|
|
43
|
+
const PANEL_HEIGHT_RATIO = 0.3
|
|
44
|
+
const MIN_PANEL_HEIGHT = 8
|
|
45
|
+
const MAX_PANEL_HEIGHT = 16
|
|
46
|
+
const PANEL_TOP_PADDING = 1
|
|
47
|
+
const FOOTER_HEIGHT = 1
|
|
48
|
+
const FOOTER_MARGIN = 1
|
|
49
|
+
const UNKNOWN = "Unknown"
|
|
50
|
+
|
|
51
|
+
type Layout = "dock" | "overlay"
|
|
52
|
+
|
|
53
|
+
type Color = RGBA | string
|
|
54
|
+
|
|
55
|
+
type Skin = {
|
|
56
|
+
panel: Color
|
|
57
|
+
text: Color
|
|
58
|
+
muted: Color
|
|
59
|
+
subtle: Color
|
|
60
|
+
key: Color
|
|
61
|
+
accent: Color
|
|
62
|
+
tab: Color
|
|
63
|
+
tabText: Color
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Entry = {
|
|
67
|
+
type: "entry"
|
|
68
|
+
key: string
|
|
69
|
+
label: string
|
|
70
|
+
group: string
|
|
71
|
+
continues: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type Group = {
|
|
75
|
+
label: string
|
|
76
|
+
entries: Entry[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type HeaderItem = { type: "tab"; group: Group } | { type: "scroll" }
|
|
80
|
+
|
|
81
|
+
type GroupHeader = {
|
|
82
|
+
type: "group"
|
|
83
|
+
label: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type Item = Entry | GroupHeader
|
|
87
|
+
|
|
88
|
+
function text(value: unknown) {
|
|
89
|
+
if (typeof value !== "string") return undefined
|
|
90
|
+
const trimmed = value.trim()
|
|
91
|
+
return trimmed || undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ink(api: TuiPluginApi, name: string, fallback: string): Color {
|
|
95
|
+
const value = Reflect.get(api.theme.current, name)
|
|
96
|
+
if (typeof value === "string") return value
|
|
97
|
+
if (value instanceof RGBA) return value
|
|
98
|
+
return fallback
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function skin(api: TuiPluginApi): Skin {
|
|
102
|
+
return {
|
|
103
|
+
panel: ink(api, "backgroundMenu", "#1c1c1c"),
|
|
104
|
+
text: ink(api, "text", "#f0f0f0"),
|
|
105
|
+
muted: ink(api, "textMuted", "#a5a5a5"),
|
|
106
|
+
subtle: ink(api, "borderSubtle", "#6f6f6f"),
|
|
107
|
+
key: ink(api, "warning", "#ffd75f"),
|
|
108
|
+
accent: ink(api, "primary", "#5f87ff"),
|
|
109
|
+
tab: ink(api, "primary", "#5f87ff"),
|
|
110
|
+
tabText: ink(api, "selectedListItemText", "#ffffff"),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function activeKeyLabel(active: ActiveKey<Renderable, KeyEvent>) {
|
|
115
|
+
if (active.continues) return text(active.tokenName) ?? text(active.display) ?? UNKNOWN
|
|
116
|
+
return (
|
|
117
|
+
text(active.commandAttrs?.title) ?? text(active.bindingAttrs?.desc) ?? text(active.commandAttrs?.desc) ?? UNKNOWN
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function activeKeyGroup(active: ActiveKey<Renderable, KeyEvent>) {
|
|
122
|
+
if (active.continues) return "System"
|
|
123
|
+
return text(active.commandAttrs?.category) ?? text(active.bindingAttrs?.group) ?? UNKNOWN
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function activeKeyEntry(api: TuiPluginApi, active: ActiveKey<Renderable, KeyEvent>): Entry {
|
|
127
|
+
const key = api.keys.formatSequence([
|
|
128
|
+
{
|
|
129
|
+
stroke: active.stroke,
|
|
130
|
+
display: active.display,
|
|
131
|
+
tokenName: active.tokenName,
|
|
132
|
+
},
|
|
133
|
+
])
|
|
134
|
+
const label = activeKeyLabel(active)
|
|
135
|
+
return {
|
|
136
|
+
type: "entry",
|
|
137
|
+
key,
|
|
138
|
+
label: active.continues ? `+${label}` : label,
|
|
139
|
+
group: activeKeyGroup(active),
|
|
140
|
+
continues: active.continues,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function grouped(entries: Entry[]): Group[] {
|
|
145
|
+
const map = new Map<string, Entry[]>()
|
|
146
|
+
for (const entry of entries) map.set(entry.group, [...(map.get(entry.group) ?? []), entry])
|
|
147
|
+
return [...map]
|
|
148
|
+
.map(([label, entries]) => ({
|
|
149
|
+
label,
|
|
150
|
+
entries: entries.toSorted(
|
|
151
|
+
(a, b) =>
|
|
152
|
+
Number(b.continues) - Number(a.continues) || a.label.localeCompare(b.label) || a.key.localeCompare(b.key),
|
|
153
|
+
),
|
|
154
|
+
}))
|
|
155
|
+
.toSorted((a, b) => a.label.localeCompare(b.label))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function commandShortcut(api: TuiPluginApi, name: string) {
|
|
159
|
+
return useKeymapSelector((keymap) =>
|
|
160
|
+
api.keys.formatSequence(
|
|
161
|
+
keymap.getCommandBindings({ visibility: "registered", commands: [name] }).get(name)?.[0]?.sequence,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function layout(value: unknown): Layout {
|
|
167
|
+
if (value === "overlay") return "overlay"
|
|
168
|
+
return "dock"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function HomeHint(props: { api: TuiPluginApi }) {
|
|
172
|
+
const trigger = commandShortcut(props.api, command.toggle)
|
|
173
|
+
const look = createMemo(() => skin(props.api))
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<box width="100%" maxWidth={75} alignItems="center" paddingTop={1} flexShrink={0}>
|
|
177
|
+
<text fg={look().muted} wrapMode="none">
|
|
178
|
+
Show keyboard shortcuts with <span style={{ fg: look().subtle }}>{trigger() || command.toggle}</span>
|
|
179
|
+
</text>
|
|
180
|
+
</box>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function WhichKeyPanel(props: {
|
|
185
|
+
api: TuiPluginApi
|
|
186
|
+
layout: Layout
|
|
187
|
+
mode: () => Layout
|
|
188
|
+
pendingPreview: () => boolean
|
|
189
|
+
pinned: () => boolean
|
|
190
|
+
}) {
|
|
191
|
+
const dimensions = useTerminalDimensions()
|
|
192
|
+
const [offset, setOffset] = createSignal(0)
|
|
193
|
+
const [activeGroup, setActiveGroup] = createSignal<string | undefined>()
|
|
194
|
+
const pending = useKeymapSelector((keymap) => keymap.getPendingSequence())
|
|
195
|
+
const active = useKeymapSelector((keymap) => keymap.getActiveKeys({ includeMetadata: true }))
|
|
196
|
+
const pendingActive = createMemo(() => pending().length > 0 && active().length > 0)
|
|
197
|
+
const pendingAutoVisible = createMemo(() => props.mode() === "overlay" && props.pendingPreview() && pendingActive())
|
|
198
|
+
const visible = createMemo(() => props.pinned() || pendingAutoVisible())
|
|
199
|
+
const pendingMode = createMemo(() => visible() && pendingActive())
|
|
200
|
+
const left = 0
|
|
201
|
+
const width = createMemo(() => Math.max(1, dimensions().width))
|
|
202
|
+
const panelHeight = createMemo(() =>
|
|
203
|
+
Math.max(MIN_PANEL_HEIGHT, Math.min(MAX_PANEL_HEIGHT, Math.floor(dimensions().height * PANEL_HEIGHT_RATIO))),
|
|
204
|
+
)
|
|
205
|
+
const contentWidth = createMemo(() => Math.max(1, width() - 2))
|
|
206
|
+
const columns = createMemo(() =>
|
|
207
|
+
Math.max(1, Math.min(3, Math.floor((contentWidth() + COLUMN_GAP) / (MAX_COLUMN_WIDTH + COLUMN_GAP)) || 1)),
|
|
208
|
+
)
|
|
209
|
+
const entries = createMemo(() => active().map((item) => activeKeyEntry(props.api, item)))
|
|
210
|
+
const groups = createMemo(() => grouped(entries()))
|
|
211
|
+
const tabsVisible = createMemo(() => !pendingMode() && groups().length > 0)
|
|
212
|
+
const headerVisible = createMemo(() => tabsVisible() || pendingMode())
|
|
213
|
+
const footerVisible = createMemo(() => !pendingMode())
|
|
214
|
+
const rows = createMemo(() =>
|
|
215
|
+
Math.max(
|
|
216
|
+
1,
|
|
217
|
+
panelHeight() -
|
|
218
|
+
PANEL_TOP_PADDING -
|
|
219
|
+
(headerVisible() ? 1 : 0) -
|
|
220
|
+
(tabsVisible() ? TAB_CONTENT_GAP : 0) -
|
|
221
|
+
(footerVisible() ? FOOTER_MARGIN + FOOTER_HEIGHT : 0),
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
const pageSize = createMemo(() => rows() * columns())
|
|
225
|
+
const currentGroup = createMemo(() => {
|
|
226
|
+
const group = activeGroup()
|
|
227
|
+
return groups().find((item) => item.label === group) ?? groups()[0]
|
|
228
|
+
})
|
|
229
|
+
const activeEntries = createMemo(() => currentGroup()?.entries ?? [])
|
|
230
|
+
const items = createMemo<Item[]>(() => {
|
|
231
|
+
if (!pendingMode()) return activeEntries()
|
|
232
|
+
return groups().flatMap((group) => [{ type: "group", label: group.label } satisfies GroupHeader, ...group.entries])
|
|
233
|
+
})
|
|
234
|
+
const maxOffset = createMemo(() => Math.max(0, items().length - pageSize()))
|
|
235
|
+
const shown = createMemo(() => {
|
|
236
|
+
const columnsItems: Item[][] = []
|
|
237
|
+
let index = offset()
|
|
238
|
+
for (let column = 0; column < columns() && index < items().length; column++) {
|
|
239
|
+
const list: Item[] = []
|
|
240
|
+
while (list.length < rows() && index < items().length) {
|
|
241
|
+
list.push(items()[index]!)
|
|
242
|
+
index += 1
|
|
243
|
+
}
|
|
244
|
+
columnsItems.push(list)
|
|
245
|
+
}
|
|
246
|
+
return columnsItems
|
|
247
|
+
})
|
|
248
|
+
const rowIndexes = createMemo(() => Array.from({ length: rows() }, (_, index) => index))
|
|
249
|
+
const trigger = commandShortcut(props.api, command.toggle)
|
|
250
|
+
const modeTrigger = commandShortcut(props.api, command.toggleLayout)
|
|
251
|
+
const upActive = createMemo(() => offset() > 0)
|
|
252
|
+
const downActive = createMemo(() => offset() < maxOffset())
|
|
253
|
+
const scrollable = createMemo(() => maxOffset() > 0)
|
|
254
|
+
const headerItems = createMemo<HeaderItem[]>(() => [
|
|
255
|
+
...(tabsVisible() ? groups().map((group) => ({ type: "tab" as const, group })) : []),
|
|
256
|
+
...(scrollable() ? [{ type: "scroll" as const }] : []),
|
|
257
|
+
])
|
|
258
|
+
const tabGap = createMemo(() => {
|
|
259
|
+
const itemCount = headerItems().length
|
|
260
|
+
if (itemCount <= 1) return 0
|
|
261
|
+
const itemWidth = headerItems().reduce(
|
|
262
|
+
(sum, item) => sum + (item.type === "tab" ? item.group.label.length + 2 : 3),
|
|
263
|
+
0,
|
|
264
|
+
)
|
|
265
|
+
return Math.max(MIN_TAB_GAP, Math.min(TAB_GAP, Math.floor((contentWidth() - itemWidth) / (itemCount - 1))))
|
|
266
|
+
})
|
|
267
|
+
const nextMode = createMemo(() => (props.mode() === "dock" ? "overlay" : "dock"))
|
|
268
|
+
const look = createMemo(() => skin(props.api))
|
|
269
|
+
const columnWidth = createMemo(() =>
|
|
270
|
+
Math.max(1, Math.min(MAX_COLUMN_WIDTH, Math.floor((contentWidth() - (columns() - 1) * COLUMN_GAP) / columns()))),
|
|
271
|
+
)
|
|
272
|
+
const clamp = (value: number) => Math.max(0, Math.min(maxOffset(), value))
|
|
273
|
+
const scroll = (delta: number) => setOffset((value) => clamp(value + delta))
|
|
274
|
+
const moveGroup = (delta: number) => {
|
|
275
|
+
if (pendingMode()) return
|
|
276
|
+
const list = groups()
|
|
277
|
+
if (!list.length) return
|
|
278
|
+
const index = Math.max(
|
|
279
|
+
0,
|
|
280
|
+
list.findIndex((item) => item.label === currentGroup()?.label),
|
|
281
|
+
)
|
|
282
|
+
setActiveGroup(list[(index + delta + list.length) % list.length]!.label)
|
|
283
|
+
setOffset(0)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
useBindings(() => ({
|
|
287
|
+
priority: 1000,
|
|
288
|
+
enabled: visible(),
|
|
289
|
+
commands: [
|
|
290
|
+
{
|
|
291
|
+
name: command.groupPrevious,
|
|
292
|
+
title: "Previous key binding group",
|
|
293
|
+
desc: "Show the previous which-key group",
|
|
294
|
+
category: "System",
|
|
295
|
+
run() {
|
|
296
|
+
moveGroup(-1)
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: command.groupNext,
|
|
301
|
+
title: "Next key binding group",
|
|
302
|
+
desc: "Show the next which-key group",
|
|
303
|
+
category: "System",
|
|
304
|
+
run() {
|
|
305
|
+
moveGroup(1)
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: command.scrollUp,
|
|
310
|
+
title: "Scroll key bindings up",
|
|
311
|
+
desc: "Scroll the which-key panel up",
|
|
312
|
+
category: "System",
|
|
313
|
+
run() {
|
|
314
|
+
scroll(-columns())
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: command.scrollDown,
|
|
319
|
+
title: "Scroll key bindings down",
|
|
320
|
+
desc: "Scroll the which-key panel down",
|
|
321
|
+
category: "System",
|
|
322
|
+
run() {
|
|
323
|
+
scroll(columns())
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: command.pageUp,
|
|
328
|
+
title: "Page key bindings up",
|
|
329
|
+
desc: "Page the which-key panel up",
|
|
330
|
+
category: "System",
|
|
331
|
+
run() {
|
|
332
|
+
scroll(-pageSize())
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: command.pageDown,
|
|
337
|
+
title: "Page key bindings down",
|
|
338
|
+
desc: "Page the which-key panel down",
|
|
339
|
+
category: "System",
|
|
340
|
+
run() {
|
|
341
|
+
scroll(pageSize())
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: command.home,
|
|
346
|
+
title: "First key binding",
|
|
347
|
+
desc: "Jump to the first which-key binding",
|
|
348
|
+
category: "System",
|
|
349
|
+
run() {
|
|
350
|
+
setOffset(0)
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: command.end,
|
|
355
|
+
title: "Last key binding",
|
|
356
|
+
desc: "Jump to the last which-key binding",
|
|
357
|
+
category: "System",
|
|
358
|
+
run() {
|
|
359
|
+
setOffset(maxOffset())
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
bindings: pendingMode()
|
|
364
|
+
? props.api.tuiConfig.keybinds.gather("which-key.scroll", scrollCommands)
|
|
365
|
+
: props.api.tuiConfig.keybinds.gather("which-key.panel", panelCommands),
|
|
366
|
+
}))
|
|
367
|
+
|
|
368
|
+
createEffect(() => {
|
|
369
|
+
if (pendingMode()) return
|
|
370
|
+
const group = currentGroup()
|
|
371
|
+
if (group?.label === activeGroup()) return
|
|
372
|
+
setActiveGroup(group?.label)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
createEffect(() => {
|
|
376
|
+
if (pendingMode()) return
|
|
377
|
+
activeGroup()
|
|
378
|
+
setOffset(0)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
createEffect(() => {
|
|
382
|
+
if (!visible()) setOffset(0)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
createEffect(() => {
|
|
386
|
+
pending()
|
|
387
|
+
setOffset(0)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
createEffect(() => {
|
|
391
|
+
setOffset((value) => clamp(value))
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<Show when={visible()}>
|
|
396
|
+
<box
|
|
397
|
+
position={props.layout === "overlay" ? "absolute" : "relative"}
|
|
398
|
+
zIndex={3500}
|
|
399
|
+
left={left}
|
|
400
|
+
bottom={props.layout === "overlay" ? 0 : undefined}
|
|
401
|
+
width={dimensions().width}
|
|
402
|
+
height={panelHeight()}
|
|
403
|
+
backgroundColor={look().panel}
|
|
404
|
+
paddingLeft={1}
|
|
405
|
+
paddingRight={1}
|
|
406
|
+
paddingTop={1}
|
|
407
|
+
flexShrink={0}
|
|
408
|
+
flexDirection="column"
|
|
409
|
+
>
|
|
410
|
+
<Show when={headerVisible()}>
|
|
411
|
+
<box width="100%" flexDirection="row" justifyContent="center" gap={tabGap()} flexShrink={0}>
|
|
412
|
+
<For each={headerItems()}>
|
|
413
|
+
{(item) => (
|
|
414
|
+
<Show
|
|
415
|
+
when={item.type === "tab" ? item.group : undefined}
|
|
416
|
+
fallback={
|
|
417
|
+
<box flexShrink={0}>
|
|
418
|
+
<text wrapMode="none">
|
|
419
|
+
<span style={{ fg: upActive() ? look().text : look().muted }}>↑</span>
|
|
420
|
+
<span style={{ fg: look().muted }}> </span>
|
|
421
|
+
<span style={{ fg: downActive() ? look().text : look().muted }}>↓</span>
|
|
422
|
+
</text>
|
|
423
|
+
</box>
|
|
424
|
+
}
|
|
425
|
+
>
|
|
426
|
+
{(group) => {
|
|
427
|
+
const selected = createMemo(() => currentGroup()?.label === group().label)
|
|
428
|
+
return (
|
|
429
|
+
<box
|
|
430
|
+
paddingLeft={1}
|
|
431
|
+
paddingRight={1}
|
|
432
|
+
flexShrink={0}
|
|
433
|
+
backgroundColor={selected() ? look().tab : undefined}
|
|
434
|
+
onMouseDown={() => {
|
|
435
|
+
setActiveGroup(group().label)
|
|
436
|
+
setOffset(0)
|
|
437
|
+
}}
|
|
438
|
+
>
|
|
439
|
+
<text
|
|
440
|
+
fg={selected() ? look().tabText : look().muted}
|
|
441
|
+
attributes={selected() ? TextAttributes.BOLD : undefined}
|
|
442
|
+
wrapMode="none"
|
|
443
|
+
>
|
|
444
|
+
{group().label}
|
|
445
|
+
</text>
|
|
446
|
+
</box>
|
|
447
|
+
)
|
|
448
|
+
}}
|
|
449
|
+
</Show>
|
|
450
|
+
)}
|
|
451
|
+
</For>
|
|
452
|
+
</box>
|
|
453
|
+
</Show>
|
|
454
|
+
<Show when={tabsVisible()}>
|
|
455
|
+
<box height={TAB_CONTENT_GAP} flexShrink={0} />
|
|
456
|
+
</Show>
|
|
457
|
+
<box height={rows()} flexShrink={0} flexDirection="column">
|
|
458
|
+
<Show when={shown().length > 0} fallback={<text fg={look().muted}>No reachable bindings</text>}>
|
|
459
|
+
<For each={rowIndexes()}>
|
|
460
|
+
{(row) => (
|
|
461
|
+
<box width="100%" flexDirection="row" justifyContent="center" gap={COLUMN_GAP}>
|
|
462
|
+
<For each={shown()}>
|
|
463
|
+
{(column) => {
|
|
464
|
+
const item = createMemo(() => column[row])
|
|
465
|
+
const entry = createMemo(() => {
|
|
466
|
+
const value = item()
|
|
467
|
+
if (value?.type !== "entry") return undefined
|
|
468
|
+
return value
|
|
469
|
+
})
|
|
470
|
+
return (
|
|
471
|
+
<box width={columnWidth()} flexDirection="row" gap={1} justifyContent="space-between">
|
|
472
|
+
<Show when={item()}>
|
|
473
|
+
{(value) => (
|
|
474
|
+
<Show
|
|
475
|
+
when={entry()}
|
|
476
|
+
fallback={
|
|
477
|
+
<text fg={look().accent} attributes={TextAttributes.BOLD} wrapMode="none" truncate>
|
|
478
|
+
{value().label}
|
|
479
|
+
</text>
|
|
480
|
+
}
|
|
481
|
+
>
|
|
482
|
+
{(binding) => (
|
|
483
|
+
<>
|
|
484
|
+
<box flexGrow={1} minWidth={0}>
|
|
485
|
+
<text
|
|
486
|
+
fg={binding().continues ? look().accent : look().muted}
|
|
487
|
+
wrapMode="none"
|
|
488
|
+
truncate
|
|
489
|
+
>
|
|
490
|
+
{binding().label}
|
|
491
|
+
</text>
|
|
492
|
+
</box>
|
|
493
|
+
<box flexShrink={0}>
|
|
494
|
+
<text fg={look().text} attributes={TextAttributes.BOLD} wrapMode="none" truncate>
|
|
495
|
+
{binding().key}
|
|
496
|
+
</text>
|
|
497
|
+
</box>
|
|
498
|
+
</>
|
|
499
|
+
)}
|
|
500
|
+
</Show>
|
|
501
|
+
)}
|
|
502
|
+
</Show>
|
|
503
|
+
</box>
|
|
504
|
+
)
|
|
505
|
+
}}
|
|
506
|
+
</For>
|
|
507
|
+
</box>
|
|
508
|
+
)}
|
|
509
|
+
</For>
|
|
510
|
+
</Show>
|
|
511
|
+
</box>
|
|
512
|
+
<Show when={footerVisible()}>
|
|
513
|
+
<box height={FOOTER_MARGIN} flexShrink={0} />
|
|
514
|
+
<box width="100%" flexDirection="row" justifyContent="space-between" flexShrink={0}>
|
|
515
|
+
<box>
|
|
516
|
+
<text fg={look().text} wrapMode="none">
|
|
517
|
+
toggle <span style={{ fg: look().subtle }}>{trigger() || command.toggle}</span>
|
|
518
|
+
</text>
|
|
519
|
+
</box>
|
|
520
|
+
<box>
|
|
521
|
+
<text fg={look().text} wrapMode="none">
|
|
522
|
+
{nextMode()} <span style={{ fg: look().subtle }}>{modeTrigger() || command.toggleLayout}</span>
|
|
523
|
+
</text>
|
|
524
|
+
</box>
|
|
525
|
+
</box>
|
|
526
|
+
</Show>
|
|
527
|
+
</box>
|
|
528
|
+
</Show>
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const tui: TuiPlugin = async (api) => {
|
|
533
|
+
const [pinned, setPinned] = createSignal(false)
|
|
534
|
+
const [mode, setMode] = createSignal(layout(api.kv.get(KV_LAYOUT, "dock")))
|
|
535
|
+
const [pendingPreview, setPendingPreview] = createSignal(api.kv.get(KV_PENDING_PREVIEW, false))
|
|
536
|
+
|
|
537
|
+
api.keymap.registerLayer({
|
|
538
|
+
priority: LAYER_PRIORITY,
|
|
539
|
+
commands: [
|
|
540
|
+
{
|
|
541
|
+
name: command.toggle,
|
|
542
|
+
title: "Show key bindings",
|
|
543
|
+
desc: "Toggle which-key overlay",
|
|
544
|
+
category: "System",
|
|
545
|
+
run() {
|
|
546
|
+
setPinned((value) => !value)
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: command.toggleLayout,
|
|
551
|
+
title: "Toggle key bindings layout",
|
|
552
|
+
desc: "Switch which-key between dock and overlay mode",
|
|
553
|
+
category: "System",
|
|
554
|
+
run() {
|
|
555
|
+
setMode((value) => {
|
|
556
|
+
const next = value === "dock" ? "overlay" : "dock"
|
|
557
|
+
api.kv.set(KV_LAYOUT, next)
|
|
558
|
+
return next
|
|
559
|
+
})
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: command.togglePending,
|
|
564
|
+
title: "Toggle pending key preview",
|
|
565
|
+
desc: "Automatically show which-key for pending key sequences in overlay mode",
|
|
566
|
+
category: "System",
|
|
567
|
+
run() {
|
|
568
|
+
setPendingPreview((value) => {
|
|
569
|
+
api.kv.set(KV_PENDING_PREVIEW, !value)
|
|
570
|
+
return !value
|
|
571
|
+
})
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
bindings: api.tuiConfig.keybinds.gather("which-key.toggle", toggleCommands),
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
api.slots.register({
|
|
579
|
+
order: 200,
|
|
580
|
+
slots: {
|
|
581
|
+
home_bottom() {
|
|
582
|
+
return <HomeHint api={api} />
|
|
583
|
+
},
|
|
584
|
+
app() {
|
|
585
|
+
return (
|
|
586
|
+
<Show when={mode() === "overlay"}>
|
|
587
|
+
<WhichKeyPanel api={api} layout="overlay" mode={mode} pendingPreview={pendingPreview} pinned={pinned} />
|
|
588
|
+
</Show>
|
|
589
|
+
)
|
|
590
|
+
},
|
|
591
|
+
app_bottom() {
|
|
592
|
+
return (
|
|
593
|
+
<Show when={mode() === "dock"}>
|
|
594
|
+
<WhichKeyPanel api={api} layout="dock" mode={mode} pendingPreview={pendingPreview} pinned={pinned} />
|
|
595
|
+
</Show>
|
|
596
|
+
)
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const plugin: InternalTuiPlugin = {
|
|
603
|
+
id: "which-key",
|
|
604
|
+
enabled: false,
|
|
605
|
+
tui,
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export default plugin
|