@encoredigitalgroup/opencode 0.2.3 → 0.2.4

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  `@encoredigitalgroup/opencode` adds file pickers for OpenCode planning commands.
4
4
 
5
- The TUI provides `/plan-review`, `/plan-resync`, `/plan-split`, and `/plan-execute` commands. Each command opens a picker for plans in `docs/_planning` and inserts the
6
- selected plan into the prompt. The server entry attaches the matching installed skill to the prompt.
5
+ The TUI provides `/plan-review`, `/plan-resync`, `/plan-split`, and `/plan-execute` commands. Each command opens a picker for plans in `docs/_planning` and
6
+ inserts the selected plan into the prompt. The server entry attaches the matching installed skill to the prompt.
7
7
 
8
8
  ## Install
9
9
 
@@ -24,15 +24,15 @@ OpenCode v2 loads this package from the `plugins` array. Add an object entry wit
24
24
  }
25
25
  ```
26
26
 
27
- Restart OpenCode after changing the configuration. OpenCode downloads missing npm plugins that appear in the `plugins` array, so you do not need to install this package
28
- with Bun or npm yourself. This package supports OpenCode v2 only; replace the legacy `plugin` configuration entry when migrating from OpenCode v1.
27
+ Restart OpenCode after changing the configuration. OpenCode downloads missing npm plugins that appear in the `plugins` array, so you do not need to install this
28
+ package with Bun or npm yourself. This package supports OpenCode v2 only; replace the legacy `plugin` configuration entry when migrating from OpenCode v1.
29
29
 
30
30
  The plugin resolves the mapped skills through OpenCode, so those skills can live in any location that OpenCode discovers.
31
31
 
32
32
  ## Configuration
33
33
 
34
- Set `planDirectory` and `skillMappings` in the object's `options` field. `planDirectory` is a non-empty path relative to the workspace. `skillMappings` can override any
35
- supported command's installed skill name. Unspecified values use the defaults: `docs/_planning` and the command's existing skill name.
34
+ Set `planDirectory` and `skillMappings` in the object's `options` field. `planDirectory` is a non-empty path relative to the workspace. `skillMappings` can override
35
+ any supported command's installed skill name. Unspecified values use the defaults: `docs/_planning` and the command's existing skill name.
36
36
 
37
37
  ```json
38
38
  {
@@ -52,7 +52,21 @@ supported command's installed skill name. Unspecified values use the defaults: `
52
52
  }
53
53
  ```
54
54
 
55
- Launching a picker from an existing session submits to that session. Launching one from the home screen creates and displays a new session before submitting the selected plan.
55
+ Launching a picker from an existing session submits to that session. Launching one from the home screen creates and displays a new session before submitting the
56
+ selected plan.
57
+
58
+ ## Todo tracker
59
+
60
+ The plugin registers the direct `todotracker` tool. Each call replaces the current session's complete ordered todo list. Pass an empty `todos` array to clear the
61
+ list.
62
+ Each todo requires:
63
+
64
+ - `content`: a non-empty string containing at least one non-whitespace character.
65
+ - `status`: `pending`, `in_progress`, `completed`, or `cancelled`.
66
+ - `priority`: `high`, `medium`, or `low`.
67
+
68
+ The checklist renders only in the OpenCode terminal TUI sidebar. After a restart, it appears only when the completed `todotracker` result is in the host's loaded
69
+ message cache. That cache starts with the newest 20 messages, so older results are not shown until the host has loaded them.
56
70
 
57
71
  ## Development
58
72
 
@@ -0,0 +1,53 @@
1
+ import type { SessionMessageInfo } from "@opencode/client";
2
+ import type { Plugin as ServerPlugin } from "@opencode/plugin";
3
+ export type TodoStatus = "pending" | "in_progress" | "completed" | "cancelled";
4
+ export type TodoPriority = "high" | "medium" | "low";
5
+ export type Todo = {
6
+ content: string;
7
+ status: TodoStatus;
8
+ priority: TodoPriority;
9
+ };
10
+ export type TodoTrackerInput = {
11
+ todos: Todo[];
12
+ };
13
+ export type TodoTrackerResultMetadata = {
14
+ todos: Todo[];
15
+ };
16
+ export declare const todoTrackerInput: {
17
+ readonly type: "object";
18
+ readonly additionalProperties: false;
19
+ readonly required: readonly ["todos"];
20
+ readonly properties: {
21
+ readonly todos: {
22
+ readonly type: "array";
23
+ readonly items: {
24
+ readonly type: "object";
25
+ readonly additionalProperties: false;
26
+ readonly required: readonly ["content", "status", "priority"];
27
+ readonly properties: {
28
+ readonly content: {
29
+ readonly type: "string";
30
+ readonly minLength: 1;
31
+ readonly pattern: "\\S";
32
+ };
33
+ readonly status: {
34
+ readonly enum: readonly ["pending", "in_progress", "completed", "cancelled"];
35
+ };
36
+ readonly priority: {
37
+ readonly enum: readonly ["high", "medium", "low"];
38
+ };
39
+ };
40
+ };
41
+ };
42
+ };
43
+ };
44
+ export declare const todoTrackerName = "todotracker";
45
+ export declare const todoTrackerDescription: string;
46
+ export declare function isTodoTrackerInput(value: unknown): value is TodoTrackerInput;
47
+ export declare function isTodoResultMetadata(value: unknown): value is TodoTrackerResultMetadata;
48
+ export declare function todosFromMessages(messages: readonly SessionMessageInfo[]): readonly Todo[];
49
+ export declare class TodoTracker {
50
+ static marker(status: TodoStatus): string;
51
+ static sidebar(messages: readonly SessionMessageInfo[]): readonly Todo[];
52
+ static register(context: ServerPlugin.Context): Promise<import("@opencode/plugin/promise/registration").Registration>;
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -128,6 +128,123 @@ class SkillCommands {
128
128
  }
129
129
  }
130
130
 
131
+ // src/lib/TodoTracker.ts
132
+ var todoTrackerInput = {
133
+ type: "object",
134
+ additionalProperties: false,
135
+ required: ["todos"],
136
+ properties: {
137
+ todos: {
138
+ type: "array",
139
+ items: {
140
+ type: "object",
141
+ additionalProperties: false,
142
+ required: ["content", "status", "priority"],
143
+ properties: {
144
+ content: { type: "string", minLength: 1, pattern: "\\S" },
145
+ status: { enum: ["pending", "in_progress", "completed", "cancelled"] },
146
+ priority: { enum: ["high", "medium", "low"] }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ };
152
+ var todoTrackerName = "todotracker";
153
+ var todoTrackerDescription = "Replace the current session's complete ordered todo list. " + "Pass every remaining todo on each call; pass an empty array to clear the list.";
154
+ var todoStatuses = ["pending", "in_progress", "completed", "cancelled"];
155
+ var todoPriorities = ["high", "medium", "low"];
156
+ function isRecord(value) {
157
+ return typeof value === "object" && value !== null && !Array.isArray(value);
158
+ }
159
+ function hasOnlyKeys(value, keys) {
160
+ const expected = new Set(keys);
161
+ return Object.keys(value).length === keys.length && Object.keys(value).every((key) => expected.has(key));
162
+ }
163
+ function isTodoStatus(value) {
164
+ return typeof value === "string" && todoStatuses.includes(value);
165
+ }
166
+ function isTodoPriority(value) {
167
+ return typeof value === "string" && todoPriorities.includes(value);
168
+ }
169
+ function isTodo(value) {
170
+ if (!isRecord(value) || !hasOnlyKeys(value, ["content", "status", "priority"])) {
171
+ return false;
172
+ }
173
+ return typeof value.content === "string" && value.content.length >= 1 && /\S/.test(value.content) && isTodoStatus(value.status) && isTodoPriority(value.priority);
174
+ }
175
+ function isTodoList(value) {
176
+ return Array.isArray(value) && value.every(isTodo);
177
+ }
178
+ function isTodoTrackerInput(value) {
179
+ return isRecord(value) && hasOnlyKeys(value, ["todos"]) && isTodoList(value.todos);
180
+ }
181
+ function isTodoResultMetadata(value) {
182
+ return isTodoTrackerInput(value);
183
+ }
184
+ function todosFromToolResult(metadata, content) {
185
+ if (isTodoResultMetadata(metadata)) {
186
+ return metadata.todos;
187
+ }
188
+ for (const item of content) {
189
+ if (item.type !== "text" || item.text === undefined) {
190
+ continue;
191
+ }
192
+ try {
193
+ const todos = JSON.parse(item.text);
194
+ if (isTodoList(todos)) {
195
+ return todos;
196
+ }
197
+ } catch {}
198
+ }
199
+ return [];
200
+ }
201
+ function todosFromMessages(messages) {
202
+ for (const message of messages.toReversed()) {
203
+ if (message.type !== "assistant")
204
+ continue;
205
+ for (const part of message.content.toReversed()) {
206
+ if (part.type === "tool" && part.name === todoTrackerName && part.state.status === "completed") {
207
+ return todosFromToolResult(part.state.metadata, part.state.content);
208
+ }
209
+ }
210
+ }
211
+ return [];
212
+ }
213
+
214
+ class TodoTracker {
215
+ static marker(status) {
216
+ switch (status) {
217
+ case "pending":
218
+ return "[ ]";
219
+ case "in_progress":
220
+ return "[~]";
221
+ case "completed":
222
+ return "[x]";
223
+ case "cancelled":
224
+ return "[-]";
225
+ }
226
+ }
227
+ static sidebar(messages) {
228
+ return todosFromMessages(messages);
229
+ }
230
+ static register(context) {
231
+ return context.tool.transform((tools) => {
232
+ tools.add({
233
+ name: todoTrackerName,
234
+ description: todoTrackerDescription,
235
+ input: todoTrackerInput,
236
+ options: { codemode: false },
237
+ async execute(input) {
238
+ if (!isTodoTrackerInput(input)) {
239
+ throw new Error("Invalid todotracker input.");
240
+ }
241
+ return { content: JSON.stringify(input.todos), metadata: { todos: input.todos } };
242
+ }
243
+ });
244
+ });
245
+ }
246
+ }
247
+
131
248
  // src/server/index.ts
132
249
  var server_default = Plugin.define({
133
250
  id: "@encoredigitalgroup/opencode",
@@ -135,7 +252,7 @@ var server_default = Plugin.define({
135
252
  const pluginConfig = ConfigLoader.loadConfig(ctx.options);
136
253
  const command = "feature-planning";
137
254
  const route = SkillCommands.getCommands(pluginConfig)[command];
138
- const registration = await ctx.command.transform((commands) => {
255
+ const commandRegistration = await ctx.command.transform((commands) => {
139
256
  commands.add({
140
257
  name: command,
141
258
  description: route.description,
@@ -154,7 +271,20 @@ var server_default = Plugin.define({
154
271
  }
155
272
  });
156
273
  });
157
- return registration.dispose;
274
+ let toolRegistration;
275
+ try {
276
+ toolRegistration = await TodoTracker.register(ctx);
277
+ } catch (error) {
278
+ await commandRegistration.dispose();
279
+ throw error;
280
+ }
281
+ return async () => {
282
+ try {
283
+ await toolRegistration.dispose();
284
+ } finally {
285
+ await commandRegistration.dispose();
286
+ }
287
+ };
158
288
  }
159
289
  });
160
290
  export {
package/dist/tui/index.js CHANGED
@@ -2,6 +2,749 @@
2
2
  // src/tui/index.tsx
3
3
  import { Plugin } from "@opencode/plugin/tui";
4
4
 
5
+ // node_modules/solid-js/dist/solid.js
6
+ var sharedConfig = {
7
+ context: undefined,
8
+ registry: undefined,
9
+ effects: undefined,
10
+ done: false,
11
+ getContextId() {
12
+ return getContextId(this.context.count);
13
+ },
14
+ getNextContextId() {
15
+ return getContextId(this.context.count++);
16
+ }
17
+ };
18
+ function getContextId(count) {
19
+ const num = String(count), len = num.length - 1;
20
+ return sharedConfig.context.id + (len ? String.fromCharCode(96 + len) : "") + num;
21
+ }
22
+ function setHydrateContext(context) {
23
+ sharedConfig.context = context;
24
+ }
25
+ var IS_DEV = false;
26
+ var equalFn = (a, b) => a === b;
27
+ var $PROXY = Symbol("solid-proxy");
28
+ var $TRACK = Symbol("solid-track");
29
+ var $DEVCOMP = Symbol("solid-dev-component");
30
+ var signalOptions = {
31
+ equals: equalFn
32
+ };
33
+ var ERROR = null;
34
+ var runEffects = runQueue;
35
+ var STALE = 1;
36
+ var PENDING = 2;
37
+ var UNOWNED = {
38
+ owned: null,
39
+ cleanups: null,
40
+ context: null,
41
+ owner: null
42
+ };
43
+ var Owner = null;
44
+ var Transition = null;
45
+ var Scheduler = null;
46
+ var ExternalSourceConfig = null;
47
+ var Listener = null;
48
+ var Updates = null;
49
+ var Effects = null;
50
+ var ExecCount = 0;
51
+ function createRoot(fn, detachedOwner) {
52
+ const listener = Listener, owner = Owner, unowned = fn.length === 0, current = detachedOwner === undefined ? owner : detachedOwner, root = unowned ? UNOWNED : {
53
+ owned: null,
54
+ cleanups: null,
55
+ context: current ? current.context : null,
56
+ owner: current
57
+ }, updateFn = unowned ? fn : () => fn(() => untrack(() => cleanNode(root)));
58
+ Owner = root;
59
+ Listener = null;
60
+ try {
61
+ return runUpdates(updateFn, true);
62
+ } finally {
63
+ Listener = listener;
64
+ Owner = owner;
65
+ }
66
+ }
67
+ function createSignal(value, options) {
68
+ options = options ? Object.assign({}, signalOptions, options) : signalOptions;
69
+ const s = {
70
+ value,
71
+ observers: null,
72
+ observerSlots: null,
73
+ comparator: options.equals || undefined
74
+ };
75
+ const setter = (value) => {
76
+ if (typeof value === "function") {
77
+ if (Transition && Transition.running && Transition.sources.has(s))
78
+ value = value(s.tValue);
79
+ else
80
+ value = value(s.value);
81
+ }
82
+ return writeSignal(s, value);
83
+ };
84
+ return [readSignal.bind(s), setter];
85
+ }
86
+ function createEffect(fn, value, options) {
87
+ runEffects = runUserEffects;
88
+ const c = createComputation(fn, value, false, STALE), s = SuspenseContext && useContext(SuspenseContext);
89
+ if (s)
90
+ c.suspense = s;
91
+ if (!options || !options.render)
92
+ c.user = true;
93
+ Effects ? Effects.push(c) : updateComputation(c);
94
+ }
95
+ function createMemo(fn, value, options) {
96
+ options = options ? Object.assign({}, signalOptions, options) : signalOptions;
97
+ const c = createComputation(fn, value, true, 0);
98
+ c.observers = null;
99
+ c.observerSlots = null;
100
+ c.comparator = options.equals || undefined;
101
+ if (Scheduler && Transition && Transition.running) {
102
+ c.tState = STALE;
103
+ Updates.push(c);
104
+ } else
105
+ updateComputation(c);
106
+ return readSignal.bind(c);
107
+ }
108
+ function untrack(fn) {
109
+ if (!ExternalSourceConfig && Listener === null)
110
+ return fn();
111
+ const listener = Listener;
112
+ Listener = null;
113
+ try {
114
+ if (ExternalSourceConfig)
115
+ return ExternalSourceConfig.untrack(fn);
116
+ return fn();
117
+ } finally {
118
+ Listener = listener;
119
+ }
120
+ }
121
+ function onMount(fn) {
122
+ createEffect(() => untrack(fn));
123
+ }
124
+ function onCleanup(fn) {
125
+ if (Owner === null)
126
+ ;
127
+ else if (Owner.cleanups === null)
128
+ Owner.cleanups = [fn];
129
+ else
130
+ Owner.cleanups.push(fn);
131
+ return fn;
132
+ }
133
+ function startTransition(fn) {
134
+ if (Transition && Transition.running) {
135
+ fn();
136
+ return Transition.done;
137
+ }
138
+ const l = Listener;
139
+ const o = Owner;
140
+ return Promise.resolve().then(() => {
141
+ Listener = l;
142
+ Owner = o;
143
+ let t;
144
+ if (Scheduler || SuspenseContext) {
145
+ t = Transition || (Transition = {
146
+ sources: new Set,
147
+ effects: [],
148
+ promises: new Set,
149
+ disposed: new Set,
150
+ queue: new Set,
151
+ running: true
152
+ });
153
+ t.done || (t.done = new Promise((res) => t.resolve = res));
154
+ t.running = true;
155
+ }
156
+ runUpdates(fn, false);
157
+ Listener = Owner = null;
158
+ return t ? t.done : undefined;
159
+ });
160
+ }
161
+ var [transPending, setTransPending] = /* @__PURE__ */ createSignal(false);
162
+ function useContext(context) {
163
+ let value;
164
+ return Owner && Owner.context && (value = Owner.context[context.id]) !== undefined ? value : context.defaultValue;
165
+ }
166
+ var SuspenseContext;
167
+ function readSignal() {
168
+ const runningTransition = Transition && Transition.running;
169
+ if (this.sources && (runningTransition ? this.tState : this.state)) {
170
+ if ((runningTransition ? this.tState : this.state) === STALE)
171
+ updateComputation(this);
172
+ else {
173
+ const updates = Updates;
174
+ Updates = null;
175
+ runUpdates(() => lookUpstream(this), false);
176
+ Updates = updates;
177
+ }
178
+ }
179
+ if (Listener) {
180
+ const sSlot = this.observers ? this.observers.length : 0;
181
+ if (!Listener.sources) {
182
+ Listener.sources = [this];
183
+ Listener.sourceSlots = [sSlot];
184
+ } else {
185
+ Listener.sources.push(this);
186
+ Listener.sourceSlots.push(sSlot);
187
+ }
188
+ if (!this.observers) {
189
+ this.observers = [Listener];
190
+ this.observerSlots = [Listener.sources.length - 1];
191
+ } else {
192
+ this.observers.push(Listener);
193
+ this.observerSlots.push(Listener.sources.length - 1);
194
+ }
195
+ }
196
+ if (runningTransition && Transition.sources.has(this))
197
+ return this.tValue;
198
+ return this.value;
199
+ }
200
+ function writeSignal(node, value, isComp) {
201
+ let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
202
+ if (!node.comparator || !node.comparator(current, value)) {
203
+ if (Transition) {
204
+ const TransitionRunning = Transition.running;
205
+ if (TransitionRunning || !isComp && Transition.sources.has(node)) {
206
+ Transition.sources.add(node);
207
+ node.tValue = value;
208
+ }
209
+ if (!TransitionRunning)
210
+ node.value = value;
211
+ } else
212
+ node.value = value;
213
+ if (node.observers && node.observers.length) {
214
+ runUpdates(() => {
215
+ for (let i = 0;i < node.observers.length; i += 1) {
216
+ const o = node.observers[i];
217
+ const TransitionRunning = Transition && Transition.running;
218
+ if (TransitionRunning && Transition.disposed.has(o))
219
+ continue;
220
+ if (TransitionRunning ? !o.tState : !o.state) {
221
+ if (o.pure)
222
+ Updates.push(o);
223
+ else
224
+ Effects.push(o);
225
+ if (o.observers)
226
+ markDownstream(o);
227
+ }
228
+ if (!TransitionRunning)
229
+ o.state = STALE;
230
+ else
231
+ o.tState = STALE;
232
+ }
233
+ if (Updates.length > 1e6) {
234
+ Updates = [];
235
+ if (IS_DEV)
236
+ ;
237
+ throw new Error;
238
+ }
239
+ }, false);
240
+ }
241
+ }
242
+ return value;
243
+ }
244
+ function updateComputation(node) {
245
+ if (!node.fn)
246
+ return;
247
+ cleanNode(node);
248
+ const time = ExecCount;
249
+ runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time);
250
+ if (Transition && !Transition.running && Transition.sources.has(node)) {
251
+ queueMicrotask(() => {
252
+ runUpdates(() => {
253
+ Transition && (Transition.running = true);
254
+ Listener = Owner = node;
255
+ runComputation(node, node.tValue, time);
256
+ Listener = Owner = null;
257
+ }, false);
258
+ });
259
+ }
260
+ }
261
+ function runComputation(node, value, time) {
262
+ let nextValue;
263
+ const owner = Owner, listener = Listener;
264
+ Listener = Owner = node;
265
+ try {
266
+ nextValue = node.fn(value);
267
+ } catch (err) {
268
+ if (node.pure) {
269
+ if (Transition && Transition.running) {
270
+ node.tState = STALE;
271
+ node.tOwned && node.tOwned.forEach(cleanNode);
272
+ node.tOwned = undefined;
273
+ } else {
274
+ node.state = STALE;
275
+ node.owned && node.owned.forEach(cleanNode);
276
+ node.owned = null;
277
+ }
278
+ }
279
+ node.updatedAt = time + 1;
280
+ return handleError(err);
281
+ } finally {
282
+ Listener = listener;
283
+ Owner = owner;
284
+ }
285
+ if (!node.updatedAt || node.updatedAt <= time) {
286
+ if (node.updatedAt != null && "observers" in node) {
287
+ writeSignal(node, nextValue, true);
288
+ } else if (Transition && Transition.running && node.pure) {
289
+ if (!Transition.sources.has(node))
290
+ node.value = nextValue;
291
+ Transition.sources.add(node);
292
+ node.tValue = nextValue;
293
+ } else
294
+ node.value = nextValue;
295
+ node.updatedAt = time;
296
+ }
297
+ }
298
+ function createComputation(fn, init, pure, state = STALE, options) {
299
+ const c = {
300
+ fn,
301
+ state,
302
+ updatedAt: null,
303
+ owned: null,
304
+ sources: null,
305
+ sourceSlots: null,
306
+ cleanups: null,
307
+ value: init,
308
+ owner: Owner,
309
+ context: Owner ? Owner.context : null,
310
+ pure
311
+ };
312
+ if (Transition && Transition.running) {
313
+ c.state = 0;
314
+ c.tState = state;
315
+ }
316
+ if (Owner === null)
317
+ ;
318
+ else if (Owner !== UNOWNED) {
319
+ if (Transition && Transition.running && Owner.pure) {
320
+ if (!Owner.tOwned)
321
+ Owner.tOwned = [c];
322
+ else
323
+ Owner.tOwned.push(c);
324
+ } else {
325
+ if (!Owner.owned)
326
+ Owner.owned = [c];
327
+ else
328
+ Owner.owned.push(c);
329
+ }
330
+ }
331
+ if (ExternalSourceConfig && c.fn) {
332
+ const sourceFn = c.fn;
333
+ const [track, trigger] = createSignal(undefined, {
334
+ equals: false
335
+ });
336
+ const ordinary = ExternalSourceConfig.factory(sourceFn, trigger);
337
+ onCleanup(() => ordinary.dispose());
338
+ let inTransition;
339
+ const triggerInTransition = () => startTransition(trigger).then(() => {
340
+ if (inTransition) {
341
+ inTransition.dispose();
342
+ inTransition = undefined;
343
+ }
344
+ });
345
+ c.fn = (x) => {
346
+ track();
347
+ if (Transition && Transition.running) {
348
+ if (!inTransition)
349
+ inTransition = ExternalSourceConfig.factory(sourceFn, triggerInTransition);
350
+ return inTransition.track(x);
351
+ }
352
+ return ordinary.track(x);
353
+ };
354
+ }
355
+ return c;
356
+ }
357
+ function runTop(node) {
358
+ const runningTransition = Transition && Transition.running;
359
+ if ((runningTransition ? node.tState : node.state) === 0)
360
+ return;
361
+ if ((runningTransition ? node.tState : node.state) === PENDING)
362
+ return lookUpstream(node);
363
+ if (node.suspense && untrack(node.suspense.inFallback))
364
+ return node.suspense.effects.push(node);
365
+ const ancestors = [node];
366
+ while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) {
367
+ if (runningTransition && Transition.disposed.has(node))
368
+ return;
369
+ if (runningTransition ? node.tState : node.state)
370
+ ancestors.push(node);
371
+ }
372
+ for (let i = ancestors.length - 1;i >= 0; i--) {
373
+ node = ancestors[i];
374
+ if (runningTransition) {
375
+ let top = node, prev = ancestors[i + 1];
376
+ while ((top = top.owner) && top !== prev) {
377
+ if (Transition.disposed.has(top))
378
+ return;
379
+ }
380
+ }
381
+ if ((runningTransition ? node.tState : node.state) === STALE) {
382
+ updateComputation(node);
383
+ } else if ((runningTransition ? node.tState : node.state) === PENDING) {
384
+ const updates = Updates;
385
+ Updates = null;
386
+ runUpdates(() => lookUpstream(node, ancestors[0]), false);
387
+ Updates = updates;
388
+ }
389
+ }
390
+ }
391
+ function runUpdates(fn, init) {
392
+ if (Updates)
393
+ return fn();
394
+ let wait = false;
395
+ if (!init)
396
+ Updates = [];
397
+ if (Effects)
398
+ wait = true;
399
+ else
400
+ Effects = [];
401
+ ExecCount++;
402
+ try {
403
+ const res = fn();
404
+ completeUpdates(wait);
405
+ return res;
406
+ } catch (err) {
407
+ if (!wait)
408
+ Effects = null;
409
+ Updates = null;
410
+ handleError(err);
411
+ }
412
+ }
413
+ function completeUpdates(wait) {
414
+ if (Updates) {
415
+ if (Scheduler && Transition && Transition.running)
416
+ scheduleQueue(Updates);
417
+ else
418
+ runQueue(Updates);
419
+ Updates = null;
420
+ }
421
+ if (wait)
422
+ return;
423
+ let res;
424
+ if (Transition) {
425
+ if (!Transition.promises.size && !Transition.queue.size) {
426
+ const sources = Transition.sources;
427
+ const disposed = Transition.disposed;
428
+ Effects.push.apply(Effects, Transition.effects);
429
+ res = Transition.resolve;
430
+ for (const e of Effects) {
431
+ "tState" in e && (e.state = e.tState);
432
+ delete e.tState;
433
+ }
434
+ Transition = null;
435
+ runUpdates(() => {
436
+ for (const d of disposed)
437
+ cleanNode(d);
438
+ for (const v of sources) {
439
+ v.value = v.tValue;
440
+ if (v.owned) {
441
+ for (let i = 0, len = v.owned.length;i < len; i++)
442
+ cleanNode(v.owned[i]);
443
+ }
444
+ if (v.tOwned)
445
+ v.owned = v.tOwned;
446
+ delete v.tValue;
447
+ delete v.tOwned;
448
+ v.tState = 0;
449
+ }
450
+ setTransPending(false);
451
+ }, false);
452
+ } else if (Transition.running) {
453
+ Transition.running = false;
454
+ Transition.effects.push.apply(Transition.effects, Effects);
455
+ Effects = null;
456
+ setTransPending(true);
457
+ return;
458
+ }
459
+ }
460
+ const e = Effects;
461
+ Effects = null;
462
+ if (e.length)
463
+ runUpdates(() => runEffects(e), false);
464
+ if (res)
465
+ res();
466
+ }
467
+ function runQueue(queue) {
468
+ for (let i = 0;i < queue.length; i++)
469
+ runTop(queue[i]);
470
+ }
471
+ function scheduleQueue(queue) {
472
+ for (let i = 0;i < queue.length; i++) {
473
+ const item = queue[i];
474
+ const tasks = Transition.queue;
475
+ if (!tasks.has(item)) {
476
+ tasks.add(item);
477
+ Scheduler(() => {
478
+ tasks.delete(item);
479
+ runUpdates(() => {
480
+ Transition.running = true;
481
+ runTop(item);
482
+ }, false);
483
+ Transition && (Transition.running = false);
484
+ });
485
+ }
486
+ }
487
+ }
488
+ function runUserEffects(queue) {
489
+ let i, userLength = 0;
490
+ for (i = 0;i < queue.length; i++) {
491
+ const e = queue[i];
492
+ if (!e.user)
493
+ runTop(e);
494
+ else
495
+ queue[userLength++] = e;
496
+ }
497
+ if (sharedConfig.context) {
498
+ if (sharedConfig.count) {
499
+ sharedConfig.effects || (sharedConfig.effects = []);
500
+ sharedConfig.effects.push(...queue.slice(0, userLength));
501
+ return;
502
+ }
503
+ setHydrateContext();
504
+ }
505
+ if (sharedConfig.effects && (sharedConfig.done || !sharedConfig.count)) {
506
+ queue = [...sharedConfig.effects, ...queue];
507
+ userLength += sharedConfig.effects.length;
508
+ delete sharedConfig.effects;
509
+ }
510
+ for (i = 0;i < userLength; i++)
511
+ runTop(queue[i]);
512
+ }
513
+ function lookUpstream(node, ignore) {
514
+ const runningTransition = Transition && Transition.running;
515
+ if (runningTransition)
516
+ node.tState = 0;
517
+ else
518
+ node.state = 0;
519
+ for (let i = 0;i < node.sources.length; i += 1) {
520
+ const source = node.sources[i];
521
+ if (source.sources) {
522
+ const state = runningTransition ? source.tState : source.state;
523
+ if (state === STALE) {
524
+ if (source !== ignore && (!source.updatedAt || source.updatedAt < ExecCount))
525
+ runTop(source);
526
+ } else if (state === PENDING)
527
+ lookUpstream(source, ignore);
528
+ }
529
+ }
530
+ }
531
+ function markDownstream(node) {
532
+ const runningTransition = Transition && Transition.running;
533
+ for (let i = 0;i < node.observers.length; i += 1) {
534
+ const o = node.observers[i];
535
+ if (runningTransition ? !o.tState : !o.state) {
536
+ if (runningTransition)
537
+ o.tState = PENDING;
538
+ else
539
+ o.state = PENDING;
540
+ if (o.pure)
541
+ Updates.push(o);
542
+ else
543
+ Effects.push(o);
544
+ o.observers && markDownstream(o);
545
+ }
546
+ }
547
+ }
548
+ function cleanNode(node) {
549
+ let i;
550
+ if (node.sources) {
551
+ while (node.sources.length) {
552
+ const source = node.sources.pop(), index = node.sourceSlots.pop(), obs = source.observers;
553
+ if (obs && obs.length) {
554
+ const n = obs.pop(), s = source.observerSlots.pop();
555
+ if (index < obs.length) {
556
+ n.sourceSlots[s] = index;
557
+ obs[index] = n;
558
+ source.observerSlots[index] = s;
559
+ }
560
+ }
561
+ }
562
+ }
563
+ if (node.tOwned) {
564
+ for (i = node.tOwned.length - 1;i >= 0; i--)
565
+ cleanNode(node.tOwned[i]);
566
+ delete node.tOwned;
567
+ }
568
+ if (Transition && Transition.running && node.pure) {
569
+ reset(node, true);
570
+ } else if (node.owned) {
571
+ for (i = node.owned.length - 1;i >= 0; i--)
572
+ cleanNode(node.owned[i]);
573
+ node.owned = null;
574
+ }
575
+ if (node.cleanups) {
576
+ for (i = node.cleanups.length - 1;i >= 0; i--)
577
+ node.cleanups[i]();
578
+ node.cleanups = null;
579
+ }
580
+ if (Transition && Transition.running)
581
+ node.tState = 0;
582
+ else
583
+ node.state = 0;
584
+ }
585
+ function reset(node, top) {
586
+ if (!top) {
587
+ node.tState = 0;
588
+ Transition.disposed.add(node);
589
+ }
590
+ if (node.owned) {
591
+ for (let i = 0;i < node.owned.length; i++)
592
+ reset(node.owned[i]);
593
+ }
594
+ }
595
+ function castError(err) {
596
+ if (err instanceof Error)
597
+ return err;
598
+ return new Error(typeof err === "string" ? err : "Unknown error", {
599
+ cause: err
600
+ });
601
+ }
602
+ function runErrors(err, fns, owner) {
603
+ try {
604
+ for (const f of fns)
605
+ f(err);
606
+ } catch (e) {
607
+ handleError(e, owner && owner.owner || null);
608
+ }
609
+ }
610
+ function handleError(err, owner = Owner) {
611
+ const fns = ERROR && owner && owner.context && owner.context[ERROR];
612
+ const error = castError(err);
613
+ if (!fns)
614
+ throw error;
615
+ if (Effects)
616
+ Effects.push({
617
+ fn() {
618
+ runErrors(error, fns, owner);
619
+ },
620
+ state: STALE
621
+ });
622
+ else
623
+ runErrors(error, fns, owner);
624
+ }
625
+ var FALLBACK = Symbol("fallback");
626
+ function dispose(d) {
627
+ for (let i = 0;i < d.length; i++)
628
+ d[i]();
629
+ }
630
+ function mapArray(list, mapFn, options = {}) {
631
+ let items = [], mapped = [], disposers = [], len = 0, indexes = mapFn.length > 1 ? [] : null;
632
+ onCleanup(() => dispose(disposers));
633
+ return () => {
634
+ let newItems = list() || [], newLen = newItems.length, i, j;
635
+ newItems[$TRACK];
636
+ return untrack(() => {
637
+ let newIndices, newIndicesNext, temp, tempdisposers, tempIndexes, start, end, newEnd, item;
638
+ if (newLen === 0) {
639
+ if (len !== 0) {
640
+ dispose(disposers);
641
+ disposers = [];
642
+ items = [];
643
+ mapped = [];
644
+ len = 0;
645
+ indexes && (indexes = []);
646
+ }
647
+ if (options.fallback) {
648
+ items = [FALLBACK];
649
+ mapped[0] = createRoot((disposer) => {
650
+ disposers[0] = disposer;
651
+ return options.fallback();
652
+ });
653
+ len = 1;
654
+ }
655
+ } else if (len === 0) {
656
+ mapped = new Array(newLen);
657
+ for (j = 0;j < newLen; j++) {
658
+ items[j] = newItems[j];
659
+ mapped[j] = createRoot(mapper);
660
+ }
661
+ len = newLen;
662
+ } else {
663
+ temp = new Array(newLen);
664
+ tempdisposers = new Array(newLen);
665
+ indexes && (tempIndexes = new Array(newLen));
666
+ for (start = 0, end = Math.min(len, newLen);start < end && items[start] === newItems[start]; start++)
667
+ ;
668
+ for (end = len - 1, newEnd = newLen - 1;end >= start && newEnd >= start && items[end] === newItems[newEnd]; end--, newEnd--) {
669
+ temp[newEnd] = mapped[end];
670
+ tempdisposers[newEnd] = disposers[end];
671
+ indexes && (tempIndexes[newEnd] = indexes[end]);
672
+ }
673
+ newIndices = new Map;
674
+ newIndicesNext = new Array(newEnd + 1);
675
+ for (j = newEnd;j >= start; j--) {
676
+ item = newItems[j];
677
+ i = newIndices.get(item);
678
+ newIndicesNext[j] = i === undefined ? -1 : i;
679
+ newIndices.set(item, j);
680
+ }
681
+ for (i = start;i <= end; i++) {
682
+ item = items[i];
683
+ j = newIndices.get(item);
684
+ if (j !== undefined && j !== -1) {
685
+ temp[j] = mapped[i];
686
+ tempdisposers[j] = disposers[i];
687
+ indexes && (tempIndexes[j] = indexes[i]);
688
+ j = newIndicesNext[j];
689
+ newIndices.set(item, j);
690
+ } else
691
+ disposers[i]();
692
+ }
693
+ for (j = start;j < newLen; j++) {
694
+ if (j in temp) {
695
+ mapped[j] = temp[j];
696
+ disposers[j] = tempdisposers[j];
697
+ if (indexes) {
698
+ indexes[j] = tempIndexes[j];
699
+ indexes[j](j);
700
+ }
701
+ } else
702
+ mapped[j] = createRoot(mapper);
703
+ }
704
+ mapped = mapped.slice(0, len = newLen);
705
+ items = newItems.slice(0);
706
+ }
707
+ return mapped;
708
+ });
709
+ function mapper(disposer) {
710
+ disposers[j] = disposer;
711
+ if (indexes) {
712
+ const [s, set] = createSignal(j);
713
+ indexes[j] = set;
714
+ return mapFn(newItems[j], s);
715
+ }
716
+ return mapFn(newItems[j]);
717
+ }
718
+ };
719
+ }
720
+ var narrowedError = (name) => `Stale read from <${name}>.`;
721
+ function For(props) {
722
+ const fallback = "fallback" in props && {
723
+ fallback: () => props.fallback
724
+ };
725
+ return createMemo(mapArray(() => props.each, props.children, fallback || undefined));
726
+ }
727
+ function Show(props) {
728
+ const keyed = props.keyed;
729
+ const conditionValue = createMemo(() => props.when, undefined, undefined);
730
+ const condition = keyed ? conditionValue : createMemo(conditionValue, undefined, {
731
+ equals: (a, b) => !a === !b
732
+ });
733
+ return createMemo(() => {
734
+ const c = condition();
735
+ if (c) {
736
+ const child = props.children;
737
+ const fn = typeof child === "function" && child.length > 0;
738
+ return fn ? untrack(() => child(keyed ? c : () => {
739
+ if (!untrack(condition))
740
+ throw narrowedError("Show");
741
+ return conditionValue();
742
+ })) : child;
743
+ }
744
+ return props.fallback;
745
+ }, undefined, undefined);
746
+ }
747
+
5
748
  // src/config/ConfigDefaults.ts
6
749
  class ConfigDefaults {
7
750
  static getDefaultSkillMappings() {
@@ -319,22 +1062,184 @@ class FilePickerCommands {
319
1062
  }
320
1063
  }
321
1064
 
1065
+ // src/lib/TodoTracker.ts
1066
+ var todoTrackerInput = {
1067
+ type: "object",
1068
+ additionalProperties: false,
1069
+ required: ["todos"],
1070
+ properties: {
1071
+ todos: {
1072
+ type: "array",
1073
+ items: {
1074
+ type: "object",
1075
+ additionalProperties: false,
1076
+ required: ["content", "status", "priority"],
1077
+ properties: {
1078
+ content: { type: "string", minLength: 1, pattern: "\\S" },
1079
+ status: { enum: ["pending", "in_progress", "completed", "cancelled"] },
1080
+ priority: { enum: ["high", "medium", "low"] }
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ };
1086
+ var todoTrackerName = "todotracker";
1087
+ var todoTrackerDescription = "Replace the current session's complete ordered todo list. " + "Pass every remaining todo on each call; pass an empty array to clear the list.";
1088
+ var todoStatuses = ["pending", "in_progress", "completed", "cancelled"];
1089
+ var todoPriorities = ["high", "medium", "low"];
1090
+ function isRecord(value) {
1091
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1092
+ }
1093
+ function hasOnlyKeys(value, keys) {
1094
+ const expected = new Set(keys);
1095
+ return Object.keys(value).length === keys.length && Object.keys(value).every((key) => expected.has(key));
1096
+ }
1097
+ function isTodoStatus(value) {
1098
+ return typeof value === "string" && todoStatuses.includes(value);
1099
+ }
1100
+ function isTodoPriority(value) {
1101
+ return typeof value === "string" && todoPriorities.includes(value);
1102
+ }
1103
+ function isTodo(value) {
1104
+ if (!isRecord(value) || !hasOnlyKeys(value, ["content", "status", "priority"])) {
1105
+ return false;
1106
+ }
1107
+ return typeof value.content === "string" && value.content.length >= 1 && /\S/.test(value.content) && isTodoStatus(value.status) && isTodoPriority(value.priority);
1108
+ }
1109
+ function isTodoList(value) {
1110
+ return Array.isArray(value) && value.every(isTodo);
1111
+ }
1112
+ function isTodoTrackerInput(value) {
1113
+ return isRecord(value) && hasOnlyKeys(value, ["todos"]) && isTodoList(value.todos);
1114
+ }
1115
+ function isTodoResultMetadata(value) {
1116
+ return isTodoTrackerInput(value);
1117
+ }
1118
+ function todosFromToolResult(metadata, content) {
1119
+ if (isTodoResultMetadata(metadata)) {
1120
+ return metadata.todos;
1121
+ }
1122
+ for (const item of content) {
1123
+ if (item.type !== "text" || item.text === undefined) {
1124
+ continue;
1125
+ }
1126
+ try {
1127
+ const todos = JSON.parse(item.text);
1128
+ if (isTodoList(todos)) {
1129
+ return todos;
1130
+ }
1131
+ } catch {}
1132
+ }
1133
+ return [];
1134
+ }
1135
+ function todosFromMessages(messages) {
1136
+ for (const message of messages.toReversed()) {
1137
+ if (message.type !== "assistant")
1138
+ continue;
1139
+ for (const part of message.content.toReversed()) {
1140
+ if (part.type === "tool" && part.name === todoTrackerName && part.state.status === "completed") {
1141
+ return todosFromToolResult(part.state.metadata, part.state.content);
1142
+ }
1143
+ }
1144
+ }
1145
+ return [];
1146
+ }
1147
+
1148
+ class TodoTracker {
1149
+ static marker(status) {
1150
+ switch (status) {
1151
+ case "pending":
1152
+ return "[ ]";
1153
+ case "in_progress":
1154
+ return "[~]";
1155
+ case "completed":
1156
+ return "[x]";
1157
+ case "cancelled":
1158
+ return "[-]";
1159
+ }
1160
+ }
1161
+ static sidebar(messages) {
1162
+ return todosFromMessages(messages);
1163
+ }
1164
+ static register(context) {
1165
+ return context.tool.transform((tools) => {
1166
+ tools.add({
1167
+ name: todoTrackerName,
1168
+ description: todoTrackerDescription,
1169
+ input: todoTrackerInput,
1170
+ options: { codemode: false },
1171
+ async execute(input) {
1172
+ if (!isTodoTrackerInput(input)) {
1173
+ throw new Error("Invalid todotracker input.");
1174
+ }
1175
+ return { content: JSON.stringify(input.todos), metadata: { todos: input.todos } };
1176
+ }
1177
+ });
1178
+ });
1179
+ }
1180
+ }
1181
+
322
1182
  // src/tui/index.tsx
323
- import { jsx, Fragment } from "@opentui/solid/jsx-runtime";
1183
+ import { jsx, jsxs, Fragment } from "@opentui/solid/jsx-runtime";
324
1184
  function PickerCommandLayer(props) {
325
1185
  FilePickerCommands.register(props.context, props.config);
326
1186
  return /* @__PURE__ */ jsx(Fragment, {});
327
1187
  }
1188
+ function TodoSidebar(props) {
1189
+ const todos = createMemo(() => TodoTracker.sidebar(props.context.data.session.message.list(props.sessionID)));
1190
+ const color = (status) => status === "in_progress" ? props.context.theme.text.feedback.warning.base : props.context.theme.text.muted;
1191
+ onMount(() => {
1192
+ props.context.data.session.message.sync(props.sessionID).catch(() => {});
1193
+ });
1194
+ return /* @__PURE__ */ jsx(Show, {
1195
+ when: todos().length > 0,
1196
+ children: /* @__PURE__ */ jsxs("box", {
1197
+ flexDirection: "column",
1198
+ children: [
1199
+ /* @__PURE__ */ jsx("text", {
1200
+ children: "Todos"
1201
+ }),
1202
+ /* @__PURE__ */ jsx(For, {
1203
+ each: todos(),
1204
+ children: (todo) => /* @__PURE__ */ jsx("text", {
1205
+ fg: color(todo.status),
1206
+ children: `${TodoTracker.marker(todo.status)} (${todo.priority}) ${todo.content}`
1207
+ })
1208
+ })
1209
+ ]
1210
+ })
1211
+ });
1212
+ }
328
1213
  var tui_default = Plugin.define({
329
1214
  id: "@encoredigitalgroup/opencode",
330
1215
  setup(ctx) {
331
- return ctx.ui.slot({
1216
+ const appSlot = ctx.ui.slot({
332
1217
  append: "app",
333
1218
  render: () => /* @__PURE__ */ jsx(PickerCommandLayer, {
334
1219
  context: ctx,
335
1220
  config: ConfigLoader.loadConfig(ctx.options)
336
1221
  })
337
1222
  });
1223
+ let sidebarSlot;
1224
+ try {
1225
+ sidebarSlot = ctx.ui.slot({
1226
+ append: "sidebar.content",
1227
+ render: (input) => /* @__PURE__ */ jsx(TodoSidebar, {
1228
+ context: ctx,
1229
+ sessionID: input.sessionID
1230
+ })
1231
+ });
1232
+ } catch (error) {
1233
+ appSlot();
1234
+ throw error;
1235
+ }
1236
+ return () => {
1237
+ try {
1238
+ sidebarSlot();
1239
+ } finally {
1240
+ appSlot();
1241
+ }
1242
+ };
338
1243
  }
339
1244
  });
340
1245
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@encoredigitalgroup/opencode",
3
3
  "type": "module",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "OpenCode file-picker commands for planning skills.",
6
6
  "publishConfig": {
7
7
  "access": "public"