opencode-codeops 2.1.1 → 2.2.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/CHANGELOG.md +87 -0
- package/README.md +11 -0
- package/bin/lib/codeops-progress.d.mts +236 -0
- package/bin/lib/codeops-progress.mjs +564 -0
- package/bin/lib/codeops-rpc.d.mts +160 -0
- package/bin/lib/codeops-rpc.mjs +177 -0
- package/package.json +25 -2
- package/plugin/index.ts +55 -2
- package/plugin/tui.js +113 -0
- package/plugin/tui.tsx +112 -0
- package/scripts/__pycache__/codeops_plan.cpython-312.pyc +0 -0
- package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
- package/scripts/build-tui.mjs +73 -0
- package/scripts/codeops_plan.py +67 -3
- package/scripts/codeops_plan_migrate.py +17 -3
- package/skills/exec-plan/SKILL.md +5 -0
- package/skills/exec-plan/execution-protocol.md +29 -0
- package/plugin/temp-lifecycle.spec.test.mjs +0 -55
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for the CodeOps RPC helper.
|
|
3
|
+
*
|
|
4
|
+
* The helper is plain JavaScript so `node --test` can exercise it directly;
|
|
5
|
+
* these declarations give the TypeScript plugin entries typed access. The
|
|
6
|
+
* definition carries the status method, the progress method, and the
|
|
7
|
+
* updated/cleared events.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CodeOpsProgressRuntime,
|
|
12
|
+
CodeOpsRunClearedSchema,
|
|
13
|
+
CodeOpsRunState,
|
|
14
|
+
CodeOpsRunStateSchema,
|
|
15
|
+
} from "./codeops-progress.mjs"
|
|
16
|
+
|
|
17
|
+
/** JSON-Schema shape of the empty input accepted by the `status` method. */
|
|
18
|
+
export type CodeOpsRpcStatusInput = {
|
|
19
|
+
readonly type: "object"
|
|
20
|
+
readonly additionalProperties: false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** JSON-Schema shape of the payload the `status` method returns. */
|
|
24
|
+
export type CodeOpsRpcStatusOutput = {
|
|
25
|
+
readonly type: "object"
|
|
26
|
+
readonly additionalProperties: false
|
|
27
|
+
readonly required: readonly ["pluginVersion", "openCodeVersion", "directory"]
|
|
28
|
+
readonly properties: {
|
|
29
|
+
readonly pluginVersion: { readonly type: "string" }
|
|
30
|
+
readonly openCodeVersion: { readonly type: "string" }
|
|
31
|
+
readonly directory: { readonly type: "string" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The status payload after runtime validation. */
|
|
36
|
+
export interface CodeOpsStatus {
|
|
37
|
+
readonly pluginVersion: string
|
|
38
|
+
readonly openCodeVersion: string
|
|
39
|
+
readonly directory: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** JSON-Schema shape of the empty input accepted by the `progress` method. */
|
|
43
|
+
export type CodeOpsRpcProgressInput = {
|
|
44
|
+
readonly type: "object"
|
|
45
|
+
readonly additionalProperties: false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** JSON-Schema shape of the `progress` method's payload: a snapshot or null. */
|
|
49
|
+
export type CodeOpsRpcProgressOutput = {
|
|
50
|
+
readonly anyOf: readonly [CodeOpsRunStateSchema, { readonly type: "null" }]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The `updated` event of the codeops RPC: the full run snapshot. */
|
|
54
|
+
export interface CodeOpsRpcUpdatedEvent {
|
|
55
|
+
readonly schema: CodeOpsRunStateSchema
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The `cleared` event of the codeops RPC: the cleared run's identity. */
|
|
59
|
+
export interface CodeOpsRpcClearedEvent {
|
|
60
|
+
readonly schema: CodeOpsRunClearedSchema
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The portable RPC definition shared by the server plugin and the TUI entry. */
|
|
64
|
+
export interface CodeOpsRpcDefinition {
|
|
65
|
+
readonly id: "codeops"
|
|
66
|
+
readonly methods: {
|
|
67
|
+
readonly status: {
|
|
68
|
+
readonly input: CodeOpsRpcStatusInput
|
|
69
|
+
readonly output: CodeOpsRpcStatusOutput
|
|
70
|
+
}
|
|
71
|
+
readonly progress: {
|
|
72
|
+
readonly input: CodeOpsRpcProgressInput
|
|
73
|
+
readonly output: CodeOpsRpcProgressOutput
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
readonly events: {
|
|
77
|
+
readonly updated: CodeOpsRpcUpdatedEvent
|
|
78
|
+
readonly cleared: CodeOpsRpcClearedEvent
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The shared definition object; plain data with no runtime imports. */
|
|
83
|
+
export declare const CodeOpsRpc: CodeOpsRpcDefinition
|
|
84
|
+
|
|
85
|
+
/** Options accepted by {@link registerCodeOpsRpc}. */
|
|
86
|
+
export interface RegisterCodeOpsRpcOptions {
|
|
87
|
+
/** Version of the installed package, reported by the status method. */
|
|
88
|
+
pluginVersion?: string
|
|
89
|
+
/** The run-state runtime the progress method serves and binds emission from. */
|
|
90
|
+
runtime?: CodeOpsProgressRuntime
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Feature-detect `ctx.rpc.register`, register the codeops RPC when present,
|
|
95
|
+
* and report success. Never throws.
|
|
96
|
+
*/
|
|
97
|
+
export declare function registerCodeOpsRpc(
|
|
98
|
+
ctx: unknown,
|
|
99
|
+
options?: RegisterCodeOpsRpcOptions
|
|
100
|
+
): Promise<boolean>
|
|
101
|
+
|
|
102
|
+
/** Validate an unknown RPC payload as a complete CodeOps status record. */
|
|
103
|
+
export declare function isCodeOpsStatus(value: unknown): value is CodeOpsStatus
|
|
104
|
+
|
|
105
|
+
/** The location a status call is scoped to. */
|
|
106
|
+
export interface CodeOpsLocation {
|
|
107
|
+
readonly directory: string
|
|
108
|
+
readonly workspaceID?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Call options forwarded to the status call. */
|
|
112
|
+
export interface CodeOpsCallOptions {
|
|
113
|
+
readonly location?: CodeOpsLocation
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Structural client shape {@link requestStatus} consumes. */
|
|
117
|
+
export interface CodeOpsStatusClient {
|
|
118
|
+
readonly rpc: (definition: CodeOpsRpcDefinition) => {
|
|
119
|
+
readonly status: (
|
|
120
|
+
input: Record<string, never>,
|
|
121
|
+
options?: CodeOpsCallOptions
|
|
122
|
+
) => Promise<unknown>
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Request the raw status payload from a host client, converting every
|
|
128
|
+
* transport failure into `undefined`.
|
|
129
|
+
*
|
|
130
|
+
* Pass the calling session's location in `options`: the host resolves RPC
|
|
131
|
+
* calls against it, and a call without one can miss the plugin's registration
|
|
132
|
+
* when the session's project differs from the host's default location.
|
|
133
|
+
*/
|
|
134
|
+
export declare function requestStatus(
|
|
135
|
+
client: CodeOpsStatusClient,
|
|
136
|
+
options?: CodeOpsCallOptions
|
|
137
|
+
): Promise<unknown>
|
|
138
|
+
|
|
139
|
+
/** Structural client shape {@link requestProgress} consumes. */
|
|
140
|
+
export interface CodeOpsProgressClient {
|
|
141
|
+
readonly rpc: (definition: CodeOpsRpcDefinition) => {
|
|
142
|
+
readonly progress: (
|
|
143
|
+
input: Record<string, never>,
|
|
144
|
+
options?: CodeOpsCallOptions
|
|
145
|
+
) => Promise<unknown>
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Request the current run snapshot from a host client, converting every
|
|
151
|
+
* transport failure into `null`.
|
|
152
|
+
*
|
|
153
|
+
* Pass the calling session's location in `options`: the host resolves RPC
|
|
154
|
+
* calls against it, and a call without one can miss the plugin's registration
|
|
155
|
+
* when the session's project differs from the host's default location.
|
|
156
|
+
*/
|
|
157
|
+
export declare function requestProgress(
|
|
158
|
+
client: CodeOpsProgressClient,
|
|
159
|
+
options?: CodeOpsCallOptions
|
|
160
|
+
): Promise<CodeOpsRunState | null>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeOps RPC: definition, registration guard, payload guard, and the
|
|
3
|
+
* progress snapshot helper.
|
|
4
|
+
*
|
|
5
|
+
* The definition is plain data — the SDK's `Rpc.define` performs identity and
|
|
6
|
+
* reserved-name validation only — so this module needs no runtime import of
|
|
7
|
+
* the SDK and `node --test` can exercise it directly. The definition carries
|
|
8
|
+
* the `status` method, the `progress` method that answers with the current run
|
|
9
|
+
* snapshot, and the `updated`/`cleared` events the sidebar follows. The
|
|
10
|
+
* registration guard keeps the surface safe on builds that do not implement
|
|
11
|
+
* custom RPCs: it never throws and reports success with a boolean. When the
|
|
12
|
+
* host returns a registration exposing `events.emit`, the guard binds the
|
|
13
|
+
* runtime's emission through it, so accepted reports push live updates.
|
|
14
|
+
*
|
|
15
|
+
* @module codeops-rpc
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { RunClearedSchema, RunStateSchema, isCodeOpsRunState } from "./codeops-progress.mjs"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The portable `codeops` RPC definition shared by the server plugin and the
|
|
22
|
+
* TUI entry: a `status` method with an empty-object input and a three-string
|
|
23
|
+
* payload, a `progress` method answering with the current run snapshot (or
|
|
24
|
+
* `null`), and the `updated`/`cleared` events that push every accepted change.
|
|
25
|
+
*/
|
|
26
|
+
export const CodeOpsRpc = {
|
|
27
|
+
id: "codeops",
|
|
28
|
+
methods: {
|
|
29
|
+
status: {
|
|
30
|
+
input: { type: "object", additionalProperties: false },
|
|
31
|
+
output: {
|
|
32
|
+
type: "object",
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
required: ["pluginVersion", "openCodeVersion", "directory"],
|
|
35
|
+
properties: {
|
|
36
|
+
pluginVersion: { type: "string" },
|
|
37
|
+
openCodeVersion: { type: "string" },
|
|
38
|
+
directory: { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
progress: {
|
|
43
|
+
input: { type: "object", additionalProperties: false },
|
|
44
|
+
output: { anyOf: [RunStateSchema, { type: "null" }] },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
events: {
|
|
48
|
+
updated: { schema: RunStateSchema },
|
|
49
|
+
cleared: { schema: RunClearedSchema },
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Coerce an optional host value to the string form the payload requires.
|
|
55
|
+
*
|
|
56
|
+
* @param value - Value reported by the host context.
|
|
57
|
+
* @returns The value as a string, or an empty string when it is absent.
|
|
58
|
+
*/
|
|
59
|
+
function asText(value) {
|
|
60
|
+
return value === undefined || value === null ? "" : String(value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register the codeops RPC on a plugin context, feature-detected and
|
|
65
|
+
* never-throwing.
|
|
66
|
+
*
|
|
67
|
+
* When a runtime is supplied, the `progress` method answers with its current
|
|
68
|
+
* snapshot and — if the returned registration exposes `events.emit` — the
|
|
69
|
+
* runtime binds its emission through that function, so every accepted report
|
|
70
|
+
* or clear pushes a live event.
|
|
71
|
+
*
|
|
72
|
+
* @param ctx - Server plugin context; only `rpc.register`, `app.version`, and
|
|
73
|
+
* `location.directory` are read.
|
|
74
|
+
* @param {{ pluginVersion?: string, runtime?: object }} [options] -
|
|
75
|
+
* Registration options.
|
|
76
|
+
* @returns `true` when the registration call completed, `false` when the host
|
|
77
|
+
* lacks the RPC API or the registration failed. Never throws.
|
|
78
|
+
*/
|
|
79
|
+
export async function registerCodeOpsRpc(ctx, { pluginVersion, runtime } = {}) {
|
|
80
|
+
if (typeof ctx?.rpc?.register !== "function") return false
|
|
81
|
+
try {
|
|
82
|
+
const registration = await ctx.rpc.register(CodeOpsRpc, {
|
|
83
|
+
async status() {
|
|
84
|
+
return {
|
|
85
|
+
pluginVersion: asText(pluginVersion),
|
|
86
|
+
openCodeVersion: asText(ctx?.app?.version),
|
|
87
|
+
directory: asText(ctx?.location?.directory),
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
// The host adapter requires handlers to return promises; a synchronous
|
|
91
|
+
// return fails the call. async even though nothing is awaited.
|
|
92
|
+
async progress() {
|
|
93
|
+
return runtime?.snapshot() ?? null
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
if (
|
|
97
|
+
runtime &&
|
|
98
|
+
typeof runtime.bindEmit === "function" &&
|
|
99
|
+
typeof registration?.events?.emit === "function"
|
|
100
|
+
) {
|
|
101
|
+
runtime.bindEmit(registration.events.emit)
|
|
102
|
+
}
|
|
103
|
+
return true
|
|
104
|
+
} catch {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate an unknown RPC payload as a complete CodeOps status record.
|
|
111
|
+
*
|
|
112
|
+
* @param value - Value received from the RPC.
|
|
113
|
+
* @returns `true` only for a plain object carrying exactly the three string
|
|
114
|
+
* fields of the contract.
|
|
115
|
+
*/
|
|
116
|
+
export function isCodeOpsStatus(value) {
|
|
117
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false
|
|
118
|
+
if (Object.keys(value).length !== 3) return false
|
|
119
|
+
return (
|
|
120
|
+
typeof value.pluginVersion === "string" &&
|
|
121
|
+
typeof value.openCodeVersion === "string" &&
|
|
122
|
+
typeof value.directory === "string"
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Request the codeops status payload from a host client, converting every
|
|
128
|
+
* transport failure into `undefined`.
|
|
129
|
+
*
|
|
130
|
+
* A caller can therefore render from the result without a `catch`: a host
|
|
131
|
+
* without the RPC, a server plugin that failed to register, and a failed call
|
|
132
|
+
* all collapse into the same "no status" outcome. The payload is returned as
|
|
133
|
+
* received; narrow it with {@link isCodeOpsStatus} before use.
|
|
134
|
+
*
|
|
135
|
+
* Pass the calling session's location in `options`. The host resolves RPC
|
|
136
|
+
* calls against a location, and a call without one can miss the plugin's
|
|
137
|
+
* registration — the default location does not necessarily match a project
|
|
138
|
+
* opened through a path argument.
|
|
139
|
+
*
|
|
140
|
+
* @param client - Host client exposing `rpc(definition).status({}, options)`.
|
|
141
|
+
* @param {{ location?: { directory?: string, workspaceID?: string } }} [options]
|
|
142
|
+
* - Call options forwarded to the status call.
|
|
143
|
+
* @returns The raw status payload, or `undefined` when the call fails.
|
|
144
|
+
*/
|
|
145
|
+
export async function requestStatus(client, options) {
|
|
146
|
+
try {
|
|
147
|
+
return await client.rpc(CodeOpsRpc).status({}, options)
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Request the current run snapshot from a host client, converting every
|
|
155
|
+
* failure into `null`.
|
|
156
|
+
*
|
|
157
|
+
* Pass the calling session's location in `options`, for the same reason as
|
|
158
|
+
* {@link requestStatus}: the host resolves RPC calls against a location, and
|
|
159
|
+
* a call without one can miss the plugin's registration. The payload is
|
|
160
|
+
* validated before it is returned, so an invalid payload, a "no run" answer,
|
|
161
|
+
* and any transport failure all collapse into `null` and the caller can
|
|
162
|
+
* render without a `catch`.
|
|
163
|
+
*
|
|
164
|
+
* @param client - Host client exposing `rpc(definition).progress({}, options)`.
|
|
165
|
+
* @param {{ location?: { directory?: string, workspaceID?: string } }} [options]
|
|
166
|
+
* - Call options forwarded to the progress call.
|
|
167
|
+
* @returns The validated snapshot, or `null` when no run is active or the
|
|
168
|
+
* call failed. Never throws.
|
|
169
|
+
*/
|
|
170
|
+
export async function requestProgress(client, options) {
|
|
171
|
+
try {
|
|
172
|
+
const payload = await client.rpc(CodeOpsRpc).progress({}, options)
|
|
173
|
+
return isCodeOpsRunState(payload) ? payload : null
|
|
174
|
+
} catch {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codeops",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
"main": "./plugin/index.ts",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./plugin/index.ts",
|
|
13
|
-
"./server": "./plugin/index.ts"
|
|
13
|
+
"./server": "./plugin/index.ts",
|
|
14
|
+
"./tui": "./plugin/tui.js"
|
|
14
15
|
},
|
|
15
16
|
"bin": {
|
|
16
17
|
"opencode-codeops": "bin/index.mjs"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
20
|
"test": "node --test",
|
|
21
|
+
"build:tui": "node scripts/build-tui.mjs",
|
|
20
22
|
"typecheck": "tsc --noEmit",
|
|
21
23
|
"check:version": "node scripts/check-version.mjs",
|
|
22
24
|
"verify": "npm run typecheck && npm run test && npm run check:version",
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
},
|
|
25
27
|
"files": [
|
|
26
28
|
"plugin/",
|
|
29
|
+
"!plugin/*.test.mjs",
|
|
30
|
+
"!plugin/*.spec.test.mjs",
|
|
27
31
|
"skills/",
|
|
28
32
|
"_shared/",
|
|
29
33
|
"standards/",
|
|
@@ -68,7 +72,26 @@
|
|
|
68
72
|
"@opencode/plugin": "^2.0.24"
|
|
69
73
|
},
|
|
70
74
|
"devDependencies": {
|
|
75
|
+
"@opentui/core": "0.5.17",
|
|
76
|
+
"@opentui/solid": "0.5.17",
|
|
71
77
|
"@types/node": "^22.20.2",
|
|
78
|
+
"solid-js": "1.9.12",
|
|
72
79
|
"typescript": "^5.0.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"@opentui/core": ">=0.5.14",
|
|
83
|
+
"@opentui/solid": ">=0.5.14",
|
|
84
|
+
"solid-js": ">=1.9.0"
|
|
85
|
+
},
|
|
86
|
+
"peerDependenciesMeta": {
|
|
87
|
+
"@opentui/core": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@opentui/solid": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"solid-js": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
73
96
|
}
|
|
74
97
|
}
|
package/plugin/index.ts
CHANGED
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
cleanStaleTmpDirs,
|
|
10
10
|
ensureSessionTmpDir,
|
|
11
11
|
} from "../bin/lib/tmp-hygiene.mjs"
|
|
12
|
+
import {
|
|
13
|
+
createCodeOpsProgress,
|
|
14
|
+
registerCodeOpsProgressTool,
|
|
15
|
+
} from "../bin/lib/codeops-progress.mjs"
|
|
16
|
+
import { registerCodeOpsRpc } from "../bin/lib/codeops-rpc.mjs"
|
|
12
17
|
import {
|
|
13
18
|
appendEffortTrace,
|
|
14
19
|
applyEffort,
|
|
@@ -223,6 +228,11 @@ export default Plugin.define({
|
|
|
223
228
|
const warnedEffortLevels = new Set<string>()
|
|
224
229
|
const effortTraceEnabled = isEffortTraceEnabled(process.env.CODEOPS_EFFORT_TRACE)
|
|
225
230
|
|
|
231
|
+
// The live run state is memory-only, for the lifetime of this plugin
|
|
232
|
+
// instance: it is never persisted and clears when the reporting session
|
|
233
|
+
// ends, so it can never outlive the run it describes.
|
|
234
|
+
const progress = createCodeOpsProgress()
|
|
235
|
+
|
|
226
236
|
// Sweep scratch directories abandoned by earlier interrupted runs, once.
|
|
227
237
|
// Cleanup is best effort: a failure must never block a session.
|
|
228
238
|
try {
|
|
@@ -234,8 +244,9 @@ export default Plugin.define({
|
|
|
234
244
|
warnOnVersionSkew(directory)
|
|
235
245
|
|
|
236
246
|
// ---------------------------------------------------------------------
|
|
237
|
-
// Session lifecycle: drop captured markers
|
|
238
|
-
// a long-lived runtime does not remember
|
|
247
|
+
// Session lifecycle: drop captured markers and the session's live run
|
|
248
|
+
// when a session is deleted, so a long-lived runtime does not remember
|
|
249
|
+
// ended sessions.
|
|
239
250
|
// ---------------------------------------------------------------------
|
|
240
251
|
const controller = new AbortController()
|
|
241
252
|
void (async () => {
|
|
@@ -243,6 +254,7 @@ export default Plugin.define({
|
|
|
243
254
|
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
244
255
|
if (event.type === "session.deleted") {
|
|
245
256
|
effortMarkers.delete(event.data.sessionID)
|
|
257
|
+
progress.clearSession(event.data.sessionID)
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
260
|
} catch {
|
|
@@ -384,6 +396,47 @@ export default Plugin.define({
|
|
|
384
396
|
}
|
|
385
397
|
})
|
|
386
398
|
|
|
399
|
+
// ---------------------------------------------------------------------
|
|
400
|
+
// CodeOps RPC: expose the plugin identity and the live run snapshot for
|
|
401
|
+
// the sidebar. Registration is feature-detected and never blocks the
|
|
402
|
+
// plugin: builds without custom RPCs keep working unchanged, and the
|
|
403
|
+
// sidebar simply stays hidden. When the host hands back an event emitter,
|
|
404
|
+
// the registration binds it into the runtime, so accepted reports and
|
|
405
|
+
// clears push live events to connected clients.
|
|
406
|
+
// ---------------------------------------------------------------------
|
|
407
|
+
try {
|
|
408
|
+
const registered = await registerCodeOpsRpc(ctx, {
|
|
409
|
+
pluginVersion: packageVersion,
|
|
410
|
+
runtime: progress,
|
|
411
|
+
})
|
|
412
|
+
if (!registered) {
|
|
413
|
+
warnContentFree(
|
|
414
|
+
"The codeops status RPC is unavailable in this OpenCode build; " +
|
|
415
|
+
"the sidebar status stays hidden."
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
warnContentFree("Could not register the codeops status RPC.")
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------
|
|
423
|
+
// Progress tool: let the agent report live task progress for the sidebar.
|
|
424
|
+
// The tool registers behind feature-detection; on every failure the
|
|
425
|
+
// plugin, the agent loop, and the Markdown execution plan keep working
|
|
426
|
+
// exactly as before.
|
|
427
|
+
// ---------------------------------------------------------------------
|
|
428
|
+
try {
|
|
429
|
+
const registered = await registerCodeOpsProgressTool(ctx, progress)
|
|
430
|
+
if (!registered) {
|
|
431
|
+
warnContentFree(
|
|
432
|
+
"The codeops progress tool is unavailable in this OpenCode build; " +
|
|
433
|
+
"the sidebar stays hidden."
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
warnContentFree("Could not register the codeops progress tool.")
|
|
438
|
+
}
|
|
439
|
+
|
|
387
440
|
// ---------------------------------------------------------------------
|
|
388
441
|
// Plugin cleanup: stop listening for events. The runtime's scratch
|
|
389
442
|
// directory is deliberately left in place — other sessions may still be
|
package/plugin/tui.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Generated from plugin/tui.tsx — do not edit; run `npm run build:tui` to regenerate.
|
|
2
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
3
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
4
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
5
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
6
|
+
/**
|
|
7
|
+
* The CodeOps TUI entry: the live run-progress view in the session sidebar.
|
|
8
|
+
*
|
|
9
|
+
* The view is deliberately honest: it shows only reported values from the
|
|
10
|
+
* server's run state, always with an as-of time, and it renders nothing when
|
|
11
|
+
* there is no run, when the host lacks the events API, or when every call
|
|
12
|
+
* fails. There are no timers — the text recomputes whenever the component
|
|
13
|
+
* re-renders — and no file access; all display and merge logic lives in the
|
|
14
|
+
* shared helper module so it can be tested under plain Node.
|
|
15
|
+
*
|
|
16
|
+
* @module tui
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Plugin, usePlugin } from "@opencode/plugin/tui";
|
|
20
|
+
import { createEffect, createResource, createSignal, onCleanup, Show } from "solid-js";
|
|
21
|
+
import { acceptRunCleared, acceptRunUpdate, describeRun, mergeRunState } from "../bin/lib/codeops-progress.mjs";
|
|
22
|
+
import { CodeOpsRpc, requestProgress } from "../bin/lib/codeops-rpc.mjs";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The live run-progress view.
|
|
26
|
+
*
|
|
27
|
+
* Resolves the session's location once, subscribes to the `updated` and
|
|
28
|
+
* `cleared` events before requesting an initial snapshot, and merges both
|
|
29
|
+
* delivery paths monotonically. A clear records its server-stamped
|
|
30
|
+
* `clearedAt`; both merge paths ignore deliveries at or before it, so a late
|
|
31
|
+
* snapshot or update can never resurrect a cleared run. Every failure path is
|
|
32
|
+
* silent: a host without the events API still renders from the snapshot, and
|
|
33
|
+
* an invalid or missing snapshot renders nothing.
|
|
34
|
+
*
|
|
35
|
+
* @returns The three display lines, or nothing when no run is known.
|
|
36
|
+
*/
|
|
37
|
+
function CodeOpsProgress() {
|
|
38
|
+
const context = usePlugin();
|
|
39
|
+
const location = context.location ?? context.data.location.default();
|
|
40
|
+
const directory = location?.directory ?? "";
|
|
41
|
+
const [run, setRun] = createSignal(null);
|
|
42
|
+
const [clearedAt, setClearedAt] = createSignal(0);
|
|
43
|
+
|
|
44
|
+
// Subscribe BEFORE the snapshot: events are live-only, so a snapshot
|
|
45
|
+
// fetched first could miss an update arriving in between. Guarded: a host
|
|
46
|
+
// without the events API still renders from the snapshot below.
|
|
47
|
+
const stops = [];
|
|
48
|
+
try {
|
|
49
|
+
const events = context.client.rpc(CodeOpsRpc).events;
|
|
50
|
+
const stopUpdated = events.on("updated", event => {
|
|
51
|
+
const snapshot = acceptRunUpdate(event, directory);
|
|
52
|
+
if (snapshot && snapshot.updatedAt > clearedAt()) {
|
|
53
|
+
setRun(current => mergeRunState(current, snapshot));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
stops.push(stopUpdated);
|
|
57
|
+
const stopCleared = events.on("cleared", event => {
|
|
58
|
+
const cleared = acceptRunCleared(event, directory);
|
|
59
|
+
if (!cleared) return;
|
|
60
|
+
// Record the clear time for every accepted clear, before the identity
|
|
61
|
+
// gate: a clear that arrives before the initial snapshot must still
|
|
62
|
+
// suppress that snapshot, or a cleared run could reappear.
|
|
63
|
+
setClearedAt(cleared.clearedAt);
|
|
64
|
+
const current = run();
|
|
65
|
+
if (current && cleared.plan === current.plan && cleared.sessionID === current.sessionID) {
|
|
66
|
+
setRun(null);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
stops.push(stopCleared);
|
|
70
|
+
} catch {
|
|
71
|
+
// Events unavailable: the snapshot-only view below still renders.
|
|
72
|
+
}
|
|
73
|
+
onCleanup(() => stops.forEach(stop => stop()));
|
|
74
|
+
|
|
75
|
+
// Initial snapshot; null or a failure means "nothing known". The clear
|
|
76
|
+
// guard applies here too: a snapshot at or before the last clear is ignored.
|
|
77
|
+
const [initial] = createResource(async () => requestProgress(context.client, {
|
|
78
|
+
location
|
|
79
|
+
}));
|
|
80
|
+
createEffect(() => {
|
|
81
|
+
const snapshot = initial();
|
|
82
|
+
if (snapshot && snapshot.updatedAt > clearedAt()) {
|
|
83
|
+
setRun(current => mergeRunState(current, snapshot));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return _$createComponent(Show, {
|
|
87
|
+
get when() {
|
|
88
|
+
return run();
|
|
89
|
+
},
|
|
90
|
+
children: snapshot => {
|
|
91
|
+
const view = () => describeRun(snapshot(), Date.now());
|
|
92
|
+
return _$memo(() => view().lines.map(line => (() => {
|
|
93
|
+
var _el$ = _$createElement("text");
|
|
94
|
+
_$insert(_el$, line);
|
|
95
|
+
return _el$;
|
|
96
|
+
})()));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The opencode-codeops TUI plugin: claims the sidebar content slot with the
|
|
103
|
+
* live run view. The host disposes the claim when the plugin unloads.
|
|
104
|
+
*/
|
|
105
|
+
export default Plugin.define({
|
|
106
|
+
id: "opencode-codeops-tui",
|
|
107
|
+
setup(context) {
|
|
108
|
+
return context.ui.slot({
|
|
109
|
+
append: "sidebar.content",
|
|
110
|
+
render: () => _$createComponent(CodeOpsProgress, {})
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|