@madsoftwaredev/opencode-sidebar-resize 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/package.json +56 -4
- package/sidebar.ts +161 -0
- package/tui.tsx +107 -0
- package/width.ts +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MadSoftwareDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @madsoftwaredev/opencode-sidebar-resize
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Resizes the **whole existing right-hand session sidebar**, including its title,
|
|
4
|
+
context, MCP, subagents, usage, and footer. It does not create a panel or replace
|
|
5
|
+
sidebar content. The left-hand session tabs are unchanged.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add this CLI-only plugin to your global `~/.config/opencode/cli.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"plugins": ["@madsoftwaredev/opencode-sidebar-resize"]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Merge the entry into your existing `plugins` list, then restart the OpenCode TUI.
|
|
18
|
+
A server restart or server-side plugin reload does not load a TUI entrypoint.
|
|
19
|
+
Do not also load the original local `plugins/sidebar-resize` copy: it has the
|
|
20
|
+
same `sidebar-resize` plugin ID and storage keys.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
- Drag the sidebar's **left edge**. Its two-cell handle highlights on hover.
|
|
25
|
+
- Use `/sidebar-width 60` to set the width in terminal columns, or select
|
|
26
|
+
**Set sidebar width** from `Ctrl+P`.
|
|
27
|
+
- Use `/sidebar-width reset` to restore the original 42-column width.
|
|
28
|
+
- Width is saved across restarts and synced across TUI instances.
|
|
29
|
+
- Reserves 64 columns for the transcript when displayed side by side, where
|
|
30
|
+
available. OpenCode's existing narrow-terminal overlay behavior is preserved.
|
|
31
|
+
- Terminal resizing temporarily clamps the sidebar without changing the saved
|
|
32
|
+
preference; the preferred width returns when space allows.
|
|
33
|
+
|
|
34
|
+
## Compatibility and limitations
|
|
35
|
+
|
|
36
|
+
The supported development baseline is **OpenCode 2.1.0**, **OpenTUI 0.6.6**,
|
|
37
|
+
and **Solid 1.9.12**. This is not a guarantee that every later OpenCode layout
|
|
38
|
+
will work. The package exports `./tui` only and runs in the OpenCode terminal,
|
|
39
|
+
not in the web or desktop UI. Its Node `>=22` engine metadata does not make the
|
|
40
|
+
TypeScript/TSX entrypoint a standalone Node program.
|
|
41
|
+
|
|
42
|
+
OpenCode V2 has no public sidebar-width API. This plugin uses the exposed
|
|
43
|
+
OpenTUI renderer and Yoga layout tree, not a browser DOM, to locate and resize
|
|
44
|
+
the original sidebar and its host pane from a `sidebar.footer` slot anchor.
|
|
45
|
+
The adapter was inspected against OpenCode 2.1.0-era checkout `32b41bab` and requires the
|
|
46
|
+
same padded, 42-column sidebar shape, heading, scrollbox, and footer branch.
|
|
47
|
+
Unrecognized layouts are left unchanged with a warning. OpenCode updates or
|
|
48
|
+
other plugins that replace the footer or alter that tree can prevent attachment.
|
|
49
|
+
No installed binaries or OpenCode source files are patched.
|
|
50
|
+
|
|
51
|
+
OpenTUI 0.6.6 removed `getLayoutNode()`. The adapter uses Renderable's direct
|
|
52
|
+
`getWidth()`, `getPadding()`, and `getPositionType()` style getters instead;
|
|
53
|
+
`getLayout()` returns computed geometry and is not a replacement for these getters.
|
|
54
|
+
|
|
55
|
+
The drag fallback temporarily wraps the renderer root's `processMouseEvent`
|
|
56
|
+
to catch drags whose first move jumps past the handle. Cleanup restores that
|
|
57
|
+
handler if it still owns it, removes the handle, and restores the original
|
|
58
|
+
widths. This relies on host internals and can interact with other mouse-handler
|
|
59
|
+
wrappers.
|
|
60
|
+
|
|
61
|
+
Existing sections keep their own text-formatting limits; widening the container
|
|
62
|
+
does not remove truncation explicitly applied by another plugin. This plugin
|
|
63
|
+
does not force a hidden sidebar to open or change OpenCode's overlay threshold.
|
|
64
|
+
|
|
65
|
+
OpenCode supplies the UI runtime and rewrites host imports in `tui.tsx`.
|
|
66
|
+
`sidebar.ts` keeps its OpenTUI imports type-only and receives constructors from
|
|
67
|
+
that entrypoint; `width.ts` has no UI runtime imports.
|
|
68
|
+
|
|
69
|
+
## Development checks
|
|
70
|
+
|
|
71
|
+
Install workspace dependencies once from the repository root, then run:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
bun test --preload @opentui/solid/preload ./packages/sidebar-resize
|
|
75
|
+
bun run --cwd packages/sidebar-resize typecheck
|
|
76
|
+
bun build ./packages/sidebar-resize/tui.tsx --target=bun --external '@opencode/*' --external '@opentui/*' --external 'solid-js'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The preload is required for the Solid JSX integration tests. Tests use the
|
|
80
|
+
native OpenTUI test renderer with a representative sidebar tree and a fake
|
|
81
|
+
plugin context to check whole-sidebar resizing, dragging, persistence success
|
|
82
|
+
and failure, terminal resizing, and cleanup. A runtime-import check ensures the
|
|
83
|
+
TypeScript adapter does not import UI packages at runtime. These checks do not
|
|
84
|
+
by themselves prove the actual layout or plugin loader in a running OpenCode TUI. The build
|
|
85
|
+
command checks transpilation, not host integration. Development dependencies
|
|
86
|
+
are not needed to load the plugin in OpenCode.
|
|
87
|
+
|
|
88
|
+
### Actual-host verification
|
|
89
|
+
|
|
90
|
+
On 2026-10-20, the same adapter was checked with the installed **OpenCode 2.1.0**
|
|
91
|
+
binary in an isolated headless PTY at **180 × 50**, using its actual render tree.
|
|
92
|
+
The local copy was auto-discovered through a controlled global `plugins` symlink,
|
|
93
|
+
not an explicit CLI package entry. This verifies the adapter, not npm package loading.
|
|
94
|
+
Disposable HOME/XDG directories, a private `--standalone` server and database, and a
|
|
95
|
+
fake local provider endpoint were used. Sounds and notifications were disabled;
|
|
96
|
+
no prompts or generations were sent and no existing user sessions changed.
|
|
97
|
+
|
|
98
|
+
| Action | Sidebar width | Session pane width | Handle x |
|
|
99
|
+
| --- | ---: | ---: | ---: |
|
|
100
|
+
| Initial layout | 42 | 138 | 138 |
|
|
101
|
+
| Dispatch `sidebar-resize.width` with `60` | 60 | 120 | 120 |
|
|
102
|
+
| SGR mouse press/drag/release through the PTY | 80 | 100 | 100 |
|
|
103
|
+
| Dispatch reset | 42 | 138 | 138 |
|
|
104
|
+
|
|
105
|
+
Resizing the PTY from 180 to 100 columns unmounted the sidebar handle and expanded
|
|
106
|
+
the session pane to 100 columns. Restoring 180 columns remounted the handle at
|
|
107
|
+
x=138, with sidebar width 42 and session pane width 138.
|
|
108
|
+
Physical-terminal behavior and a human-operated drag remain unverified.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madsoftwaredev/opencode-sidebar-resize",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "OpenCode V2 TUI plugin for resizing the existing session sidebar by dragging or command.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode",
|
|
7
|
+
"opencode-plugin",
|
|
8
|
+
"sidebar",
|
|
9
|
+
"resize",
|
|
10
|
+
"tui"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "MadSoftwareDev",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./tui": "./tui.tsx"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"tui.tsx",
|
|
20
|
+
"sidebar.ts",
|
|
21
|
+
"width.ts",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/madsoftwaredev/opencode-plugins.git",
|
|
28
|
+
"directory": "packages/sidebar-resize"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/madsoftwaredev/opencode-plugins/tree/main/packages/sidebar-resize#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/madsoftwaredev/opencode-plugins/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "bun test --preload @opentui/solid/preload",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@opencode/plugin": "^2.1.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@opentui/core": ">=0.6.6",
|
|
46
|
+
"@opentui/solid": ">=0.6.6",
|
|
47
|
+
"solid-js": ">=1.9.12"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@opentui/core": "0.8.1",
|
|
51
|
+
"@opentui/solid": "0.8.1",
|
|
52
|
+
"@types/bun": "1.4.0",
|
|
53
|
+
"typescript": "5.8.2"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/sidebar.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { CliRenderer, MouseEvent, Renderable, RGBA } from "@opentui/core";
|
|
2
|
+
import { clampWidth, DEFAULT_WIDTH } from "./width";
|
|
3
|
+
|
|
4
|
+
// OpenCode rewrites host-runtime imports in the TSX entrypoint, not this TS
|
|
5
|
+
// helper. Keep these imports type-only and receive the host's own constructors.
|
|
6
|
+
type Core = Pick<typeof import("@opentui/core"), "BoxRenderable" | "ScrollBoxRenderable" | "Yoga">;
|
|
7
|
+
|
|
8
|
+
type SidebarTarget = {
|
|
9
|
+
sidebar: Renderable;
|
|
10
|
+
pane: Renderable;
|
|
11
|
+
overlay: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// OpenCode has no public sidebar-width API. Match only the inspected V2 host
|
|
15
|
+
// shape: padded sidebar containing a heading, a scrollbox, and our footer branch.
|
|
16
|
+
// Never resize an arbitrary ancestor when an OpenCode update changes that shape.
|
|
17
|
+
function findSidebar(anchor: Renderable, core: Core): SidebarTarget | undefined {
|
|
18
|
+
const { Edge, PositionType, Unit } = core.Yoga;
|
|
19
|
+
let branch = anchor;
|
|
20
|
+
for (let node = anchor.parent; node; branch = node, node = node.parent) {
|
|
21
|
+
const children = node.getChildren();
|
|
22
|
+
// OpenTUI 0.6.6 exposes style getters on Renderable itself; getLayout()
|
|
23
|
+
// returns computed geometry, not the removed Yoga node or its style values.
|
|
24
|
+
if (
|
|
25
|
+
children.length !== 3 ||
|
|
26
|
+
!(children[1] instanceof core.ScrollBoxRenderable) ||
|
|
27
|
+
children[2] !== branch ||
|
|
28
|
+
node.getPadding(Edge.Left).value !== 2 ||
|
|
29
|
+
node.getPadding(Edge.Right).value !== 2 ||
|
|
30
|
+
node.getWidth().unit !== Unit.Point ||
|
|
31
|
+
node.getWidth().value !== DEFAULT_WIDTH
|
|
32
|
+
)
|
|
33
|
+
continue;
|
|
34
|
+
|
|
35
|
+
const pane = node.parent;
|
|
36
|
+
if (!pane) return undefined;
|
|
37
|
+
const width = pane.getWidth();
|
|
38
|
+
const overlay = pane.getPositionType() === PositionType.Absolute;
|
|
39
|
+
if (overlay && width.unit === Unit.Percent && width.value === 100) {
|
|
40
|
+
return { sidebar: node, pane, overlay: true };
|
|
41
|
+
}
|
|
42
|
+
if (!overlay && width.unit === Unit.Point && width.value === DEFAULT_WIDTH) {
|
|
43
|
+
return { sidebar: node, pane, overlay: false };
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function attachSidebarResize(options: {
|
|
51
|
+
core: Core;
|
|
52
|
+
anchor: Renderable;
|
|
53
|
+
renderer: CliRenderer;
|
|
54
|
+
width: () => number;
|
|
55
|
+
save: (width: number) => void;
|
|
56
|
+
highlight: () => RGBA;
|
|
57
|
+
}): { refresh: () => void; dispose: () => void } | undefined {
|
|
58
|
+
const target = findSidebar(options.anchor, options.core);
|
|
59
|
+
if (!target) return undefined;
|
|
60
|
+
const { sidebar, pane, overlay } = target;
|
|
61
|
+
let disposed = false;
|
|
62
|
+
let scheduled = false;
|
|
63
|
+
let drag: { right: number; offset: number; width: number } | undefined;
|
|
64
|
+
|
|
65
|
+
const available = () => (overlay ? pane.width : pane.parent?.width) || options.renderer.width;
|
|
66
|
+
const apply = (width: number) => {
|
|
67
|
+
if (disposed || sidebar.isDestroyed || pane.isDestroyed) return;
|
|
68
|
+
const next = clampWidth(width, available(), overlay);
|
|
69
|
+
// Both widths are fixed by the host. Updating only the inner box would
|
|
70
|
+
// overlap the transcript rather than give the whole sidebar more room.
|
|
71
|
+
if (sidebar.getWidth().value !== next) sidebar.width = next;
|
|
72
|
+
if (!overlay && pane.getWidth().value !== next) pane.width = next;
|
|
73
|
+
};
|
|
74
|
+
const refresh = () => apply(drag?.width ?? options.width());
|
|
75
|
+
const schedule = () => {
|
|
76
|
+
if (scheduled || disposed) return;
|
|
77
|
+
scheduled = true;
|
|
78
|
+
queueMicrotask(() => {
|
|
79
|
+
scheduled = false;
|
|
80
|
+
refresh();
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
const stop = (event: MouseEvent) => {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
};
|
|
87
|
+
const finish = (event: MouseEvent) => {
|
|
88
|
+
if (!drag) return;
|
|
89
|
+
stop(event);
|
|
90
|
+
const width = drag.width;
|
|
91
|
+
drag = undefined;
|
|
92
|
+
handle.backgroundColor = undefined;
|
|
93
|
+
options.save(width);
|
|
94
|
+
};
|
|
95
|
+
const move = (event: MouseEvent) => {
|
|
96
|
+
if (!drag) return;
|
|
97
|
+
stop(event);
|
|
98
|
+
drag.width = clampWidth(drag.right - event.x + drag.offset, available(), overlay);
|
|
99
|
+
apply(drag.width);
|
|
100
|
+
};
|
|
101
|
+
const handle = new options.core.BoxRenderable(options.renderer, {
|
|
102
|
+
id: "sidebar-resize.handle",
|
|
103
|
+
position: "absolute",
|
|
104
|
+
left: 0,
|
|
105
|
+
top: 0,
|
|
106
|
+
width: 2,
|
|
107
|
+
height: "100%",
|
|
108
|
+
zIndex: 10,
|
|
109
|
+
onMouseOver: () => {
|
|
110
|
+
handle.backgroundColor = options.highlight();
|
|
111
|
+
},
|
|
112
|
+
onMouseOut: () => {
|
|
113
|
+
if (!drag) handle.backgroundColor = undefined;
|
|
114
|
+
},
|
|
115
|
+
onMouseDown: (event) => {
|
|
116
|
+
if (event.button !== 0) return;
|
|
117
|
+
stop(event);
|
|
118
|
+
drag = {
|
|
119
|
+
right: sidebar.x + sidebar.width,
|
|
120
|
+
offset: event.x - sidebar.x,
|
|
121
|
+
width: sidebar.width,
|
|
122
|
+
};
|
|
123
|
+
handle.backgroundColor = options.highlight();
|
|
124
|
+
},
|
|
125
|
+
onMouseDrag: move,
|
|
126
|
+
onMouseDragEnd: finish,
|
|
127
|
+
onMouseUp: finish,
|
|
128
|
+
});
|
|
129
|
+
sidebar.add(handle);
|
|
130
|
+
// In the shipped OpenTUI, capture begins at the first drag's hit target,
|
|
131
|
+
// not the mouse-down target. Catch bubbled drags even when that first move
|
|
132
|
+
// jumps past the two-cell handle; preserve normal dispatch when not dragging.
|
|
133
|
+
const root = options.renderer.root;
|
|
134
|
+
const originalMouse = root.processMouseEvent;
|
|
135
|
+
const dispatchMouse = (event: MouseEvent) => {
|
|
136
|
+
if (!disposed && drag) {
|
|
137
|
+
if (event.type === "drag") return move(event);
|
|
138
|
+
if (event.type === "drag-end" || event.type === "up") return finish(event);
|
|
139
|
+
}
|
|
140
|
+
originalMouse.call(root, event);
|
|
141
|
+
};
|
|
142
|
+
root.processMouseEvent = dispatchMouse;
|
|
143
|
+
const watched = [...new Set([sidebar, pane, pane.parent].filter((node) => node !== null))];
|
|
144
|
+
for (const node of watched) node.on("resize", schedule);
|
|
145
|
+
options.renderer.on("resize", schedule);
|
|
146
|
+
refresh();
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
refresh,
|
|
150
|
+
dispose() {
|
|
151
|
+
if (disposed) return;
|
|
152
|
+
disposed = true;
|
|
153
|
+
for (const node of watched) node.off("resize", schedule);
|
|
154
|
+
options.renderer.off("resize", schedule);
|
|
155
|
+
if (root.processMouseEvent === dispatchMouse) root.processMouseEvent = originalMouse;
|
|
156
|
+
if (!handle.isDestroyed) handle.destroy();
|
|
157
|
+
if (!sidebar.isDestroyed) sidebar.width = DEFAULT_WIDTH;
|
|
158
|
+
if (!overlay && !pane.isDestroyed) pane.width = DEFAULT_WIDTH;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
package/tui.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
4
|
+
import { BoxRenderable, ScrollBoxRenderable, Yoga } from "@opentui/core";
|
|
5
|
+
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
6
|
+
import { attachSidebarResize } from "./sidebar";
|
|
7
|
+
import { DEFAULT_WIDTH, parseWidth, preferredWidth } from "./width";
|
|
8
|
+
|
|
9
|
+
export default Plugin.define({
|
|
10
|
+
id: "sidebar-resize",
|
|
11
|
+
setup(context) {
|
|
12
|
+
const [settings, update] = context.storage.store<{ width: number }>("settings", {
|
|
13
|
+
initial: { width: DEFAULT_WIDTH },
|
|
14
|
+
});
|
|
15
|
+
const [width, setWidth] = createSignal(preferredWidth(settings.width));
|
|
16
|
+
createEffect(() => setWidth(preferredWidth(settings.width)));
|
|
17
|
+
const save = async (width: number) => {
|
|
18
|
+
setWidth(width);
|
|
19
|
+
try {
|
|
20
|
+
await update((draft) => {
|
|
21
|
+
draft.width = width;
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
setWidth(preferredWidth(settings.width));
|
|
25
|
+
console.warn("[sidebar-resize] Could not save width", error);
|
|
26
|
+
context.ui.toast.show({ message: "Could not save sidebar width.", variant: "error" });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const unregister = context.ui.slot({
|
|
30
|
+
append: "sidebar.footer",
|
|
31
|
+
render: () => {
|
|
32
|
+
// Hidden anchor adds no row or footer gap. The drag handle is attached
|
|
33
|
+
// directly to the original sidebar, outside its scrolling contents.
|
|
34
|
+
let anchor: BoxRenderable | undefined;
|
|
35
|
+
let attachment: ReturnType<typeof attachSidebarResize>;
|
|
36
|
+
onMount(() => {
|
|
37
|
+
if (!anchor) return;
|
|
38
|
+
attachment = attachSidebarResize({
|
|
39
|
+
core: { BoxRenderable, ScrollBoxRenderable, Yoga },
|
|
40
|
+
anchor,
|
|
41
|
+
renderer: context.renderer,
|
|
42
|
+
width,
|
|
43
|
+
save: (width) => {
|
|
44
|
+
void save(width);
|
|
45
|
+
},
|
|
46
|
+
highlight: () => context.theme.background.raised.high,
|
|
47
|
+
});
|
|
48
|
+
if (!attachment) {
|
|
49
|
+
context.ui.toast.show({
|
|
50
|
+
message: "Sidebar resize: unsupported OpenCode layout; sidebar left unchanged.",
|
|
51
|
+
variant: "warning",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
createEffect(() => {
|
|
56
|
+
width();
|
|
57
|
+
attachment?.refresh();
|
|
58
|
+
});
|
|
59
|
+
onCleanup(() => attachment?.dispose());
|
|
60
|
+
return (
|
|
61
|
+
<box
|
|
62
|
+
ref={(node) => {
|
|
63
|
+
anchor = node;
|
|
64
|
+
}}
|
|
65
|
+
id="sidebar-resize.anchor"
|
|
66
|
+
visible={false}
|
|
67
|
+
width={0}
|
|
68
|
+
height={0}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
context.keymap.layer(() => ({
|
|
75
|
+
mode: "global",
|
|
76
|
+
commands: [
|
|
77
|
+
{
|
|
78
|
+
id: "sidebar-resize.width",
|
|
79
|
+
title: "Set sidebar width",
|
|
80
|
+
group: "Appearance",
|
|
81
|
+
palette: true,
|
|
82
|
+
slash: { name: "sidebar-width", arguments: true },
|
|
83
|
+
run: async (input) => {
|
|
84
|
+
const value =
|
|
85
|
+
input ??
|
|
86
|
+
(await context.ui.dialog.prompt({
|
|
87
|
+
title: "Sidebar width (columns)",
|
|
88
|
+
value: String(width()),
|
|
89
|
+
}));
|
|
90
|
+
if (value === undefined) return;
|
|
91
|
+
const nextWidth = value.trim() === "reset" ? DEFAULT_WIDTH : parseWidth(value);
|
|
92
|
+
if (nextWidth === undefined) {
|
|
93
|
+
context.ui.toast.show({
|
|
94
|
+
message: "Use a positive whole number, or /sidebar-width reset.",
|
|
95
|
+
variant: "error",
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await save(nextWidth);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
bindings: ["sidebar-resize.width"],
|
|
104
|
+
}));
|
|
105
|
+
return unregister;
|
|
106
|
+
},
|
|
107
|
+
});
|
package/width.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const DEFAULT_WIDTH = 42;
|
|
2
|
+
const MIN_WIDTH = 28;
|
|
3
|
+
const TRANSCRIPT_WIDTH = 64;
|
|
4
|
+
|
|
5
|
+
export function preferredWidth(value: unknown): number {
|
|
6
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
7
|
+
? Math.round(value)
|
|
8
|
+
: DEFAULT_WIDTH;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clampWidth(preferred: number, available: number, overlay: boolean): number {
|
|
12
|
+
const maximum = Math.max(1, Math.floor(available) - (overlay ? 2 : TRANSCRIPT_WIDTH));
|
|
13
|
+
return Math.max(Math.min(MIN_WIDTH, maximum), Math.min(preferredWidth(preferred), maximum));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseWidth(input: string): number | undefined {
|
|
17
|
+
const value = input.trim();
|
|
18
|
+
if (!/^\d+$/.test(value)) return undefined;
|
|
19
|
+
const width = Number(value);
|
|
20
|
+
return Number.isSafeInteger(width) && width > 0 ? width : undefined;
|
|
21
|
+
}
|