opencode-ghost 0.2.2 → 0.2.4

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
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.4] - 2026-09-25
11
+
12
+ ### Fixed
13
+
14
+ - Long next-message suggestions now wrap onto extra rows and grow the prompt
15
+ box instead of being clipped at its right edge, so the whole suggestion stays
16
+ readable.
17
+ - Slash commands and skills only suggest their accepted argument flags (from
18
+ `argHints`); command and skill descriptions are no longer echoed as if they
19
+ were arguments. Commands without hints show no ghost.
20
+
21
+ ### Changed
22
+
23
+ - Ghost styling: slash-command ghosts use the theme accent color, and every
24
+ other ghost is dimmed and clipped to the prompt box so it cannot be mistaken
25
+ for typed text.
26
+ ## [0.2.3] - 2026-09-25
27
+
28
+ ### Removed
29
+
30
+ - The temporary `GHOST_DEBUG` diagnostic toasts introduced while localizing
31
+ the in-box ghost overlay.
10
32
  ## [0.2.2] - 2026-09-25
11
33
 
12
34
  ### Changed
@@ -17,8 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
39
  - Typing ghosts are positioned from absolute screen coordinates (the prompt
18
40
  editor's `screenX/screenY` plus the caret's visual column/row) instead of a
19
41
  relative slot offset, and clear reliably when the input no longer matches.
20
- - `GHOST_DEBUG=1` makes the plugin emit diagnostic toasts about ghost
21
- placement (temporary, for localization feedback).
22
42
  ## [0.2.1] - 2026-09-24
23
43
 
24
44
  ### Changed
@@ -78,7 +98,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
78
98
  - Options for `model`, `acceptKeys`, `maxChars`, `idleDelayMs`,
79
99
  `recentMessages` and `system`.
80
100
 
81
- [Unreleased]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.2...HEAD
101
+ [Unreleased]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.4...HEAD
102
+ [0.2.4]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.3...v0.2.4
103
+ [0.2.3]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.2...v0.2.3
82
104
  [0.2.2]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.1...v0.2.2
83
105
  [0.2.1]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.0...v0.2.1
84
106
  [0.2.0]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.1.1...v0.2.0
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 opencode-ghost contributors
3
+ Copyright (c) 2026 Ozan Dogrultan
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -17,14 +17,22 @@ dimmed ghost text you accept with <kbd>Tab</kbd> (or <kbd>→</kbd>).
17
17
  default) for one line that sounds like you.
18
18
  - **Ghost text, `Tab` to accept.** The suggestion renders under the prompt;
19
19
  `Tab` or `→` drops it into the input so you can edit and send.
20
- - **Slash-command argument ghosts while typing.** After `/name `, argument
21
- option hints (from `argHints` or learned from builtins) render inside the
22
- prompt input box, right after the caret, like Claude Code's inline ghosts.
23
- Tab completes the next argument. While the command name is still being typed,
24
- opencode's own slash menu stays in charge. Purely local — no model calls.
20
+ - **Slash-command argument ghosts while typing.** After `/name `, accepted
21
+ argument options (from `argHints`) render inside the prompt input box, right
22
+ after the caret, as `[a | b | c]` like Claude Code. Running out of options or
23
+ naming a command with no configured hints shows no ghost — command and skill
24
+ descriptions are never suggested as if they were arguments. Tab completes the
25
+ next argument. While the command name is still being typed, opencode's own
26
+ slash menu stays in charge. Purely local — no model calls.
25
27
  - **History ghost while typing.** If the line you are typing is a prefix of a
26
28
  message you already sent in the same session, the rest of it is ghosted in
27
29
  the box as well; Tab pulls the full line back. Also purely local.
30
+ - **Distinct ghost styling.** Slash-command ghosts use the theme accent color;
31
+ all other ghosts (history completion, next-message suggestion) are dimmed so
32
+ they cannot be mistaken for typed text. A next-message suggestion that is
33
+ wider than the prompt wraps onto extra rows and grows the prompt box (up to
34
+ five rows) so it stays fully readable; other ghosts are clipped to the box and
35
+ never spill past it.
28
36
  - **`/suggest` toggles it.** State is stored in the plugin KV.
29
37
 
30
38
  ## Requirements
@@ -97,7 +105,7 @@ Pass an options object as the second element of the plugin tuple:
97
105
  | `system` | `string` | built-in | Override the system prompt sent to the suggestion model. |
98
106
  | `backOnEmptyLeft` | `boolean` | `false` | Return to the home screen with Left when the prompt is empty. |
99
107
  | `internalSessionMarkerDir` | `string` | unset | Optional extra crash-recovery tracking. A configuration-free sweep already reaps leftover hidden sessions, so this is only an additional safety net. |
100
- | `argHints` | `Record<string, string[]>` | `{}` | Argument option lists per slash command, shown as `[a \| b \| c]` after the command name and completed with Tab, e.g. `"/keepwarm": ["6h", "always", "off", "status"]`. |
108
+ | `argHints` | `Record<string, string[]>` | `{}` | Argument option lists per slash command, shown as `[a \| b \| c]` after a complete `/name ` and completed with Tab, e.g. `"/keepwarm": ["6h", "always", "off", "status"]`. Commands without an entry suggest no arguments. |
101
109
 
102
110
  ## Commands
103
111
 
@@ -148,6 +156,12 @@ bun test
148
156
  No build step: like other opencode TUI plugins, the package ships TSX source and
149
157
  opencode's runtime transpiles it.
150
158
 
159
+ ## Contributing
160
+
161
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the check suite and commit
162
+ conventions, [AGENTS.md](AGENTS.md) for the design rules, and
163
+ [CHANGELOG.md](CHANGELOG.md) for what changed.
164
+
151
165
  ## License
152
166
 
153
167
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ghost",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Claude Code-style next-prompt suggestions in the opencode TUI, shown as dimmed ghost text and accepted with Tab. An opencode plugin.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,6 +13,15 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/ozandogrultan/opencode-ghost/issues"
15
15
  },
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "tui",
20
+ "ghost-text",
21
+ "autosuggest",
22
+ "prompt",
23
+ "suggestion"
24
+ ],
16
25
  "exports": {
17
26
  "./tui": "./src/tui.tsx"
18
27
  },
@@ -25,25 +34,19 @@
25
34
  "publishConfig": {
26
35
  "access": "public"
27
36
  },
37
+ "packageManager": "bun@1.4.2",
38
+ "engines": {
39
+ "opencode": ">=1.18.0"
40
+ },
28
41
  "scripts": {
29
- "typecheck": "tsc --noEmit",
42
+ "prepare": "husky",
30
43
  "test": "bun test && bash tests/changelog.sh",
31
44
  "test:unit": "bun test",
32
45
  "test:changelog": "bash tests/changelog.sh",
46
+ "typecheck": "tsc --noEmit",
47
+ "lint:sh": "for script in install.sh tests/*.sh scripts/*.sh; do bash -n \"$script\" || exit; done",
33
48
  "prepublishOnly": "bun run test && bun run typecheck"
34
49
  },
35
- "engines": {
36
- "opencode": ">=1.18.0"
37
- },
38
- "keywords": [
39
- "opencode",
40
- "opencode-plugin",
41
- "tui",
42
- "ghost-text",
43
- "autosuggest",
44
- "prompt",
45
- "suggestion"
46
- ],
47
50
  "dependencies": {
48
51
  "solid-js": "^1.9.12"
49
52
  },
@@ -52,11 +55,14 @@
52
55
  "solid-js": "*"
53
56
  },
54
57
  "devDependencies": {
58
+ "@commitlint/cli": "^21.2.2",
59
+ "@commitlint/config-conventional": "^21.2.2",
55
60
  "@opencode-ai/plugin": "1.18.25",
56
61
  "@opentui/core": "0.4.5",
57
62
  "@opentui/keymap": "0.4.5",
58
63
  "@opentui/solid": "0.4.5",
59
64
  "@types/node": "24.12.2",
65
+ "husky": "^9.1.7",
60
66
  "typescript": "5.8.2"
61
67
  }
62
68
  }
package/src/completion.ts CHANGED
@@ -16,8 +16,6 @@ export type Completion = {
16
16
  insert?: string
17
17
  /** Argument options shown as `[a | b]`, filtered by the word in progress. */
18
18
  args?: string[]
19
- /** Non-insertable hint text shown when nothing can be completed. */
20
- hint?: string
21
19
  }
22
20
 
23
21
  /** Parse leading `/name args...` from the input; `args` is "" until a space exists. */
@@ -54,7 +52,6 @@ export function completeCommand(
54
52
  const hints = argHints[lower]
55
53
  return {
56
54
  args: hints ? [...hints] : undefined,
57
- hint: hints ? undefined : exact.description,
58
55
  insert: `/${exact.name} `,
59
56
  }
60
57
  }
@@ -66,18 +63,13 @@ export function completeCommand(
66
63
 
67
64
  // `/name args...`: complete the in-progress argument word against hints.
68
65
  const hintList = argHints[lower]
69
- if (hintList) {
70
- const matches = matchOptions(parsed.args, hintList)
71
- if (matches) {
72
- const completed = parsed.args + matches[0].slice(parsed.args.length)
73
- return {
74
- args: matches,
75
- ghost: matches[0].slice(parsed.args.length),
76
- insert: `/${parsed.name} ${completed} `,
77
- }
78
- }
79
- return undefined
66
+ if (!hintList) return undefined
67
+ const matches = matchOptions(parsed.args, hintList)
68
+ if (!matches) return undefined
69
+ const completed = parsed.args + matches[0].slice(parsed.args.length)
70
+ return {
71
+ args: matches,
72
+ ghost: matches[0].slice(parsed.args.length),
73
+ insert: `/${parsed.name} ${completed} `,
80
74
  }
81
- if (exact && parsed.args === "") return { hint: exact.description }
82
- return undefined
83
75
  }
package/src/text.ts CHANGED
@@ -33,6 +33,34 @@ export function normalize(raw: string, maxChars: number): string | undefined {
33
33
  return cleaned.slice(0, maxChars - 1).trimEnd() + "…"
34
34
  }
35
35
 
36
+ /**
37
+ * Number of visual rows `text` occupies when word-wrapped to `width` columns.
38
+ * Mirrors the greedy word wrap the prompt renderer uses, so a ghost can reserve
39
+ * exactly as many rows as it needs. Long words break at the column boundary.
40
+ */
41
+ export function wrapCount(text: string, width: number): number {
42
+ if (width <= 0) return 1
43
+ let lines = 1
44
+ let col = 0
45
+ for (const word of text.split(/\s+/).filter(Boolean)) {
46
+ let length = word.length
47
+ if (col > 0) {
48
+ if (col + 1 + length <= width) {
49
+ col += 1 + length
50
+ continue
51
+ }
52
+ lines += 1
53
+ col = 0
54
+ }
55
+ while (length > width) {
56
+ lines += 1
57
+ length -= width
58
+ }
59
+ col = length
60
+ }
61
+ return lines
62
+ }
63
+
36
64
  export function isEcho(suggestion: string, previous: string): boolean {
37
65
  const strip = (value: string) =>
38
66
  value
package/src/tui.tsx CHANGED
@@ -4,7 +4,7 @@ import { createSignal } from "solid-js"
4
4
  import * as fs from "node:fs"
5
5
  import * as path from "node:path"
6
6
  import { resolveOptions, type PromptSuggestOptions } from "./options"
7
- import { clip, isEcho, normalize, parseModel } from "./text"
7
+ import { clip, isEcho, normalize, parseModel, wrapCount } from "./text"
8
8
  import { BUILTIN_COMMANDS } from "./builtins"
9
9
  import { completeCommand, type CommandPool } from "./completion"
10
10
 
@@ -17,9 +17,12 @@ const ORPHAN_SWEEP_INTERVAL_MS = 30_000
17
17
  const ORPHAN_MIN_AGE_MS = 15_000
18
18
  const MAX_MESSAGE_CHARS = 800
19
19
  const MAX_TRANSCRIPT_CHARS = 6000
20
+ // The prompt editor grows to fit a wrapped next-message suggestion only up to
21
+ // this many rows; longer text is clipped. The host caps its editor at >= 6 rows
22
+ // by default, so 5 stays inside that ceiling.
23
+ const MAX_GHOST_LINES = 5
20
24
  const ENABLED_KEY = "ghost.enabled"
21
25
  const MODEL_KEY = "ghost.model"
22
- const GHOST_DEBUG = Boolean(process.env.GHOST_DEBUG)
23
26
 
24
27
  const HIDDEN_TOOLS = {
25
28
  bash: false,
@@ -342,10 +345,21 @@ const tui: TuiPlugin = async (api, rawOptions) => {
342
345
  // walking the layout tree). Uses the normal render pipeline, so it is visible
343
346
  // wherever the prompt is. When the editor or container cannot be located
344
347
  // (host layout change) completion falls back to the hint row.
348
+ type GhostColor = typeof api.theme.current.textMuted
345
349
  const [inBox, setInBox] = createSignal<
346
- { sessionID: string; x: number; y: number; text: string } | undefined
350
+ {
351
+ sessionID: string
352
+ x: number
353
+ y: number
354
+ text: string
355
+ width: number
356
+ height: number
357
+ wrap: boolean
358
+ color: GhostColor
359
+ opacity: number
360
+ } | undefined
347
361
  >()
348
- let slotNode: { screenX?: number; screenY?: number } | undefined
362
+ let slotNode: { screenX?: number; screenY?: number; width?: number } | undefined
349
363
  // Locate the prompt's editor renderable inside the layout tree.
350
364
  const findTextarea = (): unknown => {
351
365
  const root = (api.renderer as unknown as { root?: unknown }).root as
@@ -361,6 +375,8 @@ const tui: TuiPlugin = async (api, rawOptions) => {
361
375
  visualCursor?: { visualRow?: number; visualCol?: number }
362
376
  screenX?: number
363
377
  screenY?: number
378
+ width?: number
379
+ minHeight?: number
364
380
  getChildren?: () => unknown[]
365
381
  }
366
382
  | undefined
@@ -372,10 +388,38 @@ const tui: TuiPlugin = async (api, rawOptions) => {
372
388
  return undefined
373
389
  }
374
390
 
375
- const placeInBoxGhost = (sessionID: string, text: string): boolean => {
391
+ // A wrapped next-message suggestion is shown by growing the host editor's
392
+ // min height to the number of rows it needs, so the prompt box grows instead
393
+ // of clipping the text. The host sets minHeight once at creation (not
394
+ // reactively), so the value we set survives until we reset it.
395
+ let heightManaged: { minHeight?: number } | undefined
396
+ const resetEditorHeight = () => {
397
+ if (!heightManaged) return
398
+ try {
399
+ heightManaged.minHeight = 1
400
+ } catch {
401
+ // The editor may have been recreated or destroyed; best effort.
402
+ }
403
+ heightManaged = undefined
404
+ }
405
+
406
+ const placeInBoxGhost = (
407
+ sessionID: string,
408
+ text: string,
409
+ color: GhostColor,
410
+ opacity: number,
411
+ wrap: boolean,
412
+ ): boolean => {
413
+ resetEditorHeight()
376
414
  setInBox(undefined)
377
415
  const textarea = findTextarea() as
378
- | { screenX?: unknown; screenY?: unknown; visualCursor?: { visualRow?: number; visualCol?: number } }
416
+ | {
417
+ screenX?: unknown
418
+ screenY?: unknown
419
+ width?: unknown
420
+ minHeight?: number
421
+ visualCursor?: { visualRow?: number; visualCol?: number }
422
+ }
379
423
  | undefined
380
424
  if (
381
425
  !textarea ||
@@ -385,6 +429,7 @@ const tui: TuiPlugin = async (api, rawOptions) => {
385
429
  return false
386
430
  }
387
431
  const cursor = textarea.visualCursor ?? {}
432
+ const visualCol = typeof cursor.visualCol === "number" ? cursor.visualCol : 0
388
433
  // Overlay coordinates are relative to the slot container (it anchors the
389
434
  // absolute overlay), so subtract its screen origin from the editor's.
390
435
  if (
@@ -394,9 +439,30 @@ const tui: TuiPlugin = async (api, rawOptions) => {
394
439
  ) {
395
440
  return false
396
441
  }
397
- const x = textarea.screenX - slotNode.screenX + (typeof cursor.visualCol === "number" ? cursor.visualCol : 0)
442
+ const x = textarea.screenX - slotNode.screenX + visualCol
398
443
  const y = textarea.screenY - slotNode.screenY + (typeof cursor.visualRow === "number" ? cursor.visualRow : 0)
399
- setInBox({ sessionID, x, y, text })
444
+ // Keep the ghost inside the prompt box: clip it to the editor's remaining
445
+ // width so it never spills past the box's right edge.
446
+ const editorWidth = typeof textarea.width === "number" ? textarea.width : undefined
447
+ const width =
448
+ editorWidth !== undefined
449
+ ? editorWidth - visualCol
450
+ : typeof slotNode.width === "number"
451
+ ? slotNode.width - x
452
+ : undefined
453
+ if (width === undefined || width <= 0) return false
454
+ let height = 1
455
+ if (wrap) {
456
+ height = Math.max(1, Math.min(MAX_GHOST_LINES, wrapCount(text, width)))
457
+ try {
458
+ textarea.minHeight = height
459
+ heightManaged = textarea
460
+ } catch {
461
+ // If the host editor cannot be resized, fall back to a one-row clip.
462
+ height = 1
463
+ }
464
+ }
465
+ setInBox({ sessionID, x, y, text, width, height, wrap: wrap && height > 1, color, opacity })
400
466
  return true
401
467
  }
402
468
 
@@ -410,47 +476,52 @@ const tui: TuiPlugin = async (api, rawOptions) => {
410
476
  setCompletion(undefined)
411
477
  setInBox(undefined)
412
478
  }
479
+ resetEditorHeight()
413
480
  return
414
481
  }
415
482
  const input = promptRef?.current.input ?? ""
416
483
  if (poolFetchedAt && Date.now() - poolFetchedAt > POOL_TTL_MS) void refreshPool()
484
+ const isSlash = input.startsWith("/")
485
+ const startsEmpty = !input.trim()
417
486
  // `/name` with no space yet is opencode's native slash menu; the plugin
418
487
  // stays out of its way. Our slash completion only covers `/name args...`.
419
- const found = !input.startsWith("/")
488
+ const found = !isSlash
420
489
  ? historyCandidate(sessionID, input)
421
490
  : input.includes(" ")
422
491
  ? completeCommand(input, commandPool, opts.argHints)
423
492
  : undefined
424
493
  let display = ""
425
494
  let insert = ""
495
+ let wrap = false
496
+ // Slash-command completions are tinted like commands and kept at full
497
+ // strength; every other ghost (history, next-message suggestion) is dimmed
498
+ // so it cannot be mistaken for typed text.
499
+ const color = isSlash ? api.theme.current.accent : api.theme.current.textMuted
500
+ const opacity = isSlash ? 1 : 0.6
426
501
  if (found) {
427
502
  const args = "args" in found ? found.args : undefined
428
- const hint = "hint" in found ? found.hint : undefined
429
503
  insert = found.insert ?? ""
430
- display = args && args.length > 0 ? `[${args.join(" | ")}]` : (found.ghost ?? hint ?? "")
431
- } else if (!input.trim()) {
504
+ display = args && args.length > 0 ? `[${args.join(" | ")}]` : (found.ghost ?? "")
505
+ } else if (startsEmpty) {
432
506
  // Empty prompt: the model's next-message suggestion is ghosted in the
433
- // box as well (the accept layer's `accept()` handles Tab for it).
507
+ // box as well (the accept layer's `accept()` handles Tab for it). It is
508
+ // the only ghost allowed to wrap and grow the prompt box, since it can be
509
+ // longer than the visible width and there is no typed text to disturb.
434
510
  const next = ghost()
435
- if (next && next.sessionID === sessionID) display = next.text
511
+ if (next && next.sessionID === sessionID) {
512
+ display = next.text
513
+ wrap = true
514
+ }
436
515
  }
437
- // Include the display in the key: a fresh suggestion for an unchanged
438
- // (empty) input must still be re-placed.
439
- const key = `${sessionID}\u0000${input}\u0000${display}`
516
+ // Include the display and renderer width in the key: a fresh suggestion for
517
+ // an unchanged (empty) input must be re-placed, and a resize must re-wrap.
518
+ const key = `${sessionID}\u0000${input}\u0000${display}\u0000${api.renderer.width}`
440
519
  if (completionKey === key) return
441
520
  completionKey = key
442
- const drawn = display ? placeInBoxGhost(sessionID, display) : false
443
- if (!drawn) setInBox(undefined)
444
- if (GHOST_DEBUG && (display || input.startsWith("/"))) {
445
- const overlay = inBox()
446
- try {
447
- api.ui.toast({
448
- title: "ghost-debug",
449
- message: `display=${display.slice(0, 20) || "∅"} drawn=${drawn} slot=${slotNode ? `${slotNode.screenX}/${slotNode.screenY}` : "n"} box=${overlay ? `${overlay.x}/${overlay.y}` : "∅"}`,
450
- })
451
- } catch {
452
- // ignore
453
- }
521
+ const drawn = display ? placeInBoxGhost(sessionID, display, color, opacity, wrap) : false
522
+ if (!drawn) {
523
+ setInBox(undefined)
524
+ resetEditorHeight()
454
525
  }
455
526
  setCompletion(
456
527
  found
@@ -668,6 +739,9 @@ const tui: TuiPlugin = async (api, rawOptions) => {
668
739
  if (currentInput().trim()) return false
669
740
  promptRef.set({ input: current.text, parts: [] })
670
741
  promptRef.focus()
742
+ completionKey = ""
743
+ setInBox(undefined)
744
+ resetEditorHeight()
671
745
  showGhost(undefined)
672
746
  return true
673
747
  }
@@ -718,7 +792,7 @@ const tui: TuiPlugin = async (api, rawOptions) => {
718
792
  return (
719
793
  <box
720
794
  position="relative"
721
- ref={(ref: { screenX?: number; screenY?: number }) => {
795
+ ref={(ref: { screenX?: number; screenY?: number; width?: number }) => {
722
796
  slotNode = ref
723
797
  }}
724
798
  >
@@ -739,8 +813,23 @@ const tui: TuiPlugin = async (api, rawOptions) => {
739
813
  }}
740
814
  />
741
815
  {overlay() ? (
742
- <box position="absolute" left={overlay()!.x} top={overlay()!.y} zIndex={100}>
743
- <text fg={api.theme.current.textMuted}>{overlay()!.text}</text>
816
+ <box
817
+ position="absolute"
818
+ left={overlay()!.x}
819
+ top={overlay()!.y}
820
+ width={overlay()!.width}
821
+ height={overlay()!.height}
822
+ overflow="hidden"
823
+ opacity={overlay()!.opacity}
824
+ zIndex={100}
825
+ >
826
+ <text
827
+ fg={overlay()!.color}
828
+ wrapMode={overlay()!.wrap ? "word" : "none"}
829
+ truncate={!overlay()!.wrap}
830
+ >
831
+ {overlay()!.text}
832
+ </text>
744
833
  </box>
745
834
  ) : undefined}
746
835
  </box>
@@ -782,6 +871,7 @@ const tui: TuiPlugin = async (api, rawOptions) => {
782
871
  clearInterval(sweepTimer)
783
872
  clearInterval(pollTimer)
784
873
  setInBox(undefined)
874
+ resetEditorHeight()
785
875
  offIdle()
786
876
  offStatus()
787
877
  backLayer()