opencode-ghost 1.0.1 → 1.0.3
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/CHANGELOG.md +38 -0
- package/README.md +12 -5
- package/package.json +8 -7
- package/src/composer.ts +9 -2
- package/src/keymap.ts +22 -0
- package/src/lifecycle.ts +2 -0
- package/src/options.ts +6 -6
- package/src/stateless.ts +5 -3
- package/src/text.ts +36 -5
- package/src/tui.tsx +12 -19
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.3] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **packaging:** Declare required OpenTUI runtime peers
|
|
13
|
+
|
|
14
|
+
## [1.0.2] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **composer:** Remove redundant editor existence guard
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **lifecycle:** Bound recent completion deduplication history
|
|
23
|
+
- **options:** Validate booleans and normalize accept bindings
|
|
24
|
+
- **deps:** Align host and renderer compatibility requirements
|
|
25
|
+
- **install:** Tolerate unrelated legacy configs and spaced paths
|
|
26
|
+
- **composer:** Preserve navigation and rebound accept shortcuts
|
|
27
|
+
- **model:** Anchor small-model provider on the session agent model
|
|
28
|
+
- **model:** Match host title-model variant and default-provider fallback
|
|
29
|
+
|
|
8
30
|
## [1.0.1] - 2026-10-08
|
|
9
31
|
|
|
10
32
|
### Fixed
|
|
@@ -13,6 +35,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
35
|
|
|
14
36
|
## [1.0.0] - 2026-10-08
|
|
15
37
|
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `/suggest model provider/model` and `/suggest model clear` for persistent model overrides.
|
|
41
|
+
- Effective title-agent model resolution and same-provider small-model selection.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Register TUI plugins by directory in `cli.json` instead of `tui.json`.
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
|
|
49
|
+
- V1 inline typing ghosts and slash-command argument hints.
|
|
50
|
+
- The `endpoint`, `apiKey`, `internalSessionMarkerDir`, and `argHints` options.
|
|
51
|
+
|
|
16
52
|
### Breaking changes
|
|
17
53
|
|
|
18
54
|
- Migrate inline prompt ghosts to opencode v2 _(breaking)_
|
|
@@ -167,6 +203,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
167
203
|
- Options for `model`, `acceptKeys`, `maxChars`, `idleDelayMs`,
|
|
168
204
|
`recentMessages` and `system`.
|
|
169
205
|
|
|
206
|
+
[1.0.3]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.2...v1.0.3
|
|
207
|
+
[1.0.2]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.1...v1.0.2
|
|
170
208
|
[1.0.1]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.0...v1.0.1
|
|
171
209
|
[1.0.0]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.6.0...v1.0.0
|
|
172
210
|
[0.6.0]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.5.1...v0.6.0
|
package/README.md
CHANGED
|
@@ -20,8 +20,9 @@ a dimmed inline placeholder in the empty composer with
|
|
|
20
20
|
dimmed placeholder only while the normal composer is empty and focused in an
|
|
21
21
|
idle session. `Tab` or `→` inserts it as editable text; Enter does not accept
|
|
22
22
|
or submit the ghost. Acceptance never replaces a draft or touches the clipboard.
|
|
23
|
-
- **Clears as you type.**
|
|
24
|
-
|
|
23
|
+
- **Clears as you type.** Text-editing keys and pasting dismiss the suggestion;
|
|
24
|
+
navigation keys leave it intact. Left on an empty eligible composer returns
|
|
25
|
+
home, even when suggestions are off (`backOnEmptyLeft`).
|
|
25
26
|
- **`/suggest` toggles it.** State is stored durably via the plugin's storage.
|
|
26
27
|
|
|
27
28
|
## Requirements
|
|
@@ -106,11 +107,17 @@ and directory definitions such as `agents/title.md` and `agent/title.md`, includ
|
|
|
106
107
|
agent deletion/recreation. Explicitly configured variants are preserved.
|
|
107
108
|
Without a configured title model, or when the title agent is disabled, Ghost
|
|
108
109
|
mirrors OpenCode 2.0.25's internal small-model policy until a public API exists:
|
|
109
|
-
within the session model's provider (or the
|
|
110
|
+
within the session model's provider (or the session agent model's provider,
|
|
111
|
+
then the location's default provider, then the first enabled text model's
|
|
112
|
+
provider when no default exists), choose
|
|
110
113
|
the first enabled, active text-input/text-output catalog model in family order
|
|
111
114
|
`gpt-luna`, `gemini-flash-lite`, `gemini-flash`, `claude-haiku`.
|
|
115
|
+
Like OpenCode's title generation, an override, title or small model without a
|
|
116
|
+
configured variant uses its first supported `none`, `minimal` or `low` variant.
|
|
112
117
|
Catalog IDs are used directly. No matching small model means a warning and no
|
|
113
|
-
suggestion
|
|
118
|
+
suggestion. Ghost never uses the main model as a generation fallback or retries
|
|
119
|
+
with another provider. An explicit override or configured title model may use
|
|
120
|
+
a different provider from the session's primary model.
|
|
114
121
|
|
|
115
122
|
When enabling, a suggestion is generated immediately for the current session.
|
|
116
123
|
|
|
@@ -167,7 +174,7 @@ When enabling, a suggestion is generated immediately for the current session.
|
|
|
167
174
|
```sh
|
|
168
175
|
bun install
|
|
169
176
|
bun run typecheck
|
|
170
|
-
bun test
|
|
177
|
+
bun run test
|
|
171
178
|
```
|
|
172
179
|
|
|
173
180
|
No build step: like other opencode TUI plugins, the package ships TSX source and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ghost",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Stateless inline next-prompt ghosts for the empty OpenCode V2 composer, accepted with Tab or Right.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"packageManager": "bun@1.4.2",
|
|
38
38
|
"engines": {
|
|
39
|
-
"opencode": ">=2.0.
|
|
39
|
+
"opencode": ">=2.0.25"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"prepare": "husky",
|
|
@@ -47,21 +47,22 @@
|
|
|
47
47
|
"lint:sh": "for script in install.sh tests/*.sh scripts/*.sh; do bash -n \"$script\" || exit; done",
|
|
48
48
|
"prepublishOnly": "bun run test && bun run typecheck"
|
|
49
49
|
},
|
|
50
|
-
"dependencies": {
|
|
51
|
-
"solid-js": "^1.9.12"
|
|
52
|
-
},
|
|
53
50
|
"peerDependencies": {
|
|
54
51
|
"@opencode/plugin": "*",
|
|
52
|
+
"@opentui/core": ">=0.5.16",
|
|
53
|
+
"@opentui/solid": ">=0.5.16",
|
|
55
54
|
"solid-js": "*"
|
|
56
55
|
},
|
|
57
56
|
"devDependencies": {
|
|
58
57
|
"@commitlint/cli": "^21.2.2",
|
|
59
58
|
"@commitlint/config-conventional": "^21.2.2",
|
|
60
59
|
"@opencode/plugin": "2.0.25",
|
|
61
|
-
"@opentui/core": "^0.5.
|
|
62
|
-
"@opentui/solid": "^0.5.
|
|
60
|
+
"@opentui/core": "^0.5.16",
|
|
61
|
+
"@opentui/solid": "^0.5.16",
|
|
62
|
+
"@types/bun": "^1.4.2",
|
|
63
63
|
"@types/node": "24.12.2",
|
|
64
64
|
"husky": "^9.1.7",
|
|
65
|
+
"solid-js": "^1.9.12",
|
|
65
66
|
"typescript": "7.0.2"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/src/composer.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { KeyEvent, KeyHandler, TextareaRenderable } from "@opentui/core"
|
|
2
2
|
|
|
3
|
+
export function editsComposer(event: { name: string; sequence: string; ctrl: boolean; meta: boolean; option?: boolean; super?: boolean; hyper?: boolean }): boolean {
|
|
4
|
+
if (event.ctrl && !event.meta && !event.option && !event.super && !event.hyper) return ["v", "h", "d", "k", "u", "w", "j", "m"].includes(event.name)
|
|
5
|
+
if (event.ctrl || event.meta || event.option || event.super || event.hyper) return false
|
|
6
|
+
if (["return", "enter", "linefeed", "backspace", "delete", "space"].includes(event.name)) return true
|
|
7
|
+
return [...event.sequence].length === 1 && !/[\u0000-\u001f\u007f]/.test(event.sequence)
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export function observeComposerInput(input: KeyHandler, preserve: (event: KeyEvent) => boolean, cancel: () => void): () => void {
|
|
4
11
|
const onKeypress = (event: KeyEvent) => {
|
|
5
|
-
if (!preserve(event)) cancel()
|
|
12
|
+
if (editsComposer(event) && !preserve(event)) cancel()
|
|
6
13
|
}
|
|
7
14
|
input.prependListener("keypress", onKeypress)
|
|
8
15
|
input.prependListener("paste", cancel)
|
|
@@ -31,7 +38,7 @@ export function isEmptyComposer(editor: TextareaRenderable | undefined, focused:
|
|
|
31
38
|
!(node.id.startsWith("slot-layout-") && node.getChildren().length === 0),
|
|
32
39
|
)
|
|
33
40
|
if (siblings.length !== 2 || siblings[0] !== editor) return false
|
|
34
|
-
return
|
|
41
|
+
return normal && mode === "base" && !selected && editor === focused && editor.focused && !editor.isDestroyed && editor.plainText === "" && !editor.hasSelection() && !editor.traits.capture?.includes("navigate") && editor.extmarks.getAll().length === 0
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
export function findComposerEditor<T extends EditorNode>(marker: EditorNode | undefined, isEditor: (node: EditorNode) => node is T, isComposer: (editor: T) => boolean): T | undefined {
|
package/src/keymap.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import type { KeymapCommand, KeymapLayer } from "@opencode/plugin/tui/context"
|
|
2
|
+
import { canonicalKey } from "./composer"
|
|
3
|
+
|
|
4
|
+
export const acceptCommandId = (key: string) => `ghost.accept.${key}`
|
|
5
|
+
|
|
6
|
+
export function acceptCommand(key: string, session: () => string | undefined, suggestionSession: () => string | undefined, act: (sessionID: string) => boolean): KeymapCommand {
|
|
7
|
+
return {
|
|
8
|
+
id: acceptCommandId(key),
|
|
9
|
+
bind: key,
|
|
10
|
+
enabled: () => Boolean(suggestionSession()),
|
|
11
|
+
run: () => {
|
|
12
|
+
const sessionID = session()
|
|
13
|
+
if (!sessionID || suggestionSession() !== sessionID || !act(sessionID)) return false
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function acceptShortcuts(acceptKeys: readonly string[], shortcuts: (id: string) => readonly string[]): Set<string> {
|
|
19
|
+
return new Set(acceptKeys.flatMap((key) => {
|
|
20
|
+
const bound = shortcuts(acceptCommandId(key))
|
|
21
|
+
return bound.map(canonicalKey)
|
|
22
|
+
}))
|
|
23
|
+
}
|
|
2
24
|
|
|
3
25
|
export function ghostKeymapLayers(
|
|
4
26
|
composerCommands: KeymapCommand[],
|
package/src/lifecycle.ts
CHANGED
|
@@ -28,6 +28,7 @@ export function createLifecycle(delay: number, allowed: (sessionID: string) => b
|
|
|
28
28
|
succeeded(id: string, sessionID: string) {
|
|
29
29
|
if (disposed || !allowed(sessionID) || completed.has(id)) return
|
|
30
30
|
completed.add(id)
|
|
31
|
+
if (completed.size > COMPLETION_HISTORY_LIMIT) completed.delete(completed.values().next().value!)
|
|
31
32
|
cancel()
|
|
32
33
|
timer = setTimeout(() => { timer = undefined; void run(sessionID) }, delay)
|
|
33
34
|
},
|
|
@@ -39,3 +40,4 @@ export function createLifecycle(delay: number, allowed: (sessionID: string) => b
|
|
|
39
40
|
dispose() { disposed = true; cancel() },
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
export const COMPLETION_HISTORY_LIMIT = 1024
|
package/src/options.ts
CHANGED
|
@@ -66,14 +66,14 @@ export function resolveOptions(options: Readonly<Record<string, unknown>> | unde
|
|
|
66
66
|
const maxChars = input.maxChars
|
|
67
67
|
const idleDelayMs = input.idleDelayMs
|
|
68
68
|
const recentMessages = input.recentMessages
|
|
69
|
+
const acceptKeys = Array.isArray(input.acceptKeys)
|
|
70
|
+
? [...new Set(input.acceptKeys.filter((key) => typeof key === "string" && key.trim() && key.trim().split("+").every((part) => part.trim())).map(canonicalKey))]
|
|
71
|
+
: []
|
|
69
72
|
return {
|
|
70
|
-
enabled: input.enabled
|
|
73
|
+
enabled: typeof input.enabled === "boolean" ? input.enabled : DEFAULTS.enabled,
|
|
71
74
|
model: typeof input.model === "string" && input.model.trim() ? input.model.trim() : undefined,
|
|
72
|
-
acceptKeys:
|
|
73
|
-
|
|
74
|
-
? input.acceptKeys.filter((key) => typeof key === "string" && key.trim()).map(canonicalKey)
|
|
75
|
-
: [...DEFAULTS.acceptKeys],
|
|
76
|
-
backOnEmptyLeft: input.backOnEmptyLeft ?? DEFAULTS.backOnEmptyLeft,
|
|
75
|
+
acceptKeys: acceptKeys.length > 0 ? acceptKeys : [...DEFAULTS.acceptKeys],
|
|
76
|
+
backOnEmptyLeft: typeof input.backOnEmptyLeft === "boolean" ? input.backOnEmptyLeft : DEFAULTS.backOnEmptyLeft,
|
|
77
77
|
maxChars: typeof maxChars === "number" && maxChars > 0 ? maxChars : DEFAULTS.maxChars,
|
|
78
78
|
idleDelayMs:
|
|
79
79
|
typeof idleDelayMs === "number" && idleDelayMs >= 0 ? idleDelayMs : DEFAULTS.idleDelayMs,
|
package/src/stateless.ts
CHANGED
|
@@ -12,14 +12,16 @@ export type GenerateClient = {
|
|
|
12
12
|
export async function generateSuggestion(
|
|
13
13
|
client: GenerateClient,
|
|
14
14
|
prompt: string,
|
|
15
|
-
model: SuggestionModel | undefined
|
|
15
|
+
model: SuggestionModel | undefined | Promise<SuggestionModel | undefined>,
|
|
16
16
|
signal: AbortSignal,
|
|
17
17
|
onError?: (error: unknown) => void,
|
|
18
|
+
allowed: () => boolean = () => true,
|
|
18
19
|
): Promise<string | undefined> {
|
|
19
|
-
if (signal.aborted || !model) return undefined
|
|
20
20
|
try {
|
|
21
|
+
const resolved = await model
|
|
22
|
+
if (signal.aborted || !allowed() || !resolved) return undefined
|
|
21
23
|
const result = await client.generate.text(
|
|
22
|
-
{ prompt, model: { id:
|
|
24
|
+
{ prompt, model: { id: resolved.modelID, providerID: resolved.providerID, ...(resolved.variant ? { variant: resolved.variant } : {}) } },
|
|
23
25
|
{ signal },
|
|
24
26
|
)
|
|
25
27
|
return result.text
|
package/src/text.ts
CHANGED
|
@@ -24,24 +24,55 @@ export async function resolveSmallModel(
|
|
|
24
24
|
client: SmallModelClient,
|
|
25
25
|
location: LocationRef,
|
|
26
26
|
signal: AbortSignal,
|
|
27
|
-
providerID?: string,
|
|
27
|
+
session?: { model?: { providerID: string }; agent?: string },
|
|
28
28
|
): Promise<SuggestionModel | undefined> {
|
|
29
29
|
if (signal.aborted) return undefined
|
|
30
30
|
const agents = await client.agent.list({ location }, { signal })
|
|
31
31
|
if (signal.aborted) return undefined
|
|
32
32
|
const configured = agents.data.find((agent) => agent.id === "title")?.model
|
|
33
|
-
|
|
34
|
-
if (
|
|
33
|
+
let providerID = session?.model?.providerID ?? (session?.agent ? agents.data.find((agent) => agent.id === session.agent)?.model?.providerID : undefined)
|
|
34
|
+
if (configured?.variant) return { providerID: configured.providerID, modelID: configured.id, variant: configured.variant }
|
|
35
|
+
if (!configured && !providerID) {
|
|
35
36
|
const primary = await client.model.default({ location }, { signal })
|
|
36
37
|
if (signal.aborted) return undefined
|
|
37
38
|
providerID = primary.data?.providerID
|
|
38
39
|
}
|
|
39
|
-
if (!providerID) throw new Error(NO_SMALL_MODEL)
|
|
40
40
|
const catalog = await client.model.list({ location }, { signal })
|
|
41
41
|
if (signal.aborted) return undefined
|
|
42
|
+
if (configured) {
|
|
43
|
+
const info = catalog.data.find((model) => model.providerID === configured.providerID && model.id === configured.id)
|
|
44
|
+
return withLowEffortVariant({ providerID: configured.providerID, modelID: configured.id }, info)
|
|
45
|
+
}
|
|
46
|
+
providerID ??= catalog.data.find((model) => model.enabled && isTextModel(model))?.providerID
|
|
47
|
+
if (!providerID) throw new Error(NO_SMALL_MODEL)
|
|
42
48
|
const model = selectSmallModel(catalog.data, providerID)
|
|
43
49
|
if (!model) throw new Error(NO_SMALL_MODEL)
|
|
44
|
-
return model
|
|
50
|
+
return withLowEffortVariant(model, catalog.data.find((info) => info.providerID === model.providerID && info.id === model.modelID))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function resolveExplicitModel(
|
|
54
|
+
client: Pick<SmallModelClient, "model">,
|
|
55
|
+
location: LocationRef,
|
|
56
|
+
signal: AbortSignal,
|
|
57
|
+
model: SuggestionModel,
|
|
58
|
+
): Promise<SuggestionModel | undefined> {
|
|
59
|
+
if (signal.aborted) return undefined
|
|
60
|
+
if (model.variant) return model
|
|
61
|
+
const catalog = await client.model.list({ location }, { signal })
|
|
62
|
+
if (signal.aborted) return undefined
|
|
63
|
+
return withLowEffortVariant(model, catalog.data.find((info) => info.providerID === model.providerID && info.id === model.modelID))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const LOW_EFFORT_VARIANTS = ["none", "minimal", "low"]
|
|
67
|
+
|
|
68
|
+
function withLowEffortVariant(model: SuggestionModel, info: ModelInfo | undefined): SuggestionModel {
|
|
69
|
+
const variant = LOW_EFFORT_VARIANTS.find((id) => info?.variants.some((item) => item.id === id))
|
|
70
|
+
return variant ? { ...model, variant } : model
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTextModel(model: ModelInfo) {
|
|
74
|
+
const text = (items: readonly string[]) => items.length === 0 || items.some((item) => item.startsWith("text"))
|
|
75
|
+
return text(model.capabilities.input) && text(model.capabilities.output)
|
|
45
76
|
}
|
|
46
77
|
|
|
47
78
|
export function parseModel(spec: string | undefined): { providerID: string; modelID: string } | undefined {
|
package/src/tui.tsx
CHANGED
|
@@ -4,11 +4,11 @@ import { TextareaRenderable, InputRenderable, type BoxRenderable, type Renderabl
|
|
|
4
4
|
import { createEffect, createRoot, createSignal, on, onCleanup } from "solid-js"
|
|
5
5
|
import { composerAction, findComposerEditor, isEmptyComposer, keyName, observeComposerInput } from "./composer"
|
|
6
6
|
import { createLifecycle } from "./lifecycle"
|
|
7
|
-
import { ghostKeymapLayers } from "./keymap"
|
|
7
|
+
import { acceptCommand, acceptShortcuts, ghostKeymapLayers } from "./keymap"
|
|
8
8
|
import { parseSuggestCommand, removedOptionKeys, resolveOptions, suggestionModel } from "./options"
|
|
9
9
|
import { createInlinePlaceholder } from "./placeholder"
|
|
10
10
|
import { generateSuggestion } from "./stateless"
|
|
11
|
-
import { isEcho, normalize, resolveSmallModel } from "./text"
|
|
11
|
+
import { isEcho, normalize, resolveExplicitModel, resolveSmallModel } from "./text"
|
|
12
12
|
import { buildTranscript, lastUserText, suggestionPrompt, type TranscriptMessage } from "./transcript"
|
|
13
13
|
|
|
14
14
|
type Suggestion = { sessionID: string; text: string }
|
|
@@ -73,10 +73,11 @@ export default Plugin.define({
|
|
|
73
73
|
if (!transcript) return
|
|
74
74
|
const session = context.data.session.get(sessionID)
|
|
75
75
|
const location = session?.location ?? context.location ?? context.data.location.default()
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
const explicit = suggestionModel(state.model, opts.model)
|
|
77
|
+
const model = explicit
|
|
78
|
+
? resolveExplicitModel(context.client, location, signal, explicit)
|
|
79
|
+
: resolveSmallModel(context.client, location, signal, session)
|
|
80
|
+
const raw = await generateSuggestion(context.client, suggestionPrompt(opts.system, transcript), model, signal, warnGeneration, () => canGenerate(sessionID))
|
|
80
81
|
if (signal.aborted || !raw) return
|
|
81
82
|
const clean = normalize(raw, opts.maxChars)
|
|
82
83
|
if (!clean) return
|
|
@@ -151,22 +152,14 @@ export default Plugin.define({
|
|
|
151
152
|
syncInline()
|
|
152
153
|
const key = keyName(event)
|
|
153
154
|
const current = suggestion()
|
|
154
|
-
return Boolean(current && inline.visible(getComposer(current.sessionID).editor, current.text) && opts.acceptKeys.
|
|
155
|
+
return Boolean(current && inline.visible(getComposer(current.sessionID).editor, current.text) && acceptShortcuts(opts.acceptKeys, context.keymap.shortcuts).has(key) && canGenerate(current.sessionID) && isComposerEmpty(current.sessionID))
|
|
155
156
|
}, cancelSuggestion)
|
|
156
157
|
|
|
157
158
|
const commandLayers = () => ghostKeymapLayers([
|
|
158
|
-
...opts.acceptKeys.map((key) => ({
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
run: () => {
|
|
163
|
-
const route = context.ui.router.current()
|
|
164
|
-
if (route.type !== "session") return false
|
|
165
|
-
const current = suggestion()
|
|
166
|
-
if (!current || current.sessionID !== route.sessionID) return false
|
|
167
|
-
if (!act(route.sessionID)) return false
|
|
168
|
-
},
|
|
169
|
-
})),
|
|
159
|
+
...opts.acceptKeys.map((key) => acceptCommand(key, () => {
|
|
160
|
+
const route = context.ui.router.current()
|
|
161
|
+
return route.type === "session" ? route.sessionID : undefined
|
|
162
|
+
}, () => suggestion()?.sessionID, act)),
|
|
170
163
|
{
|
|
171
164
|
id: "ghost.home",
|
|
172
165
|
bind: "left",
|