@opencode-cockpit/shell 0.1.4 → 0.1.5

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.
@@ -0,0 +1,156 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createEffect, createMemo, createRoot, createSignal, on, onCleanup } from "solid-js";
3
+ import { createStore, reconcile } from "solid-js/store";
4
+ import { order, partition } from "./view.js";
5
+ export function createShellStore(api, client, options = {}) {
6
+ return createRoot(dispose => {
7
+ const [state, setState] = createStore({
8
+ list: []
9
+ });
10
+ const [connected, setConnected] = createSignal(false);
11
+ const [now, setNow] = createSignal(Date.now());
12
+ const [frame, setFrame] = createSignal(0);
13
+ const [selectedId, setSelectedId] = createSignal();
14
+ const [showAll, setShowAll] = createSignal(api.kv.get("cockpit.shells.showAll", false));
15
+ const project = () => api.state.path.directory;
16
+ const historyMs = (options.historyMinutes ?? 30) * 60_000;
17
+ const refresh = async () => {
18
+ try {
19
+ const list = await client.call("shell.list", {
20
+ owner: {
21
+ project: project()
22
+ }
23
+ });
24
+ setState("list", reconcile(list, {
25
+ key: "id"
26
+ }));
27
+ } catch {
28
+ // daemon not running yet; the reconnect loop will pick it up
29
+ }
30
+ };
31
+ const offs = [client.on("shell.started", () => void refresh()), client.on("shell.exited", () => void refresh()), client.on("shell.removed", () => void refresh()), client.onState(s => {
32
+ setConnected(s === "connected");
33
+ if (s === "connected") void refresh();
34
+ })];
35
+
36
+ // Connect only to a daemon that already exists; starting one is left to explicit actions.
37
+ const probe = () => {
38
+ if (!client.connected && existsSync(client.paths.socket)) void client.connect().catch(() => {});
39
+ };
40
+ probe();
41
+ const probeTimer = setInterval(probe, 3000);
42
+ const tick = setInterval(() => setNow(Date.now()), 1000);
43
+ const spin = setInterval(() => {
44
+ if (state.list.some(s => s.status === "running")) setFrame(f => f + 1);
45
+ }, 120);
46
+ onCleanup(() => {
47
+ clearInterval(probeTimer);
48
+ clearInterval(tick);
49
+ clearInterval(spin);
50
+ for (const off of offs) off();
51
+ });
52
+ const pick = createMemo(() => {
53
+ const ordered = order(state.list);
54
+ return ordered.find(s => s.id === selectedId()) ?? ordered[0];
55
+ });
56
+ const folded = createMemo(() => partition(state.list, {
57
+ showAll: showAll(),
58
+ historyMs,
59
+ now: now(),
60
+ keep: pick()?.id
61
+ }));
62
+ return {
63
+ client,
64
+ project,
65
+ shells: () => state.list,
66
+ visible: () => folded().visible,
67
+ hidden: () => folded().hidden,
68
+ showAll,
69
+ toggleAll() {
70
+ const next = !showAll();
71
+ setShowAll(next);
72
+ api.kv.set("cockpit.shells.showAll", next);
73
+ },
74
+ connected,
75
+ now,
76
+ frame,
77
+ selected: pick,
78
+ select: id => setSelectedId(id),
79
+ step(delta) {
80
+ const list = folded().visible;
81
+ if (list.length === 0) return;
82
+ const index = Math.max(0, list.findIndex(s => s.id === pick()?.id));
83
+ const next = list[(index + delta + list.length) % list.length];
84
+ if (next) setSelectedId(next.id);
85
+ },
86
+ refresh,
87
+ async clearFinished() {
88
+ const {
89
+ removed
90
+ } = await client.call("shell.clear", {
91
+ owner: {
92
+ project: project()
93
+ }
94
+ });
95
+ await refresh();
96
+ return removed.length;
97
+ },
98
+ dispose
99
+ };
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Live terminal view of one shell: attaches for change notifications and re-renders the daemon's
105
+ * emulated screen, throttled. Must be called inside a reactive owner.
106
+ */
107
+ export function useScreen(store, id) {
108
+ const [screen, setScreen] = createSignal();
109
+ let timer;
110
+ let current;
111
+ const fetch = shellId => {
112
+ timer = undefined;
113
+ void store.client.call("shell.screen", {
114
+ id: shellId
115
+ }).then(s => {
116
+ if (current === shellId) setScreen(s);
117
+ }).catch(() => {});
118
+ };
119
+ const schedule = shellId => {
120
+ timer ??= setTimeout(() => fetch(shellId), 80);
121
+ };
122
+ const offOutput = store.client.on("shell.output", e => {
123
+ if (e.id === current) schedule(e.id);
124
+ });
125
+ const offExit = store.client.on("shell.exited", info => {
126
+ if (info.id === current) schedule(info.id);
127
+ });
128
+ const attach = next => {
129
+ if (next === current) return;
130
+ if (current) void store.client.call("shell.detach", {
131
+ id: current
132
+ }).catch(() => {});
133
+ current = next;
134
+ setScreen(undefined);
135
+ if (!next) return;
136
+ const info = store.shells().find(s => s.id === next);
137
+ void store.client.call("shell.attach", {
138
+ id: next,
139
+ fromOffset: info?.bytes ?? Number.MAX_SAFE_INTEGER
140
+ }).catch(() => {});
141
+ fetch(next);
142
+ };
143
+ createEffect(on(id, next => attach(next)));
144
+ onCleanup(() => {
145
+ clearTimeout(timer);
146
+ offOutput();
147
+ offExit();
148
+ if (current) void store.client.call("shell.detach", {
149
+ id: current
150
+ }).catch(() => {});
151
+ });
152
+ return {
153
+ screen,
154
+ refetch: () => current && fetch(current)
155
+ };
156
+ }
@@ -0,0 +1,154 @@
1
+ import { duration } from "../tools/format.js";
2
+
3
+ /** What a human cares about, derived from status + exit code so every surface agrees. */
4
+
5
+ export function kindOf(s) {
6
+ if (s.status === "running") return "run";
7
+ if (s.status === "killed") return "stop";
8
+ if (s.status === "exited" && s.exitCode === 0) return "done";
9
+ return "fail";
10
+ }
11
+ export const BADGE_LABEL = {
12
+ run: "RUN",
13
+ fail: "FAIL",
14
+ stop: "STOP",
15
+ done: "DONE"
16
+ };
17
+
18
+ /** Braille spinner: single-width in every terminal font. */
19
+ export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
20
+ export function kindColor(theme, kind) {
21
+ switch (kind) {
22
+ case "run":
23
+ return theme.success;
24
+ case "fail":
25
+ return theme.error;
26
+ case "stop":
27
+ return theme.warning;
28
+ case "done":
29
+ return theme.textMuted;
30
+ }
31
+ }
32
+
33
+ /** Fixed 7-column pill so lists line up: " ⠹ RUN ", " FAIL ". */
34
+ export function badgeText(kind, frame = 0) {
35
+ if (kind === "run") return ` ${SPINNER[frame % SPINNER.length]} RUN `;
36
+ return ` ${BADGE_LABEL[kind].padEnd(4)} `;
37
+ }
38
+ const RANK = {
39
+ run: 0,
40
+ fail: 1,
41
+ stop: 2,
42
+ done: 3
43
+ };
44
+
45
+ /** Running first (oldest first, stable tabs), then failures, stopped, done (most recent first). */
46
+ export function order(list) {
47
+ return [...list].sort((a, b) => {
48
+ const ka = kindOf(a);
49
+ const kb = kindOf(b);
50
+ if (ka !== kb) return RANK[ka] - RANK[kb];
51
+ if (ka === "run") return a.startedAt - b.startedAt;
52
+ return (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt);
53
+ });
54
+ }
55
+ /** Default view: running shells and recent failures. Everything else folds into a "N more" chip. */
56
+ export function partition(list, opts) {
57
+ const ordered = order(list);
58
+ if (opts.showAll) return {
59
+ visible: ordered,
60
+ hidden: []
61
+ };
62
+ const visible = [];
63
+ const hidden = [];
64
+ for (const s of ordered) {
65
+ const kind = kindOf(s);
66
+ const recent = opts.now - (s.endedAt ?? opts.now) <= opts.historyMs;
67
+ if (kind === "run" || kind === "fail" && recent || s.id === opts.keep) visible.push(s);else hidden.push(s);
68
+ }
69
+ return {
70
+ visible,
71
+ hidden
72
+ };
73
+ }
74
+ export function statusDetail(s, now) {
75
+ const kind = kindOf(s);
76
+ const ran = duration((s.endedAt ?? now) - s.startedAt);
77
+ const ago = s.endedAt ? since(now - s.endedAt) : "";
78
+ switch (kind) {
79
+ case "run":
80
+ return ran;
81
+ case "done":
82
+ return `took ${ran} · ${ago}`;
83
+ case "stop":
84
+ return `stopped after ${ran} · ${ago}`;
85
+ case "fail":
86
+ if (s.status === "failed") return "could not start";
87
+ return `exit ${s.exitCode ?? "?"} after ${ran} · ${ago}`;
88
+ }
89
+ }
90
+
91
+ /** Compact detail for narrow lists. */
92
+ export function shortDetail(s, now) {
93
+ switch (kindOf(s)) {
94
+ case "run":
95
+ return duration(now - s.startedAt);
96
+ case "fail":
97
+ return s.status === "failed" ? "no start" : `exit ${s.exitCode ?? "?"}`;
98
+ case "stop":
99
+ return "stopped";
100
+ case "done":
101
+ return s.endedAt ? since(now - s.endedAt) : "done";
102
+ }
103
+ }
104
+
105
+ /** Coarse relative time: "just now", "12s ago", "9m ago", "3h ago". */
106
+ export function since(ms) {
107
+ const s = Math.floor(ms / 1000);
108
+ if (s < 5) return "just now";
109
+ if (s < 60) return `${s}s ago`;
110
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
111
+ if (s < 86_400) return `${Math.floor(s / 3600)}h ago`;
112
+ return `${Math.floor(s / 86_400)}d ago`;
113
+ }
114
+
115
+ /** The command as the user or agent wrote it, without the `$SHELL -c` wrapper. */
116
+ export function displayCommand(s) {
117
+ if (s.args.length === 2 && s.args[0] === "-c" && /(^|\/)(ba|z|fi|da|k)?sh$/.test(s.command)) return s.args[1];
118
+ return [s.command, ...s.args].join(" ");
119
+ }
120
+
121
+ /** Folder relative to the project: "" at the root, "./sub" inside, "~/x" elsewhere under home. */
122
+ export function relativeCwd(cwd, project, home = process.env.HOME ?? "") {
123
+ const trim = p => p.replace(/\/+$/, "");
124
+ const c = trim(cwd);
125
+ const p = trim(project);
126
+ if (c === p) return "";
127
+ if (c.startsWith(`${p}/`)) return `./${c.slice(p.length + 1)}`;
128
+ if (home && c.startsWith(`${trim(home)}/`)) return `~/${c.slice(trim(home).length + 1)}`;
129
+ return c;
130
+ }
131
+
132
+ /** Hard-wraps to `width`, at most `maxLines`; the last line ends with … when text was cut. */
133
+ export function wrapText(text, width, maxLines) {
134
+ const flat = text.replace(/\s*\n\s*/g, " ⏎ ");
135
+ const w = Math.max(4, width);
136
+ const lines = [];
137
+ for (let i = 0; i < flat.length && lines.length < maxLines; i += w) lines.push(flat.slice(i, i + w));
138
+ if (lines.length === 0) return [""];
139
+ if (flat.length > w * maxLines) {
140
+ const last = lines[lines.length - 1];
141
+ lines[lines.length - 1] = `${last.slice(0, w - 1)}…`;
142
+ }
143
+ return lines;
144
+ }
145
+
146
+ /** Last `rows` lines of screen text, each cut to `cols`. */
147
+ export function tailLines(text, rows, cols) {
148
+ if (!text) return "";
149
+ const lines = text.split("\n");
150
+ return lines.slice(Math.max(0, lines.length - rows)).map(l => l.length > cols ? `${l.slice(0, Math.max(0, cols - 1))}…` : l).join("\n");
151
+ }
152
+ export function truncate(text, max) {
153
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cockpit/shell",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Background shells for OpenCode: the agent starts, waits on and drives PTYs; you watch them in a docked TUI panel",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,16 +24,26 @@
24
24
  "terminal"
25
25
  ],
26
26
  "exports": {
27
- "./server": "./src/server.ts",
28
- "./tui": "./src/tui/index.tsx",
29
- "./connect": "./src/connect.ts"
27
+ "./server": {
28
+ "types": "./types/server.d.ts",
29
+ "default": "./dist/server.js"
30
+ },
31
+ "./tui": {
32
+ "types": "./types/tui/index.d.ts",
33
+ "default": "./dist/tui/index.js"
34
+ },
35
+ "./connect": {
36
+ "types": "./types/connect.d.ts",
37
+ "default": "./dist/connect.js"
38
+ }
30
39
  },
31
40
  "engines": {
32
41
  "opencode": ">=1.18.0",
33
42
  "bun": ">=1.3.5"
34
43
  },
35
44
  "files": [
36
- "src",
45
+ "dist",
46
+ "types",
37
47
  "README.md",
38
48
  "LICENSE"
39
49
  ],
@@ -41,16 +51,16 @@
41
51
  "access": "public"
42
52
  },
43
53
  "dependencies": {
44
- "@opencode-cockpit/client": "0.1.4",
45
- "@opencode-cockpit/daemon": "0.1.4",
46
- "@opencode-cockpit/protocol": "0.1.4",
47
- "@opencode-ai/plugin": "1.18.31",
54
+ "@opencode-cockpit/client": "0.1.5",
55
+ "@opencode-cockpit/daemon": "0.1.5",
56
+ "@opencode-cockpit/protocol": "0.1.5",
57
+ "@opencode-ai/plugin": "1.18.31"
58
+ },
59
+ "devDependencies": {
60
+ "zod": "4.6.5",
48
61
  "@opentui/core": "0.4.5",
49
62
  "@opentui/keymap": "0.4.5",
50
63
  "@opentui/solid": "0.4.5",
51
64
  "solid-js": "1.9.12"
52
- },
53
- "devDependencies": {
54
- "zod": "4.6.5"
55
65
  }
56
66
  }
@@ -0,0 +1,9 @@
1
+ import { CockpitClient } from "@opencode-cockpit/client";
2
+ /** Resolves the daemon entry shipped with this package. */
3
+ export declare function daemonEntry(): string;
4
+ /**
5
+ * Inside OpenCode `process.execPath` is the OpenCode binary; the client starts the daemon with
6
+ * BUN_BE_BUN=1 so it runs on OpenCode's embedded Bun (ADR 0001). The expected build lets the
7
+ * client replace a daemon left running from older plugin code.
8
+ */
9
+ export declare function createClient(name: string): CockpitClient;
@@ -0,0 +1,12 @@
1
+ import type { Plugin, PluginModule } from "@opencode-ai/plugin";
2
+ export declare const SHELL_PACKAGE = "@opencode-cockpit/shell";
3
+ export interface ShellServerOptions {
4
+ /** Package that loaded Shell, reported when a duplicate copy is skipped. */
5
+ source?: string;
6
+ }
7
+ /** Shell's server half as a factory, so bundles such as `opencode-cockpit` can include it. */
8
+ export declare function createShellServer({ source }?: ShellServerOptions): Plugin;
9
+ declare const plugin: PluginModule & {
10
+ id: string;
11
+ };
12
+ export default plugin;
@@ -0,0 +1,32 @@
1
+ import type { ShellInfo } from "@opencode-cockpit/protocol/shell";
2
+ /** The command as written, without the `$SHELL -c` wrapper. */
3
+ export declare function commandOf(s: ShellInfo): string;
4
+ export type StatusFilter = "running" | "failed" | "finished" | "any";
5
+ export type SessionFilter = "this" | "others" | "any";
6
+ export interface ShellFilter {
7
+ /** Case-insensitive text found in the name or the command. */
8
+ query?: string;
9
+ status?: StatusFilter;
10
+ session?: SessionFilter;
11
+ /** The asking agent's session, for `session` filtering. */
12
+ currentSession?: string;
13
+ }
14
+ export declare function isFailed(s: ShellInfo): boolean;
15
+ export declare function filterShells(list: readonly ShellInfo[], filter: ShellFilter): ShellInfo[];
16
+ export type NameMatch = {
17
+ kind: "found";
18
+ shell: ShellInfo;
19
+ alsoMatched: ShellInfo[];
20
+ } | {
21
+ kind: "ambiguous";
22
+ candidates: ShellInfo[];
23
+ } | {
24
+ kind: "none";
25
+ available: ShellInfo[];
26
+ };
27
+ /**
28
+ * Finds the shell a name refers to. Exact names (ignoring case) beat partial matches on name or
29
+ * command. When several match, a single running shell is the obvious intent (earlier finished
30
+ * shells with the same name are history); otherwise the caller must choose.
31
+ */
32
+ export declare function matchByName(list: readonly ShellInfo[], name: string): NameMatch;
@@ -0,0 +1,8 @@
1
+ import type { LogLine, ReadResult, ShellInfo, WaitResult } from "@opencode-cockpit/protocol/shell";
2
+ /** Compact log lines for a model: numbered, long lines cut, consecutive repeats collapsed. */
3
+ export declare function formatLines(lines: LogLine[]): string;
4
+ export declare function describeStatus(info: ShellInfo): string;
5
+ export declare function header(info: ShellInfo): string;
6
+ export declare function formatRead(info: ShellInfo, page: ReadResult, empty?: string): string;
7
+ export declare function formatWait(result: WaitResult, timeoutSeconds: number): string;
8
+ export declare function duration(ms: number): string;
@@ -0,0 +1,17 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { CockpitClient } from "@opencode-cockpit/client";
3
+ export interface ToolDeps {
4
+ client: CockpitClient;
5
+ /** Identifies this OpenCode instance so only it notifies the owning session. */
6
+ instance: string;
7
+ /** Shells whose exit should not message the agent (it stopped them itself, or opted out). */
8
+ quiet: Set<string>;
9
+ shellCommand(command: string): {
10
+ command: string;
11
+ args: string[];
12
+ };
13
+ env(): Record<string, string>;
14
+ /** Human title of an OpenCode session, for telling agents which session started a shell. */
15
+ sessionTitle?(sessionID: string): Promise<string | undefined>;
16
+ }
17
+ export declare function createTools(deps: ToolDeps): Record<string, ToolDefinition>;
@@ -0,0 +1,3 @@
1
+ /** Translates named keys (`enter`, `ctrl+c`, `up`) into the bytes a terminal would send. */
2
+ export declare function encodeKey(name: string): string;
3
+ export declare const KEY_NAMES: string[];
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
3
+ import type { ShellInfo } from "@opencode-cockpit/protocol/shell";
4
+ /** Status pill: label on a coloured background, readable in any font and colour scheme. */
5
+ export declare function Badge(props: {
6
+ api: TuiPluginApi;
7
+ shell: ShellInfo;
8
+ frame: number;
9
+ }): import("solid-js").JSX.Element;
@@ -0,0 +1,16 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
3
+ import { type ShellStore } from "./store.ts";
4
+ export interface ConsoleProps {
5
+ api: TuiPluginApi;
6
+ store: ShellStore;
7
+ /** Start in typing mode. */
8
+ typing?: boolean;
9
+ onClose: () => void;
10
+ onNewShell: () => void;
11
+ }
12
+ /**
13
+ * Keyboard-first shell console in an overlay. Normal mode: single-key actions. Typing mode:
14
+ * every key goes to the program (ctrl+c included); ctrl+] returns to normal mode.
15
+ */
16
+ export declare function Console(props: ConsoleProps): import("solid-js").JSX.Element;
@@ -0,0 +1,12 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
3
+ import type { ShellStore } from "./store.ts";
4
+ export interface DockProps {
5
+ api: TuiPluginApi;
6
+ store: ShellStore;
7
+ height: number;
8
+ hint: () => string;
9
+ onOpenConsole: (id: string) => void;
10
+ }
11
+ /** Split pane under the chat: status tabs for shells and the selected shell's live screen. */
12
+ export declare function Dock(props: DockProps): import("solid-js").JSX.Element;
@@ -0,0 +1,19 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { type TuiPlugin, type TuiPluginModule } from "@opencode-ai/plugin/tui";
3
+ export interface ShellTuiOptions {
4
+ dockHeight?: number;
5
+ /** Shell rows the sidebar shows before folding the rest away (default 5). */
6
+ sidebarRows?: number;
7
+ /** Failures stay visible this long after they end (default 30). */
8
+ historyMinutes?: number;
9
+ dockOpen?: boolean;
10
+ keybinds?: Record<string, string>;
11
+ }
12
+ /** Shell's TUI half as a factory, so bundles such as `opencode-cockpit` can include it. */
13
+ export declare function createShellTui({ source }?: {
14
+ source?: string;
15
+ }): TuiPlugin;
16
+ declare const plugin: TuiPluginModule & {
17
+ id: string;
18
+ };
19
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ import type { KeyEvent } from "@opentui/core";
2
+ /**
3
+ * Re-encodes a parsed key as legacy terminal bytes. The TUI may receive keys via the kitty
4
+ * protocol, whose raw form programs in the PTY would not understand.
5
+ */
6
+ export declare function keyToBytes(e: KeyEvent): string | undefined;
7
+ /** ctrl+] releases typing mode (the telnet escape), since esc and ctrl+c belong to the program. */
8
+ export declare function isReleaseKey(e: KeyEvent): boolean;
@@ -0,0 +1,14 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
3
+ import type { ShellStore } from "./store.ts";
4
+ export interface SidebarProps {
5
+ api: TuiPluginApi;
6
+ store: ShellStore;
7
+ /** Rows shown before the rest folds away; the sidebar is a narrow, shared column. */
8
+ rows?: number;
9
+ /** Rows shown while expanded, so a hundred shells can never push the sidebar over. */
10
+ expandedRows?: number;
11
+ onOpen: (id: string) => void;
12
+ consoleShortcut: () => string;
13
+ }
14
+ export declare function SidebarShells(props: SidebarProps): import("solid-js").JSX.Element;
@@ -0,0 +1,46 @@
1
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
2
+ import type { CockpitClient } from "@opencode-cockpit/client";
3
+ import type { ShellInfo } from "@opencode-cockpit/protocol/shell";
4
+ import { type Accessor } from "solid-js";
5
+ export interface ShellStore {
6
+ client: CockpitClient;
7
+ project: () => string;
8
+ /** Every shell in the project, unordered. */
9
+ shells: Accessor<ShellInfo[]>;
10
+ /** Ordered and folded for display: running, recent failures, plus the selection. */
11
+ visible: Accessor<ShellInfo[]>;
12
+ hidden: Accessor<ShellInfo[]>;
13
+ showAll: Accessor<boolean>;
14
+ toggleAll(): void;
15
+ connected: Accessor<boolean>;
16
+ now: Accessor<number>;
17
+ /** Spinner frame index; advances only while something is running. */
18
+ frame: Accessor<number>;
19
+ selected: Accessor<ShellInfo | undefined>;
20
+ select(id: string): void;
21
+ step(delta: number): void;
22
+ refresh(): Promise<void>;
23
+ clearFinished(): Promise<number>;
24
+ dispose(): void;
25
+ }
26
+ export interface StoreOptions {
27
+ /** Failures stay in the default view this long after they end. */
28
+ historyMinutes?: number;
29
+ }
30
+ export declare function createShellStore(api: TuiPluginApi, client: CockpitClient, options?: StoreOptions): ShellStore;
31
+ /**
32
+ * Live terminal view of one shell: attaches for change notifications and re-renders the daemon's
33
+ * emulated screen, throttled. Must be called inside a reactive owner.
34
+ */
35
+ export declare function useScreen(store: ShellStore, id: Accessor<string | undefined>): {
36
+ screen: Accessor<{
37
+ text: string;
38
+ cols: number;
39
+ rows: number;
40
+ cursor: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ } | undefined>;
45
+ refetch: () => void | "" | undefined;
46
+ };
@@ -0,0 +1,67 @@
1
+ import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui";
2
+ import type { ShellInfo } from "@opencode-cockpit/protocol/shell";
3
+ /** What a human cares about, derived from status + exit code so every surface agrees. */
4
+ export type Kind = "run" | "fail" | "stop" | "done";
5
+ export declare function kindOf(s: ShellInfo): Kind;
6
+ export declare const BADGE_LABEL: Record<Kind, string>;
7
+ /** Braille spinner: single-width in every terminal font. */
8
+ export declare const SPINNER: string[];
9
+ export declare function kindColor(theme: TuiThemeCurrent, kind: Kind): import("@opentui/core").RGBA;
10
+ /** Fixed 7-column pill so lists line up: " ⠹ RUN ", " FAIL ". */
11
+ export declare function badgeText(kind: Kind, frame?: number): string;
12
+ /** Running first (oldest first, stable tabs), then failures, stopped, done (most recent first). */
13
+ export declare function order(list: readonly ShellInfo[]): ShellInfo[];
14
+ export interface PartitionOptions {
15
+ showAll: boolean;
16
+ /** Failures stay visible this long after they end. */
17
+ historyMs: number;
18
+ now: number;
19
+ /** Always visible, so the selection never disappears under the user. */
20
+ keep?: string;
21
+ }
22
+ /** Default view: running shells and recent failures. Everything else folds into a "N more" chip. */
23
+ export declare function partition(list: readonly ShellInfo[], opts: PartitionOptions): {
24
+ visible: {
25
+ id: string;
26
+ title: string;
27
+ command: string;
28
+ args: string[];
29
+ cwd: string;
30
+ owner: {
31
+ project: string;
32
+ session?: string | undefined;
33
+ instance?: string | undefined;
34
+ };
35
+ status: "exited" | "failed" | "killed" | "running";
36
+ run: number;
37
+ pid?: number | undefined;
38
+ exitCode?: number | undefined;
39
+ signal?: string | undefined;
40
+ error?: string | undefined;
41
+ summary?: string | undefined;
42
+ startedAt: number;
43
+ endedAt?: number | undefined;
44
+ cols: number;
45
+ rows: number;
46
+ lines: {
47
+ first: number;
48
+ last: number;
49
+ };
50
+ bytes: number;
51
+ }[];
52
+ hidden: ShellInfo[];
53
+ };
54
+ export declare function statusDetail(s: ShellInfo, now: number): string;
55
+ /** Compact detail for narrow lists. */
56
+ export declare function shortDetail(s: ShellInfo, now: number): string;
57
+ /** Coarse relative time: "just now", "12s ago", "9m ago", "3h ago". */
58
+ export declare function since(ms: number): string;
59
+ /** The command as the user or agent wrote it, without the `$SHELL -c` wrapper. */
60
+ export declare function displayCommand(s: ShellInfo): string;
61
+ /** Folder relative to the project: "" at the root, "./sub" inside, "~/x" elsewhere under home. */
62
+ export declare function relativeCwd(cwd: string, project: string, home?: string): string;
63
+ /** Hard-wraps to `width`, at most `maxLines`; the last line ends with … when text was cut. */
64
+ export declare function wrapText(text: string, width: number, maxLines: number): string[];
65
+ /** Last `rows` lines of screen text, each cut to `cols`. */
66
+ export declare function tailLines(text: string | undefined, rows: number, cols: number): string;
67
+ export declare function truncate(text: string, max: number): string;