opencode-ollama-ps 0.0.0-stage → 0.1.1

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/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ # opencode-ollama-ps
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ OpenCode TUI sidebar plugin that displays Ollama running models (model **name**, **size**, **processor**, and **until**) in the right sidebar alongside the built-in context length and todo list.
4
+
5
+ ```
6
+ Context 12,345 tokens 45% used
7
+ Ollama
8
+ devstral-small-2:latest 55.0 GB llamacpp 4 minutes
9
+ Todo
10
+ □ Review pull request
11
+ □ Fix login bug
12
+ ```
13
+
14
+ ## Install (OpenCode v2.2.6+)
15
+
16
+ For file-based TUI plugins, OpenCode only needs the single `.tsx` file — do **not** copy `package.json` into the plugins directory (it triggers a failed `npm install`).
17
+
18
+ 1. Copy just the plugin file:
19
+
20
+ ```sh
21
+ mkdir -p ~/.config/opencode/plugins/opencode-ollama-ps/src
22
+ cp /path/to/opencode-ollama-ps/src/index.tsx ~/.config/opencode/plugins/opencode-ollama-ps/src/index.tsx
23
+ ```
24
+
25
+ 2. Register it in `~/.config/opencode/tui.json` (use `file://` prefix if the plain path doesn't load):
26
+
27
+ ```json
28
+ {
29
+ "$schema": "https://opencode.ai/tui.json",
30
+ "plugin": [
31
+ "file:///home/eike/.config/opencode/plugins/opencode-ollama-ps/src/index.tsx"
32
+ ]
33
+ }
34
+ ```
35
+
36
+ If `file://` doesn't work, try the plain absolute path:
37
+ ```json
38
+ {
39
+ "plugin": [
40
+ "/home/eike/.config/opencode/plugins/opencode-ollama-ps/src/index.tsx"
41
+ ]
42
+ }
43
+ ```
44
+
45
+ 3. **Do not** add it to `cli.json` — that would try to load it as a server plugin and fail.
46
+
47
+ 4. Clear OpenCode's cache and restart:
48
+
49
+ ```sh
50
+ rm -rf ~/.cache/opencode/
51
+ export OLLAMA_HOST=http://172.23.192.1:11434
52
+ opencode
53
+ ```
54
+
55
+ ## Configuration (optional)
56
+
57
+ Add options under the plugin entry in `tui.json`:
58
+
59
+ ```json
60
+ {
61
+ "plugin": [
62
+ ["file:///home/eike/.config/opencode/plugins/opencode-ollama-ps/src/index.tsx", {
63
+ "ollamaIntervalMs": 5000
64
+ }]
65
+ ]
66
+ }
67
+ ```
68
+
69
+ | Option | Default | Description |
70
+ |--------|---------|-------------|
71
+ | `ollamaIntervalMs` | `5000` | How often to poll the Ollama API (ms) |
72
+
73
+ ## How it works
74
+
75
+ - Registers a `sidebar_content` slot contribution, so the panel appears in the right sidebar next to Context, MCP, LSP, and Todos.
76
+ - Polls the Ollama HTTP API (`GET {host}/api/ps`) via `fetch` on a timer — no `ollama` CLI binary needed.
77
+ - Reads **`$OLLAMA_HOST`** from the environment (default `http://localhost:11434`), so remote Ollama hosts work out of the box.
78
+ - Renders each loaded model's name, size, processor, and time-until-unload.
79
+ - Shows an error line in the sidebar if the API call fails, and a "no models loaded" line when nothing is running.
80
+
81
+ ## Important: where the panel appears
82
+
83
+ - **TUI plugins do not show in `opencode plugin list`** — that command only lists server plugins.
84
+ - The sidebar panel only renders when you're in an **active session** (not on the home screen). Start a chat session and look at the right sidebar.
85
+ - If you don't see a sidebar, press `Ctrl+X B` (or `leader b`) to toggle it.
86
+
87
+ ## Troubleshooting
88
+
89
+ Panel not showing at all:
90
+ 1. Make sure you're in an active chat session (sidebar panels only render during sessions)
91
+ 2. Export `OLLAMA_HOST` before launching OpenCode:
92
+ ```sh
93
+ export OLLAMA_HOST=http://172.23.192.1:11434
94
+ opencode
95
+ ```
96
+ 3. Completely quit OpenCode, clear cache, and restart:
97
+ ```sh
98
+ rm -rf ~/.cache/opencode/
99
+ ```
100
+ 4. Try the `file://` prefix in `tui.json` (see Install above)
101
+ 5. Verify the Ollama API is reachable: `curl http://172.23.192.1:11434/api/ps`
102
+
103
+ Panel shows `error: ... connection refused` / `host: http://...`:
104
+ - OpenCode can't reach the Ollama host. The host should be the base URL without `/v1` (your Ollama API base is `http://172.23.192.1:11434/v1`, so `OLLAMA_HOST=http://172.23.192.1:11434`).
105
+
106
+ Panel stays empty / `no models loaded`:
107
+ - No Ollama models are currently loaded. Load a model first, then the panel appears.
108
+
109
+ Server plugin error / NpmInstallFailedError:
110
+ - You likely added the plugin to `cli.json` (server plugins) or copied `package.json` into the plugins directory. Remove it from `cli.json` and install only `src/index.tsx` (see Install above).
111
+
112
+ ## Uninstall
113
+
114
+ Remove the plugin entry from `tui.json` and restart OpenCode.
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "opencode-ollama-ps",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "OpenCode TUI sidebar plugin that shows ollama ps output (size, processor, context) alongside context length and todos.",
5
+ "type": "module",
6
+ "main": "src/index.tsx",
7
+ "exports": {
8
+ ".": "./src/index.tsx",
9
+ "./tui": "./src/index.tsx"
10
+ },
11
+ "tui": {
12
+ "plugin": "./src/index.tsx"
13
+ },
14
+ "dependencies": {
15
+ "@opencode-ai/plugin": "^1.4.0",
16
+ "@opentui/core": "^0.1.97",
17
+ "@opentui/solid": "^0.1.97",
18
+ "solid-js": "^1.9.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^24.0.0",
22
+ "typescript": "^5.0.0"
23
+ },
24
+ "scripts": {
25
+ "typecheck": "tsc --noEmit"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "tui.ts"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,137 @@
1
+ /** @jsxImportSource ./jsx-runtime */
2
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
+ import { createMemo, createSignal, Show, For } from "solid-js"
4
+
5
+ type OllamaModel = {
6
+ name: string
7
+ id: string
8
+ size: string
9
+ processor: string
10
+ until?: string
11
+ }
12
+
13
+ const DEFAULT_INTERVAL_MS = 5000
14
+ const DEFAULT_OLLAMA_HOST = "http://localhost:11434"
15
+
16
+ function resolveHost(options: Record<string, unknown> | undefined): string {
17
+ return (
18
+ (process.env.OLLAMA_HOST as string | undefined) ??
19
+ DEFAULT_OLLAMA_HOST
20
+ )
21
+ }
22
+
23
+ function fmtBytes(bytes: number): string {
24
+ if (!bytes) return "—"
25
+ const units = ["B", "KB", "MB", "GB", "TB"]
26
+ let v = bytes
27
+ let u = 0
28
+ while (v >= 1024 && u < units.length - 1) {
29
+ v /= 1024
30
+ u++
31
+ }
32
+ return `${v.toFixed(1)} ${units[u]}`
33
+ }
34
+
35
+ function rgbaToHex(rgba: { r: number; g: number; b: number; a: number }): string {
36
+ const toHex = (n: number) => Math.round(n * 255).toString(16).padStart(2, "0")
37
+ return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`
38
+ }
39
+
40
+ async function fetchOllamaPs(host: string): Promise<{ models: OllamaModel[]; error?: string }> {
41
+ try {
42
+ const res = await fetch(`${host}/api/ps`, { signal: AbortSignal.timeout(10000) })
43
+ if (!res.ok) {
44
+ return { models: [], error: `HTTP ${res.status}` }
45
+ }
46
+ const data = (await res.json()) as { models?: unknown[] }
47
+ const models: OllamaModel[] = (data.models ?? []).map((m: any) => ({
48
+ name: m.name ?? m.model ?? "unknown",
49
+ id: m.id ?? "",
50
+ size: m.size_label ?? fmtBytes(m.size ?? 0),
51
+ processor: m.processor ?? "?",
52
+ until: m.expires_at_label ?? m.expires_at,
53
+ }))
54
+ return { models, error: undefined }
55
+ } catch (err) {
56
+ return {
57
+ models: [],
58
+ error: err instanceof Error ? err.message : String(err),
59
+ }
60
+ }
61
+ }
62
+
63
+ const tui: TuiPlugin = async (api, options, meta) => {
64
+ const intervalMs =
65
+ (options?.ollamaIntervalMs as number | undefined) ?? DEFAULT_INTERVAL_MS
66
+ const host = resolveHost(options)
67
+
68
+ const [version, setVersion] = createSignal(0)
69
+ const [state, setState] = createSignal<{
70
+ models: OllamaModel[]
71
+ error?: string
72
+ }>({ models: [], error: undefined })
73
+
74
+ const refresh = () => {
75
+ void fetchOllamaPs(host).then((result) => {
76
+ setState({ models: result.models, error: result.error })
77
+ setVersion((v) => v + 1)
78
+ })
79
+ }
80
+
81
+ const intervalId = setInterval(refresh, intervalMs)
82
+ refresh()
83
+
84
+ api.lifecycle.onDispose(() => {
85
+ clearInterval(intervalId)
86
+ })
87
+
88
+ const data = createMemo(() => {
89
+ version()
90
+ return state()
91
+ })
92
+
93
+ api.slots.register({
94
+ order: 450,
95
+ slots: {
96
+ sidebar_content(_ctx, props) {
97
+ const d = data()
98
+ const theme = api.theme.current
99
+ const rows = d.models
100
+
101
+ return (
102
+ <>
103
+ <text fg={rgbaToHex(theme.text)}>
104
+ <b>Ollama</b>
105
+ </text>
106
+ <Show when={d.error}>
107
+ <text fg={rgbaToHex(theme.textMuted)}>
108
+ error: {d.error} (host: {host})
109
+ </text>
110
+ </Show>
111
+ <Show when={!d.error && rows.length === 0}>
112
+ <text fg={rgbaToHex(theme.textMuted)}>no models loaded</text>
113
+ </Show>
114
+ <For each={rows}>
115
+ {(m) => (
116
+ <>
117
+ <text fg={rgbaToHex(theme.textMuted)}>{m.name}</text>
118
+ <text fg={rgbaToHex(theme.textMuted)}>{m.size}</text>
119
+ <text fg={rgbaToHex(theme.textMuted)}>{m.processor}</text>
120
+ <Show when={m.until}>
121
+ <text fg={rgbaToHex(theme.textMuted)}>{m.until}</text>
122
+ </Show>
123
+ </>
124
+ )}
125
+ </For>
126
+ </>
127
+ )
128
+ },
129
+ },
130
+ })
131
+ }
132
+
133
+ const plugin: TuiPluginModule & { id: string } = {
134
+ id: "opencode.ollama-ps",
135
+ tui,
136
+ }
137
+ export default plugin
@@ -0,0 +1,24 @@
1
+ // JSX runtime for @opentui/solid compatibility
2
+ import { createElement, spread, createComponent } from "@opentui/solid"
3
+
4
+ function Fragment(props: { children?: unknown }): unknown {
5
+ const children = props.children
6
+ return typeof children === "function" ? children() : children
7
+ }
8
+
9
+ function jsx(type: any, props: any = {}, ...children: any[]): any {
10
+ if (typeof type === "string") {
11
+ const el = createElement(type)
12
+ if (props) {
13
+ const merged = { ...props }
14
+ if (children.length > 0) {
15
+ merged.children = children.length === 1 ? children[0] : children
16
+ }
17
+ spread(el, merged)
18
+ }
19
+ return el
20
+ }
21
+ return createComponent(type, { ...props, children: children.length === 1 ? children[0] : children })
22
+ }
23
+
24
+ export { jsx, jsx as jsxs, jsx as jsxDEV, Fragment }
@@ -0,0 +1,24 @@
1
+ // JSX runtime entry for @opentui/solid compatibility
2
+ import { createElement, spread, createComponent } from "@opentui/solid"
3
+
4
+ function Fragment(props: { children?: unknown }): unknown {
5
+ const children = props.children
6
+ return typeof children === "function" ? children() : children
7
+ }
8
+
9
+ function jsx(type: any, props: any = {}, ...children: any[]): any {
10
+ if (typeof type === "string") {
11
+ const el = createElement(type)
12
+ if (props) {
13
+ const merged = { ...props }
14
+ if (children.length > 0) {
15
+ merged.children = children.length === 1 ? children[0] : children
16
+ }
17
+ spread(el, merged)
18
+ }
19
+ return el
20
+ }
21
+ return createComponent(type, { ...props, children: children.length === 1 ? children[0] : children })
22
+ }
23
+
24
+ export { jsx, jsx as jsxs, Fragment }
@@ -0,0 +1,24 @@
1
+ // JSX dev runtime for @opentui/solid compatibility
2
+ import { createElement, spread, createComponent } from "@opentui/solid"
3
+
4
+ function Fragment(props: { children?: unknown }): unknown {
5
+ const children = props.children
6
+ return typeof children === "function" ? children() : children
7
+ }
8
+
9
+ function jsxDEV(type: any, props: any = {}, ...children: any[]): any {
10
+ if (typeof type === "string") {
11
+ const el = createElement(type)
12
+ if (props) {
13
+ const merged = { ...props }
14
+ if (children.length > 0) {
15
+ merged.children = children.length === 1 ? children[0] : children
16
+ }
17
+ spread(el, merged)
18
+ }
19
+ return el
20
+ }
21
+ return createComponent(type, { ...props, children: children.length === 1 ? children[0] : children })
22
+ }
23
+
24
+ export { jsxDEV, Fragment }
@@ -0,0 +1,25 @@
1
+ // JSX runtime for @opentui/solid compatibility
2
+ // Re-exports createElement, spread, createComponent with a local Fragment
3
+ import { createElement, spread, createComponent } from "@opentui/solid"
4
+
5
+ function Fragment(props: { children?: unknown }): unknown {
6
+ const children = props.children
7
+ return typeof children === "function" ? children() : children
8
+ }
9
+
10
+ function jsx(type: any, props: any = {}, ...children: any[]): any {
11
+ if (typeof type === "string") {
12
+ const el = createElement(type)
13
+ if (props) {
14
+ const merged = { ...props }
15
+ if (children.length > 0) {
16
+ merged.children = children.length === 1 ? children[0] : children
17
+ }
18
+ spread(el, merged)
19
+ }
20
+ return el
21
+ }
22
+ return createComponent(type, { ...props, children: children.length === 1 ? children[0] : children })
23
+ }
24
+
25
+ export { jsx, jsx as jsxs, jsx as jsxDEV, Fragment }
package/tui.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/index.tsx"