@madsoftwaredev/opencode-chat-title 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +17 -15
  2. package/header.ts +16 -15
  3. package/package.json +8 -6
package/README.md CHANGED
@@ -35,15 +35,15 @@ Do not also load the original local `plugins/chat-title` copy: it has the same
35
35
 
36
36
  ## Compatibility and limitations
37
37
 
38
- The supported development baseline is **OpenCode 2.0.27**, **OpenTUI 0.5.17**,
38
+ The supported development baseline is **OpenCode 2.1.0**, **OpenTUI 0.6.6**,
39
39
  and **Solid 1.9.12**. This is not a guarantee that every later OpenCode layout
40
40
  will work. The package exports `./tui` only and runs in the OpenCode terminal,
41
41
  not in the web or desktop UI. Its Node `>=22` engine metadata does not make the
42
42
  TypeScript/TSX entrypoint a standalone Node program.
43
43
 
44
44
  OpenCode V2 publishes no slot above the transcript. This plugin uses the exposed
45
- OpenTUI renderer and Yoga layout tree, not a browser DOM, to insert the header
46
- into the session column that owns the `session.composer.top` slot.
45
+ OpenTUI render tree and renderable style getters, not a browser DOM, to insert
46
+ the header into the session column that owns the `session.composer.top` slot.
47
47
 
48
48
  Sidebar visibility is tracked through the documented `sidebar.content` slot,
49
49
  which mounts while the sidebar renders. Replacing that slot can interfere with
@@ -52,16 +52,17 @@ this visibility signal.
52
52
  The adapter matches the layout inspected in OpenCode 2.0.27: exactly three rows
53
53
  for the transcript (a box wrapping the scrollbox), the one-row "jump to latest"
54
54
  bar, and the composer branch containing the plugin anchor. Unrecognized layouts
55
- are left alone and reported with a warning toast. OpenCode updates or other
56
- plugins that change that tree may require an adapter update. No installed
55
+ are left alone and reported with a warning toast. Errors during initial layout
56
+ discovery also leave the transcript intact rather than crashing the composer
57
+ slot. OpenCode updates or other plugins that change that tree may require an
58
+ adapter update. No installed
57
59
  binaries or OpenCode source files are patched.
58
60
 
59
- The original local implementation records two host behaviors: `requestRender()`
60
- is needed for a newly inserted row to be measured, and renderables emit `resize`
61
- for Yoga layout changes (`resized` is a different root-only event). It also
62
- records that calling Yoga's `markDirty()` crashes the native renderer; this
63
- plugin does not call it. These are implementation dependencies, not public
64
- layout guarantees.
61
+ Version 0.1.1 replaces the removed `getLayoutNode()` API with renderable style
62
+ getters. The header requests a frame after insertion and uses `onSizeChange` to
63
+ refit its text after native layout and terminal resizes. It never accesses or
64
+ modifies a raw Yoga node. Host structure remains an implementation dependency,
65
+ not a public layout guarantee.
65
66
 
66
67
  OpenCode supplies the UI runtime and rewrites host imports in `tui.tsx`.
67
68
  `header.ts` and `title.ts` deliberately avoid runtime imports of UI packages so
@@ -72,14 +73,15 @@ the adapter receives the host's own constructors.
72
73
  Install workspace dependencies once from the repository root, then run:
73
74
 
74
75
  ```sh
75
- bun test ./packages/chat-title
76
+ bun run --cwd packages/chat-title test
76
77
  bun run --cwd packages/chat-title typecheck
77
78
  bun build ./packages/chat-title/tui.tsx --target=bun --external '@opencode/*' --external '@opentui/*' --external 'solid-js'
78
79
  ```
79
80
 
80
- Tests cover title fitting, render-tree rejection, header insertion, resizing,
81
- and disposal using fake renderables. They do not prove the actual OpenCode
82
- layout, sidebar visibility lifecycle, or persisted settings in a running TUI.
81
+ Unit tests cover title fitting and render-tree matching. Headless OpenTUI 0.6.6
82
+ integration tests cover rendering, title updates, narrow-terminal resizing,
83
+ sidebar visibility, saved-toggle wiring, disposal, and safe layout rejection.
84
+ They use the inspected host shape, not a live installed OpenCode release.
83
85
  The build command checks transpilation, not host integration. Development
84
86
  dependencies are not needed to load the plugin in OpenCode.
85
87
 
package/header.ts CHANGED
@@ -18,16 +18,14 @@ export type YogaLayout = {
18
18
  getHeight(): YogaValue
19
19
  }
20
20
 
21
- export type Node = {
21
+ export type Node = YogaLayout & {
22
22
  readonly isDestroyed: boolean
23
23
  readonly parent: Node | null
24
24
  readonly width: number
25
25
  getChildren(): Node[]
26
- getLayoutNode(): YogaLayout
26
+ onSizeChange?: () => void
27
27
  add(child: unknown, index?: number): number
28
28
  destroy(): void
29
- on(event: string, handler: () => void): void
30
- off(event: string, handler: () => void): void
31
29
  }
32
30
 
33
31
  export type LabelNode = Node & { content: string }
@@ -67,20 +65,19 @@ function isColumn(node: Node, branch: Node, core: Core): boolean {
67
65
  if (!(children[0] instanceof core.BoxRenderable)) return false
68
66
  if (!(children[0].getChildren()[0] instanceof core.ScrollBoxRenderable)) return false
69
67
 
70
- const jump = children[1].getLayoutNode()
68
+ const jump = children[1]
71
69
  const jumpHeight = jump.getHeight()
72
70
  if (jumpHeight.unit !== core.Yoga.Unit.Point || jumpHeight.value !== 1) return false
73
71
  if (jump.getFlexShrink() !== 0) return false
74
72
 
75
- const layout = node.getLayoutNode()
76
- const padding = layout.getPadding(core.Yoga.Edge.Bottom)
73
+ const padding = node.getPadding(core.Yoga.Edge.Bottom)
77
74
  if (padding.unit !== core.Yoga.Unit.Point || padding.value !== 1) return false
78
- if (layout.getFlexGrow() !== 1) return false
75
+ if (node.getFlexGrow() !== 1) return false
79
76
 
80
77
  // The column sits beside the sidebar, so its own parent is a row.
81
78
  const parent = node.parent
82
79
  if (!parent) return false
83
- return parent.getLayoutNode().getFlexDirection() === core.Yoga.FlexDirection.Row
80
+ return parent.getFlexDirection() === core.Yoga.FlexDirection.Row
84
81
  }
85
82
 
86
83
  /**
@@ -101,7 +98,13 @@ export function findSessionColumn(anchor: Node, core: Core): Node | undefined {
101
98
  * flexbox, so the header takes a row from the transcript instead of covering it.
102
99
  */
103
100
  export function attachHeader(options: AttachOptions): Attachment | undefined {
104
- const column = findSessionColumn(options.anchor, options.core)
101
+ let column: Node | undefined
102
+ try {
103
+ column = findSessionColumn(options.anchor, options.core)
104
+ } catch (error) {
105
+ console.warn("[chat-title] Unsupported render tree; the chat title is hidden", error)
106
+ return undefined
107
+ }
105
108
  if (!column) return undefined
106
109
 
107
110
  const header = new options.core.BoxRenderable(options.renderer, {
@@ -121,8 +124,7 @@ export function attachHeader(options: AttachOptions): Attachment | undefined {
121
124
  })
122
125
  header.add(label)
123
126
  column.add(header, 0)
124
- // add() marks the Yoga tree dirty and requests a frame, so the header is
125
- // measured on the next pass and "resize" fires with a real width.
127
+ // Request a frame so native layout measures the row and invokes onSizeChange.
126
128
  options.renderer.requestRender?.()
127
129
 
128
130
  let disposed = false
@@ -138,8 +140,7 @@ export function attachHeader(options: AttachOptions): Attachment | undefined {
138
140
  label.content = next
139
141
  }
140
142
 
141
- // "resize" is the event Renderable emits when Yoga gives it a new size.
142
- header.on("resize", refresh)
143
+ header.onSizeChange = refresh
143
144
  refresh()
144
145
 
145
146
  return {
@@ -147,7 +148,7 @@ export function attachHeader(options: AttachOptions): Attachment | undefined {
147
148
  dispose() {
148
149
  if (disposed) return
149
150
  disposed = true
150
- header.off("resize", refresh)
151
+ if (!header.isDestroyed) header.onSizeChange = undefined
151
152
  // The host detaches children without destroying them, so drop the label
152
153
  // first and then the row itself.
153
154
  if (!label.isDestroyed) label.destroy()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madsoftwaredev/opencode-chat-title",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenCode V2 TUI plugin that shows the session title above the transcript while the sidebar is hidden.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -35,19 +35,21 @@
35
35
  "node": ">=22"
36
36
  },
37
37
  "scripts": {
38
- "test": "bun test",
38
+ "test": "bun test --preload @opentui/solid/preload",
39
39
  "typecheck": "tsc --noEmit"
40
40
  },
41
41
  "dependencies": {
42
- "@opencode/plugin": "^2.0.27"
42
+ "@opencode/plugin": "^2.1.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@opentui/core": ">=0.5.17",
46
- "@opentui/solid": ">=0.5.17",
45
+ "@opentui/core": ">=0.6.6",
46
+ "@opentui/solid": ">=0.6.6",
47
47
  "solid-js": ">=1.9.12"
48
48
  },
49
49
  "devDependencies": {
50
- "@types/bun": "1.3.9",
50
+ "@opentui/core": "0.8.1",
51
+ "@opentui/solid": "0.8.1",
52
+ "@types/bun": "1.4.0",
51
53
  "typescript": "5.8.2"
52
54
  },
53
55
  "publishConfig": {