opencode-ghost 1.0.6 → 1.1.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/CHANGELOG.md +15 -0
- package/README.md +17 -3
- package/package.json +1 -1
- package/src/composer.ts +2 -2
- package/src/diagnostics.ts +76 -0
- package/src/options.ts +13 -6
- package/src/tui.tsx +79 -23
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.1.0] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **tui:** Add /suggest debug and log diagnostics
|
|
13
|
+
|
|
14
|
+
## [1.0.7] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **tui:** Remove left-arrow home navigation
|
|
19
|
+
- **tui:** Ignore all slot wrappers when detecting the composer
|
|
20
|
+
|
|
8
21
|
## [1.0.6] - 2026-10-08
|
|
9
22
|
|
|
10
23
|
### Fixed
|
|
@@ -221,6 +234,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
221
234
|
- Options for `model`, `acceptKeys`, `maxChars`, `idleDelayMs`,
|
|
222
235
|
`recentMessages` and `system`.
|
|
223
236
|
|
|
237
|
+
[1.1.0]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.7...v1.1.0
|
|
238
|
+
[1.0.7]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.6...v1.0.7
|
|
224
239
|
[1.0.6]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.5...v1.0.6
|
|
225
240
|
[1.0.5]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.4...v1.0.5
|
|
226
241
|
[1.0.4]: https://github.com/ozandogrultan/opencode-ghost/compare/v1.0.3...v1.0.4
|
package/README.md
CHANGED
|
@@ -24,8 +24,6 @@ a dimmed inline placeholder in the empty composer with
|
|
|
24
24
|
- **Hides as you type.** Typing and pasting abort pending generation and hide the
|
|
25
25
|
preview. Emptying the composer restores the same suggestion without another
|
|
26
26
|
API call; focus changes also reuse it. Navigation keys leave it intact.
|
|
27
|
-
Left on an empty eligible focused composer returns
|
|
28
|
-
home, even when suggestions are off (`backOnEmptyLeft`).
|
|
29
27
|
- **`/suggest` toggles it.** State is stored durably via the plugin's storage.
|
|
30
28
|
|
|
31
29
|
## Requirements
|
|
@@ -94,7 +92,6 @@ copy, use `/absolute/path/to/config/plugins/opencode-ghost`.
|
|
|
94
92
|
| `idleDelayMs` | `number` | `500` | Debounce after a turn finishes before generating. |
|
|
95
93
|
| `recentMessages` | `number` | `10` | How many recent messages feed the suggestion prompt. |
|
|
96
94
|
| `system` | `string` | built-in | Override the system prompt sent to the suggestion model. |
|
|
97
|
-
| `backOnEmptyLeft` | `boolean` | `true` | Return to the home screen with Left when the composer is empty. |
|
|
98
95
|
|
|
99
96
|
## Commands
|
|
100
97
|
|
|
@@ -102,6 +99,12 @@ copy, use `/absolute/path/to/config/plugins/opencode-ghost`.
|
|
|
102
99
|
- `/suggest model openai/gpt-6-luna-fast` — set a persisted runtime model override.
|
|
103
100
|
- `/suggest model clear` — clear the override and use the configured `model`,
|
|
104
101
|
or the OpenCode small default, never the main model.
|
|
102
|
+
- `/suggest debug` — toggle debug mode (persisted). While on, each generation
|
|
103
|
+
attempt shows a toast with its outcome, resolved model, latency and length, or
|
|
104
|
+
the error.
|
|
105
|
+
- `/suggest debug on` / `/suggest debug off` — set debug mode explicitly.
|
|
106
|
+
- `/suggest log` — show the last 20 generation attempts (outcome, model,
|
|
107
|
+
latency, length and error detail) in a dialog, regardless of debug mode.
|
|
105
108
|
|
|
106
109
|
Model precedence is the persisted `/suggest model` override, then the `model`
|
|
107
110
|
option, then the effective title agent's model from OpenCode's public agent API.
|
|
@@ -144,6 +147,17 @@ When enabling, a suggestion is generated immediately for the current session.
|
|
|
144
147
|
Acceptance, a new turn, session changes, disabling, model changes and unloading
|
|
145
148
|
discard it.
|
|
146
149
|
|
|
150
|
+
## Debugging
|
|
151
|
+
|
|
152
|
+
Ghost records every generation attempt to a bounded in-memory history (the last
|
|
153
|
+
20): outcome (`ok`, `empty`, `echo`, `no model`, `error`, `aborted`), the resolved
|
|
154
|
+
model with its variant, latency, suggestion length and error detail. `/suggest log`
|
|
155
|
+
shows it in a dialog at any time; `aborted` covers attempts cancelled by typing,
|
|
156
|
+
a newer turn, leaving the session, disabling or unload, so a missing suggestion can
|
|
157
|
+
be traced to a debounce, an empty or `NONE` reply, an echo filter, or a provider
|
|
158
|
+
error. `/suggest debug` toggles the same data as a toast on each attempt (aborts
|
|
159
|
+
are logged but not toasted). The history is not written to disk.
|
|
160
|
+
|
|
147
161
|
## Caveats
|
|
148
162
|
|
|
149
163
|
- **No inline typing completions.** V1's slash-argument and history ghosts
|
package/package.json
CHANGED
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
|
-
!
|
|
42
|
+
!node.id.startsWith("slot-layout-"),
|
|
43
43
|
)
|
|
44
|
-
if (siblings.length !==
|
|
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
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type DiagnosticOutcome = "ok" | "empty" | "echo" | "unavailable" | "error" | "aborted"
|
|
2
|
+
|
|
3
|
+
export type DiagnosticEntry = {
|
|
4
|
+
readonly at: number
|
|
5
|
+
readonly sessionID: string
|
|
6
|
+
readonly outcome: DiagnosticOutcome
|
|
7
|
+
readonly model?: string
|
|
8
|
+
readonly durationMs?: number
|
|
9
|
+
readonly chars?: number
|
|
10
|
+
readonly detail?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DIAGNOSTIC_HISTORY_LIMIT = 20
|
|
14
|
+
|
|
15
|
+
export type Diagnostics = {
|
|
16
|
+
record(entry: Omit<DiagnosticEntry, "at">): DiagnosticEntry
|
|
17
|
+
list(): readonly DiagnosticEntry[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createDiagnostics(limit = DIAGNOSTIC_HISTORY_LIMIT, now: () => number = Date.now): Diagnostics {
|
|
21
|
+
const entries: DiagnosticEntry[] = []
|
|
22
|
+
return {
|
|
23
|
+
record(input) {
|
|
24
|
+
const entry: DiagnosticEntry = { ...input, at: now() }
|
|
25
|
+
entries.push(entry)
|
|
26
|
+
if (entries.length > limit) entries.splice(0, entries.length - limit)
|
|
27
|
+
return entry
|
|
28
|
+
},
|
|
29
|
+
list() {
|
|
30
|
+
return [...entries].reverse()
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function modelLabel(model: { providerID: string; modelID: string; variant?: string }): string {
|
|
36
|
+
return `${model.providerID}/${model.modelID}${model.variant ? `@${model.variant}` : ""}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function errorDetail(error: unknown): string {
|
|
40
|
+
const text = error instanceof Error
|
|
41
|
+
? error.message
|
|
42
|
+
: typeof error === "string"
|
|
43
|
+
? error
|
|
44
|
+
: error === undefined || error === null
|
|
45
|
+
? ""
|
|
46
|
+
: String(error)
|
|
47
|
+
return text.replace(/\s+/g, " ").trim() || "unknown error"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const OUTCOME_LABELS: Record<DiagnosticOutcome, string> = {
|
|
51
|
+
ok: "ok",
|
|
52
|
+
empty: "empty",
|
|
53
|
+
echo: "echo",
|
|
54
|
+
unavailable: "no model",
|
|
55
|
+
error: "error",
|
|
56
|
+
aborted: "aborted",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clockTime(at: number): string {
|
|
60
|
+
const date = new Date(at)
|
|
61
|
+
const pad = (value: number) => String(value).padStart(2, "0")
|
|
62
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatDiagnostic(entry: DiagnosticEntry): string {
|
|
66
|
+
const parts = [clockTime(entry.at), OUTCOME_LABELS[entry.outcome], entry.model ?? "—"]
|
|
67
|
+
if (entry.durationMs !== undefined) parts.push(`${entry.durationMs}ms`)
|
|
68
|
+
if (entry.chars !== undefined) parts.push(`${entry.chars} chars`)
|
|
69
|
+
const line = parts.join(" ")
|
|
70
|
+
return entry.detail ? `${line}\n ${entry.detail}` : line
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatDiagnostics(entries: readonly DiagnosticEntry[]): string {
|
|
74
|
+
if (entries.length === 0) return "No generation attempts recorded yet."
|
|
75
|
+
return entries.map(formatDiagnostic).join("\n")
|
|
76
|
+
}
|
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,
|
|
@@ -85,9 +81,20 @@ export function resolveOptions(options: Readonly<Record<string, unknown>> | unde
|
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
export
|
|
84
|
+
export type SuggestCommand =
|
|
85
|
+
| { type: "toggle" }
|
|
86
|
+
| { type: "model"; model: string | undefined }
|
|
87
|
+
| { type: "debug"; value: boolean | undefined }
|
|
88
|
+
| { type: "log" }
|
|
89
|
+
| { type: "invalid" }
|
|
90
|
+
|
|
91
|
+
export function parseSuggestCommand(input: string | undefined): SuggestCommand {
|
|
89
92
|
const raw = input?.trim() ?? ""
|
|
90
93
|
if (!raw) return { type: "toggle" }
|
|
94
|
+
if (raw === "log") return { type: "log" }
|
|
95
|
+
if (raw === "debug") return { type: "debug", value: undefined }
|
|
96
|
+
const debug = /^debug\s+(on|off)$/.exec(raw)
|
|
97
|
+
if (debug) return { type: "debug", value: debug[1] === "on" }
|
|
91
98
|
const match = /^model\s+(\S+)$/.exec(raw)
|
|
92
99
|
if (!match || (match[1] !== "clear" && !parseModel(match[1]))) return { type: "invalid" }
|
|
93
100
|
return { type: "model", model: match[1] === "clear" ? undefined : match[1] }
|
package/src/tui.tsx
CHANGED
|
@@ -3,12 +3,13 @@ import { Plugin } from "@opencode/plugin/tui"
|
|
|
3
3
|
import { TextareaRenderable, InputRenderable, type BoxRenderable, type Renderable } from "@opentui/core"
|
|
4
4
|
import { createEffect, createMemo, createRoot, createSignal, on, onCleanup } from "solid-js"
|
|
5
5
|
import { composerAction, findComposerEditor, isEligibleComposer, isEmptyComposer, keyName, observeComposerInput } from "./composer"
|
|
6
|
+
import { createDiagnostics, errorDetail, formatDiagnostic, formatDiagnostics, modelLabel, type DiagnosticEntry } from "./diagnostics"
|
|
6
7
|
import { createLifecycle } from "./lifecycle"
|
|
7
8
|
import { acceptCommand, acceptShortcuts, ghostKeymapLayers } from "./keymap"
|
|
8
9
|
import { parseSuggestCommand, removedOptionKeys, resolveOptions, suggestionModel } from "./options"
|
|
9
10
|
import { createInlinePlaceholder, dimPlaceholderColor } from "./placeholder"
|
|
10
11
|
import { generateSuggestion } from "./stateless"
|
|
11
|
-
import { isEcho, normalize, resolveExplicitModel, resolveSmallModel } from "./text"
|
|
12
|
+
import { isEcho, normalize, resolveExplicitModel, resolveSmallModel, type SuggestionModel } from "./text"
|
|
12
13
|
import { buildTranscript, lastUserText, suggestionPrompt, type TranscriptMessage } from "./transcript"
|
|
13
14
|
|
|
14
15
|
type Suggestion = { sessionID: string; text: string }
|
|
@@ -35,13 +36,22 @@ export default Plugin.define({
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const [state, updateState] = context.storage.store("state", {
|
|
38
|
-
initial: { enabled: opts.enabled, model: undefined as string | undefined },
|
|
39
|
+
initial: { enabled: opts.enabled, model: undefined as string | undefined, debug: false },
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
const [suggestion, setSuggestion] = createSignal<Suggestion | undefined>()
|
|
42
43
|
const [composerRevision, setComposerRevision] = createSignal(0)
|
|
43
44
|
const inline = createInlinePlaceholder()
|
|
44
45
|
const ghostColor = createMemo(() => dimPlaceholderColor(context.theme.text.muted))
|
|
46
|
+
const diagnostics = createDiagnostics()
|
|
47
|
+
|
|
48
|
+
const note = (entry: Omit<DiagnosticEntry, "at">) => {
|
|
49
|
+
const recorded = diagnostics.record(entry)
|
|
50
|
+
if (state.debug === true && recorded.outcome !== "aborted") {
|
|
51
|
+
context.ui.toast.show({ title: "Ghost", message: formatDiagnostic(recorded) })
|
|
52
|
+
}
|
|
53
|
+
return recorded
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
let disposed = false
|
|
47
57
|
const composers = new Map<string, { marker: BoxRenderable; normal: () => boolean }>()
|
|
@@ -66,6 +76,10 @@ export default Plugin.define({
|
|
|
66
76
|
|
|
67
77
|
const generate = async (sessionID: string, signal: AbortSignal) => {
|
|
68
78
|
if (!canGenerate(sessionID)) return
|
|
79
|
+
const started = Date.now()
|
|
80
|
+
const elapsed = () => Date.now() - started
|
|
81
|
+
let model: SuggestionModel | undefined
|
|
82
|
+
let failure: unknown
|
|
69
83
|
try {
|
|
70
84
|
await context.data.session.message.sync(sessionID)
|
|
71
85
|
if (signal.aborted || !canGenerate(sessionID)) return
|
|
@@ -75,19 +89,58 @@ export default Plugin.define({
|
|
|
75
89
|
const session = context.data.session.get(sessionID)
|
|
76
90
|
const location = session?.location ?? context.location ?? context.data.location.default()
|
|
77
91
|
const explicit = suggestionModel(state.model, opts.model)
|
|
78
|
-
|
|
79
|
-
? resolveExplicitModel(context.client, location, signal, explicit)
|
|
80
|
-
: resolveSmallModel(context.client, location, signal, session)
|
|
81
|
-
|
|
82
|
-
if (
|
|
92
|
+
model = explicit
|
|
93
|
+
? await resolveExplicitModel(context.client, location, signal, explicit)
|
|
94
|
+
: await resolveSmallModel(context.client, location, signal, session)
|
|
95
|
+
if (signal.aborted || !canGenerate(sessionID)) return
|
|
96
|
+
if (!model) {
|
|
97
|
+
note({ sessionID, outcome: "unavailable", durationMs: elapsed() })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const raw = await generateSuggestion(
|
|
101
|
+
context.client,
|
|
102
|
+
suggestionPrompt(opts.system, transcript),
|
|
103
|
+
model,
|
|
104
|
+
signal,
|
|
105
|
+
(error) => { failure = error; warnGeneration(error) },
|
|
106
|
+
() => canGenerate(sessionID),
|
|
107
|
+
)
|
|
108
|
+
const label = modelLabel(model)
|
|
109
|
+
if (signal.aborted) {
|
|
110
|
+
note({ sessionID, outcome: "aborted", model: label, durationMs: elapsed() })
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (failure) {
|
|
114
|
+
note({ sessionID, outcome: "error", model: label, durationMs: elapsed(), detail: errorDetail(failure) })
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if (!raw) {
|
|
118
|
+
note({ sessionID, outcome: "empty", model: label, durationMs: elapsed() })
|
|
119
|
+
return
|
|
120
|
+
}
|
|
83
121
|
const clean = normalize(raw, opts.maxChars)
|
|
84
|
-
if (!clean)
|
|
122
|
+
if (!clean) {
|
|
123
|
+
note({ sessionID, outcome: "empty", model: label, durationMs: elapsed(), chars: raw.length })
|
|
124
|
+
return
|
|
125
|
+
}
|
|
85
126
|
const previous = lastUserText(messages)
|
|
86
|
-
if (previous && isEcho(clean, previous))
|
|
127
|
+
if (previous && isEcho(clean, previous)) {
|
|
128
|
+
note({ sessionID, outcome: "echo", model: label, durationMs: elapsed(), chars: clean.length })
|
|
129
|
+
return
|
|
130
|
+
}
|
|
87
131
|
if (!canGenerate(sessionID)) return
|
|
88
132
|
setSuggestion({ sessionID, text: clean })
|
|
133
|
+
note({ sessionID, outcome: "ok", model: label, durationMs: elapsed(), chars: clean.length })
|
|
89
134
|
} catch (error) {
|
|
90
|
-
if (
|
|
135
|
+
if (signal.aborted) return
|
|
136
|
+
warnGeneration(error)
|
|
137
|
+
note({
|
|
138
|
+
sessionID,
|
|
139
|
+
outcome: "error",
|
|
140
|
+
...(model ? { model: modelLabel(model) } : {}),
|
|
141
|
+
durationMs: elapsed(),
|
|
142
|
+
detail: errorDetail(error),
|
|
143
|
+
})
|
|
91
144
|
}
|
|
92
145
|
}
|
|
93
146
|
|
|
@@ -161,18 +214,6 @@ export default Plugin.define({
|
|
|
161
214
|
const route = context.ui.router.current()
|
|
162
215
|
return route.type === "session" ? route.sessionID : undefined
|
|
163
216
|
}, () => 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
217
|
], state.enabled, async (input) => {
|
|
177
218
|
const command = parseSuggestCommand(input)
|
|
178
219
|
if (command.type === "model") {
|
|
@@ -181,8 +222,23 @@ export default Plugin.define({
|
|
|
181
222
|
context.ui.toast.show({ message: `Suggestion model: ${state.model ?? opts.model ?? "OpenCode small default"}` })
|
|
182
223
|
return
|
|
183
224
|
}
|
|
225
|
+
if (command.type === "debug") {
|
|
226
|
+
const next = command.value ?? (state.debug !== true)
|
|
227
|
+
await updateState((draft) => { draft.debug = next })
|
|
228
|
+
context.ui.toast.show({ message: `Ghost debug: ${next ? "on" : "off"}` })
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
if (command.type === "log") {
|
|
232
|
+
const override = state.model ?? opts.model
|
|
233
|
+
const summary = `suggestions ${state.enabled ? "on" : "off"}, debug ${state.debug === true ? "on" : "off"}, model ${override ?? "OpenCode small default"}`
|
|
234
|
+
void context.ui.dialog.alert({
|
|
235
|
+
title: "Ghost — recent generations",
|
|
236
|
+
message: `${summary}\n\n${formatDiagnostics(diagnostics.list())}`,
|
|
237
|
+
})
|
|
238
|
+
return
|
|
239
|
+
}
|
|
184
240
|
if (command.type === "invalid") {
|
|
185
|
-
context.ui.toast.show({ message: "Use /suggest, /suggest model provider/model,
|
|
241
|
+
context.ui.toast.show({ message: "Use /suggest, /suggest model provider/model, /suggest model clear, /suggest debug [on|off], or /suggest log", variant: "warning" })
|
|
186
242
|
return
|
|
187
243
|
}
|
|
188
244
|
cancelSuggestion()
|