opencode-cockpit 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.
- package/README.md +41 -78
- package/package.json +7 -34
- package/src/compose.ts +43 -0
- package/src/features.ts +34 -0
- package/src/server.ts +13 -95
- package/src/tui.ts +13 -0
- package/src/connect.ts +0 -24
- package/src/tools/format.ts +0 -78
- package/src/tools/index.ts +0 -382
- package/src/tools/keys.ts +0 -33
- package/src/tui/badge.tsx +0 -17
- package/src/tui/console.tsx +0 -438
- package/src/tui/dock.tsx +0 -130
- package/src/tui/index.tsx +0 -264
- package/src/tui/keys.ts +0 -52
- package/src/tui/sidebar.tsx +0 -44
- package/src/tui/store.ts +0 -185
- package/src/tui/view.ts +0 -161
package/src/tui/view.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
2
|
-
import type { ShellInfo } from "@opencode-cockpit/protocol/shell"
|
|
3
|
-
import { duration } from "../tools/format.ts"
|
|
4
|
-
|
|
5
|
-
/** What a human cares about, derived from status + exit code so every surface agrees. */
|
|
6
|
-
export type Kind = "run" | "fail" | "stop" | "done"
|
|
7
|
-
|
|
8
|
-
export function kindOf(s: ShellInfo): Kind {
|
|
9
|
-
if (s.status === "running") return "run"
|
|
10
|
-
if (s.status === "killed") return "stop"
|
|
11
|
-
if (s.status === "exited" && s.exitCode === 0) return "done"
|
|
12
|
-
return "fail"
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const BADGE_LABEL: Record<Kind, string> = { run: "RUN", fail: "FAIL", stop: "STOP", done: "DONE" }
|
|
16
|
-
|
|
17
|
-
/** Braille spinner: single-width in every terminal font. */
|
|
18
|
-
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
19
|
-
|
|
20
|
-
export function kindColor(theme: TuiThemeCurrent, kind: 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: Kind, frame = 0): string {
|
|
35
|
-
if (kind === "run") return ` ${SPINNER[frame % SPINNER.length]} RUN `
|
|
36
|
-
return ` ${BADGE_LABEL[kind].padEnd(4)} `
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const RANK: Record<Kind, number> = { run: 0, fail: 1, stop: 2, done: 3 }
|
|
40
|
-
|
|
41
|
-
/** Running first (oldest first, stable tabs), then failures, stopped, done (most recent first). */
|
|
42
|
-
export function order(list: readonly ShellInfo[]): ShellInfo[] {
|
|
43
|
-
return [...list].sort((a, b) => {
|
|
44
|
-
const ka = kindOf(a)
|
|
45
|
-
const kb = kindOf(b)
|
|
46
|
-
if (ka !== kb) return RANK[ka] - RANK[kb]
|
|
47
|
-
if (ka === "run") return a.startedAt - b.startedAt
|
|
48
|
-
return (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt)
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface PartitionOptions {
|
|
53
|
-
showAll: boolean
|
|
54
|
-
/** Failures stay visible this long after they end. */
|
|
55
|
-
historyMs: number
|
|
56
|
-
now: number
|
|
57
|
-
/** Always visible, so the selection never disappears under the user. */
|
|
58
|
-
keep?: string
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Default view: running shells and recent failures. Everything else folds into a "N more" chip. */
|
|
62
|
-
export function partition(list: readonly ShellInfo[], opts: PartitionOptions) {
|
|
63
|
-
const ordered = order(list)
|
|
64
|
-
if (opts.showAll) return { visible: ordered, hidden: [] as ShellInfo[] }
|
|
65
|
-
const visible: ShellInfo[] = []
|
|
66
|
-
const hidden: ShellInfo[] = []
|
|
67
|
-
for (const s of ordered) {
|
|
68
|
-
const kind = kindOf(s)
|
|
69
|
-
const recent = opts.now - (s.endedAt ?? opts.now) <= opts.historyMs
|
|
70
|
-
if (kind === "run" || (kind === "fail" && recent) || s.id === opts.keep) visible.push(s)
|
|
71
|
-
else hidden.push(s)
|
|
72
|
-
}
|
|
73
|
-
return { visible, hidden }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function statusDetail(s: ShellInfo, now: number): string {
|
|
77
|
-
const kind = kindOf(s)
|
|
78
|
-
const ran = duration((s.endedAt ?? now) - s.startedAt)
|
|
79
|
-
const ago = s.endedAt ? since(now - s.endedAt) : ""
|
|
80
|
-
switch (kind) {
|
|
81
|
-
case "run":
|
|
82
|
-
return ran
|
|
83
|
-
case "done":
|
|
84
|
-
return `took ${ran} · ${ago}`
|
|
85
|
-
case "stop":
|
|
86
|
-
return `stopped after ${ran} · ${ago}`
|
|
87
|
-
case "fail":
|
|
88
|
-
if (s.status === "failed") return "could not start"
|
|
89
|
-
return `exit ${s.exitCode ?? "?"} after ${ran} · ${ago}`
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Compact detail for narrow lists. */
|
|
94
|
-
export function shortDetail(s: ShellInfo, now: number): string {
|
|
95
|
-
switch (kindOf(s)) {
|
|
96
|
-
case "run":
|
|
97
|
-
return duration(now - s.startedAt)
|
|
98
|
-
case "fail":
|
|
99
|
-
return s.status === "failed" ? "no start" : `exit ${s.exitCode ?? "?"}`
|
|
100
|
-
case "stop":
|
|
101
|
-
return "stopped"
|
|
102
|
-
case "done":
|
|
103
|
-
return s.endedAt ? since(now - s.endedAt) : "done"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Coarse relative time: "just now", "12s ago", "9m ago", "3h ago". */
|
|
108
|
-
export function since(ms: number): string {
|
|
109
|
-
const s = Math.floor(ms / 1000)
|
|
110
|
-
if (s < 5) return "just now"
|
|
111
|
-
if (s < 60) return `${s}s ago`
|
|
112
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`
|
|
113
|
-
if (s < 86_400) return `${Math.floor(s / 3600)}h ago`
|
|
114
|
-
return `${Math.floor(s / 86_400)}d ago`
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** The command as the user or agent wrote it, without the `$SHELL -c` wrapper. */
|
|
118
|
-
export function displayCommand(s: ShellInfo): string {
|
|
119
|
-
if (s.args.length === 2 && s.args[0] === "-c" && /(^|\/)(ba|z|fi|da|k)?sh$/.test(s.command))
|
|
120
|
-
return s.args[1] as string
|
|
121
|
-
return [s.command, ...s.args].join(" ")
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Folder relative to the project: "" at the root, "./sub" inside, "~/x" elsewhere under home. */
|
|
125
|
-
export function relativeCwd(cwd: string, project: string, home = process.env.HOME ?? ""): string {
|
|
126
|
-
const trim = (p: string) => p.replace(/\/+$/, "")
|
|
127
|
-
const c = trim(cwd)
|
|
128
|
-
const p = trim(project)
|
|
129
|
-
if (c === p) return ""
|
|
130
|
-
if (c.startsWith(`${p}/`)) return `./${c.slice(p.length + 1)}`
|
|
131
|
-
if (home && c.startsWith(`${trim(home)}/`)) return `~/${c.slice(trim(home).length + 1)}`
|
|
132
|
-
return c
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Hard-wraps to `width`, at most `maxLines`; the last line ends with … when text was cut. */
|
|
136
|
-
export function wrapText(text: string, width: number, maxLines: number): string[] {
|
|
137
|
-
const flat = text.replace(/\s*\n\s*/g, " ⏎ ")
|
|
138
|
-
const w = Math.max(4, width)
|
|
139
|
-
const lines: string[] = []
|
|
140
|
-
for (let i = 0; i < flat.length && lines.length < maxLines; i += w) lines.push(flat.slice(i, i + w))
|
|
141
|
-
if (lines.length === 0) return [""]
|
|
142
|
-
if (flat.length > w * maxLines) {
|
|
143
|
-
const last = lines[lines.length - 1] as string
|
|
144
|
-
lines[lines.length - 1] = `${last.slice(0, w - 1)}…`
|
|
145
|
-
}
|
|
146
|
-
return lines
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Last `rows` lines of screen text, each cut to `cols`. */
|
|
150
|
-
export function tailLines(text: string | undefined, rows: number, cols: number): string {
|
|
151
|
-
if (!text) return ""
|
|
152
|
-
const lines = text.split("\n")
|
|
153
|
-
return lines
|
|
154
|
-
.slice(Math.max(0, lines.length - rows))
|
|
155
|
-
.map((l) => (l.length > cols ? `${l.slice(0, Math.max(0, cols - 1))}…` : l))
|
|
156
|
-
.join("\n")
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function truncate(text: string, max: number): string {
|
|
160
|
-
return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text
|
|
161
|
-
}
|