opencode-ghost 1.0.5 → 1.0.7

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 CHANGED
@@ -5,6 +5,19 @@ 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.7] - 2026-10-08
9
+
10
+ ### Fixed
11
+
12
+ - **tui:** Remove left-arrow home navigation
13
+ - **tui:** Ignore all slot wrappers when detecting the composer
14
+
15
+ ## [1.0.6] - 2026-10-08
16
+
17
+ ### Fixed
18
+
19
+ - **tui:** Restore retained suggestions when drafts are emptied
20
+
8
21
  ## [1.0.5] - 2026-10-08
9
22
 
10
23
  ### Fixed
@@ -215,6 +228,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
215
228
  - Options for `model`, `acceptKeys`, `maxChars`, `idleDelayMs`,
216
229
  `recentMessages` and `system`.
217
230
 
231
+ [1.0.7]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.6...v1.0.7
232
+ [1.0.6]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.5...v1.0.6
218
233
  [1.0.5]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.4...v1.0.5
219
234
  [1.0.4]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.3...v1.0.4
220
235
  [1.0.3]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.2...v1.0.3
package/README.md CHANGED
@@ -17,12 +17,13 @@ a dimmed inline placeholder in the empty composer with
17
17
  short transcript of the recent turns, and asks a model for one line that
18
18
  sounds like you.
19
19
  - **Inline empty-composer ghost.** The suggestion renders as the textarea's
20
- dimmed placeholder only while the normal composer is empty and focused in an
21
- idle session. `Tab` or `→` inserts it as editable text; Enter does not accept
20
+ dimmed placeholder while the normal composer is empty in an idle session,
21
+ including when unfocused. With the composer focused, `Tab` or `→` inserts it
22
+ as editable text; Enter does not accept
22
23
  or submit the ghost. Acceptance never replaces a draft or touches the clipboard.
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`).
24
+ - **Hides as you type.** Typing and pasting abort pending generation and hide the
25
+ preview. Emptying the composer restores the same suggestion without another
26
+ API call; focus changes also reuse it. Navigation keys leave it intact.
26
27
  - **`/suggest` toggles it.** State is stored durably via the plugin's storage.
27
28
 
28
29
  ## Requirements
@@ -91,7 +92,6 @@ copy, use `/absolute/path/to/config/plugins/opencode-ghost`.
91
92
  | `idleDelayMs` | `number` | `500` | Debounce after a turn finishes before generating. |
92
93
  | `recentMessages` | `number` | `10` | How many recent messages feed the suggestion prompt. |
93
94
  | `system` | `string` | built-in | Override the system prompt sent to the suggestion model. |
94
- | `backOnEmptyLeft` | `boolean` | `true` | Return to the home screen with Left when the composer is empty. |
95
95
 
96
96
  ## Commands
97
97
 
@@ -137,6 +137,9 @@ When enabling, a suggestion is generated immediately for the current session.
137
137
  - Accepting uses the composer editor's `insertText` directly, without submitting.
138
138
  - Typing, pasting, navigation, a new execution, disabling and unloading cancel
139
139
  pending generation. Duplicate completion events do not generate again.
140
+ - Retains only one suggestion, bounded by `maxChars`, for the visible session.
141
+ Acceptance, a new turn, session changes, disabling, model changes and unloading
142
+ discard it.
140
143
 
141
144
  ## Caveats
142
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ghost",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",
package/src/composer.ts CHANGED
@@ -39,9 +39,9 @@ export function isEmptyComposer(editor: TextareaRenderable | undefined, focused:
39
39
  export function isEligibleComposer(editor: TextareaRenderable | undefined, mode: string, normal: boolean, selected: boolean): boolean {
40
40
  if (!editor?.parent || editor.parent.isDestroyed) return false
41
41
  const siblings = editor.parent.getChildren().filter((node) =>
42
- !(node.id.startsWith("slot-layout-") && node.getChildren().length === 0),
42
+ !node.id.startsWith("slot-layout-"),
43
43
  )
44
- if (siblings.length !== 2 || siblings[0] !== editor) return false
44
+ if (siblings.length !== 1 || siblings[0] !== editor) return false
45
45
  return normal && mode === "base" && !selected && !editor.isDestroyed && editor.plainText === "" && !editor.hasSelection() && !editor.traits.capture?.includes("navigate") && editor.extmarks.getAll().length === 0
46
46
  }
47
47
 
package/src/lifecycle.ts CHANGED
@@ -4,13 +4,13 @@ export function createLifecycle(delay: number, allowed: (sessionID: string) => b
4
4
  let disposed = false
5
5
  let warned = false
6
6
  const completed = new Set<string>()
7
- const cancel = () => {
7
+ const interrupt = () => {
8
8
  clearTimeout(timer)
9
9
  timer = undefined
10
10
  controller?.abort()
11
11
  controller = undefined
12
- clear()
13
12
  }
13
+ const cancel = () => { interrupt(); clear() }
14
14
  const run = async (sessionID: string) => {
15
15
  cancel()
16
16
  if (disposed || !allowed(sessionID)) return
@@ -23,6 +23,7 @@ export function createLifecycle(delay: number, allowed: (sessionID: string) => b
23
23
  }
24
24
  }
25
25
  return {
26
+ interrupt,
26
27
  cancel,
27
28
  run,
28
29
  succeeded(id: string, sessionID: string) {
package/src/options.ts CHANGED
@@ -5,7 +5,6 @@ export type GhostOptions = {
5
5
  enabled?: boolean
6
6
  model?: string
7
7
  acceptKeys?: string[]
8
- backOnEmptyLeft?: boolean
9
8
  maxChars?: number
10
9
  idleDelayMs?: number
11
10
  recentMessages?: number
@@ -16,7 +15,6 @@ export type ResolvedOptions = {
16
15
  enabled: boolean
17
16
  model: string | undefined
18
17
  acceptKeys: string[]
19
- backOnEmptyLeft: boolean
20
18
  maxChars: number
21
19
  idleDelayMs: number
22
20
  recentMessages: number
@@ -48,13 +46,12 @@ export const DEFAULT_SYSTEM = [
48
46
  const DEFAULTS = {
49
47
  enabled: true,
50
48
  acceptKeys: ["tab", "right"],
51
- backOnEmptyLeft: true,
52
49
  maxChars: 120,
53
50
  idleDelayMs: 500,
54
51
  recentMessages: 10,
55
52
  }
56
53
 
57
- const REMOVED_OPTION_KEYS = ["endpoint", "apiKey", "internalSessionMarkerDir", "argHints"] as const
54
+ const REMOVED_OPTION_KEYS = ["endpoint", "apiKey", "internalSessionMarkerDir", "argHints", "backOnEmptyLeft"] as const
58
55
 
59
56
  export function removedOptionKeys(raw: Readonly<Record<string, unknown>> | undefined): string[] {
60
57
  if (!raw) return []
@@ -73,7 +70,6 @@ export function resolveOptions(options: Readonly<Record<string, unknown>> | unde
73
70
  enabled: typeof input.enabled === "boolean" ? input.enabled : DEFAULTS.enabled,
74
71
  model: typeof input.model === "string" && input.model.trim() ? input.model.trim() : undefined,
75
72
  acceptKeys: acceptKeys.length > 0 ? acceptKeys : [...DEFAULTS.acceptKeys],
76
- backOnEmptyLeft: typeof input.backOnEmptyLeft === "boolean" ? input.backOnEmptyLeft : DEFAULTS.backOnEmptyLeft,
77
73
  maxChars: typeof maxChars === "number" && maxChars > 0 ? maxChars : DEFAULTS.maxChars,
78
74
  idleDelayMs:
79
75
  typeof idleDelayMs === "number" && idleDelayMs >= 0 ? idleDelayMs : DEFAULTS.idleDelayMs,
package/src/tui.tsx CHANGED
@@ -154,25 +154,13 @@ export default Plugin.define({
154
154
  const key = keyName(event)
155
155
  const current = suggestion()
156
156
  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))
157
- }, cancelSuggestion)
157
+ }, () => { lifecycle.interrupt(); inline.clear() })
158
158
 
159
159
  const commandLayers = () => ghostKeymapLayers([
160
160
  ...opts.acceptKeys.map((key) => acceptCommand(key, () => {
161
161
  const route = context.ui.router.current()
162
162
  return route.type === "session" ? route.sessionID : undefined
163
163
  }, () => suggestion()?.sessionID, act)),
164
- {
165
- id: "ghost.home",
166
- bind: "left",
167
- enabled: () => opts.backOnEmptyLeft,
168
- run: () => {
169
- const route = context.ui.router.current()
170
- if (route.type !== "session") return false
171
- if (!isComposerEmpty(route.sessionID)) return false
172
- cancelSuggestion()
173
- context.ui.router.navigate({ type: "home" })
174
- },
175
- },
176
164
  ], state.enabled, async (input) => {
177
165
  const command = parseSuggestCommand(input)
178
166
  if (command.type === "model") {