@encoredigitalgroup/opencode 0.2.3 → 0.2.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.
package/dist/tui/index.js CHANGED
@@ -1,6 +1,600 @@
1
1
  // @bun
2
2
  // src/tui/index.tsx
3
3
  import { Plugin } from "@opencode/plugin/tui";
4
+ import { fg, StyledText } from "@opentui/core";
5
+
6
+ // node_modules/solid-js/dist/solid.js
7
+ var sharedConfig = {
8
+ context: undefined,
9
+ registry: undefined,
10
+ effects: undefined,
11
+ done: false,
12
+ getContextId() {
13
+ return getContextId(this.context.count);
14
+ },
15
+ getNextContextId() {
16
+ return getContextId(this.context.count++);
17
+ }
18
+ };
19
+ function getContextId(count) {
20
+ const num = String(count), len = num.length - 1;
21
+ return sharedConfig.context.id + (len ? String.fromCharCode(96 + len) : "") + num;
22
+ }
23
+ function setHydrateContext(context) {
24
+ sharedConfig.context = context;
25
+ }
26
+ var IS_DEV = false;
27
+ var equalFn = (a, b) => a === b;
28
+ var $PROXY = Symbol("solid-proxy");
29
+ var $TRACK = Symbol("solid-track");
30
+ var $DEVCOMP = Symbol("solid-dev-component");
31
+ var signalOptions = {
32
+ equals: equalFn
33
+ };
34
+ var ERROR = null;
35
+ var runEffects = runQueue;
36
+ var STALE = 1;
37
+ var PENDING = 2;
38
+ var UNOWNED = {
39
+ owned: null,
40
+ cleanups: null,
41
+ context: null,
42
+ owner: null
43
+ };
44
+ var Owner = null;
45
+ var Transition = null;
46
+ var Scheduler = null;
47
+ var ExternalSourceConfig = null;
48
+ var Listener = null;
49
+ var Updates = null;
50
+ var Effects = null;
51
+ var ExecCount = 0;
52
+ function createSignal(value, options) {
53
+ options = options ? Object.assign({}, signalOptions, options) : signalOptions;
54
+ const s = {
55
+ value,
56
+ observers: null,
57
+ observerSlots: null,
58
+ comparator: options.equals || undefined
59
+ };
60
+ const setter = (value) => {
61
+ if (typeof value === "function") {
62
+ if (Transition && Transition.running && Transition.sources.has(s))
63
+ value = value(s.tValue);
64
+ else
65
+ value = value(s.value);
66
+ }
67
+ return writeSignal(s, value);
68
+ };
69
+ return [readSignal.bind(s), setter];
70
+ }
71
+ function createEffect(fn, value, options) {
72
+ runEffects = runUserEffects;
73
+ const c = createComputation(fn, value, false, STALE), s = SuspenseContext && useContext(SuspenseContext);
74
+ if (s)
75
+ c.suspense = s;
76
+ if (!options || !options.render)
77
+ c.user = true;
78
+ Effects ? Effects.push(c) : updateComputation(c);
79
+ }
80
+ function untrack(fn) {
81
+ if (!ExternalSourceConfig && Listener === null)
82
+ return fn();
83
+ const listener = Listener;
84
+ Listener = null;
85
+ try {
86
+ if (ExternalSourceConfig)
87
+ return ExternalSourceConfig.untrack(fn);
88
+ return fn();
89
+ } finally {
90
+ Listener = listener;
91
+ }
92
+ }
93
+ function onMount(fn) {
94
+ createEffect(() => untrack(fn));
95
+ }
96
+ function onCleanup(fn) {
97
+ if (Owner === null)
98
+ ;
99
+ else if (Owner.cleanups === null)
100
+ Owner.cleanups = [fn];
101
+ else
102
+ Owner.cleanups.push(fn);
103
+ return fn;
104
+ }
105
+ function startTransition(fn) {
106
+ if (Transition && Transition.running) {
107
+ fn();
108
+ return Transition.done;
109
+ }
110
+ const l = Listener;
111
+ const o = Owner;
112
+ return Promise.resolve().then(() => {
113
+ Listener = l;
114
+ Owner = o;
115
+ let t;
116
+ if (Scheduler || SuspenseContext) {
117
+ t = Transition || (Transition = {
118
+ sources: new Set,
119
+ effects: [],
120
+ promises: new Set,
121
+ disposed: new Set,
122
+ queue: new Set,
123
+ running: true
124
+ });
125
+ t.done || (t.done = new Promise((res) => t.resolve = res));
126
+ t.running = true;
127
+ }
128
+ runUpdates(fn, false);
129
+ Listener = Owner = null;
130
+ return t ? t.done : undefined;
131
+ });
132
+ }
133
+ var [transPending, setTransPending] = /* @__PURE__ */ createSignal(false);
134
+ function useContext(context) {
135
+ let value;
136
+ return Owner && Owner.context && (value = Owner.context[context.id]) !== undefined ? value : context.defaultValue;
137
+ }
138
+ var SuspenseContext;
139
+ function readSignal() {
140
+ const runningTransition = Transition && Transition.running;
141
+ if (this.sources && (runningTransition ? this.tState : this.state)) {
142
+ if ((runningTransition ? this.tState : this.state) === STALE)
143
+ updateComputation(this);
144
+ else {
145
+ const updates = Updates;
146
+ Updates = null;
147
+ runUpdates(() => lookUpstream(this), false);
148
+ Updates = updates;
149
+ }
150
+ }
151
+ if (Listener) {
152
+ const sSlot = this.observers ? this.observers.length : 0;
153
+ if (!Listener.sources) {
154
+ Listener.sources = [this];
155
+ Listener.sourceSlots = [sSlot];
156
+ } else {
157
+ Listener.sources.push(this);
158
+ Listener.sourceSlots.push(sSlot);
159
+ }
160
+ if (!this.observers) {
161
+ this.observers = [Listener];
162
+ this.observerSlots = [Listener.sources.length - 1];
163
+ } else {
164
+ this.observers.push(Listener);
165
+ this.observerSlots.push(Listener.sources.length - 1);
166
+ }
167
+ }
168
+ if (runningTransition && Transition.sources.has(this))
169
+ return this.tValue;
170
+ return this.value;
171
+ }
172
+ function writeSignal(node, value, isComp) {
173
+ let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
174
+ if (!node.comparator || !node.comparator(current, value)) {
175
+ if (Transition) {
176
+ const TransitionRunning = Transition.running;
177
+ if (TransitionRunning || !isComp && Transition.sources.has(node)) {
178
+ Transition.sources.add(node);
179
+ node.tValue = value;
180
+ }
181
+ if (!TransitionRunning)
182
+ node.value = value;
183
+ } else
184
+ node.value = value;
185
+ if (node.observers && node.observers.length) {
186
+ runUpdates(() => {
187
+ for (let i = 0;i < node.observers.length; i += 1) {
188
+ const o = node.observers[i];
189
+ const TransitionRunning = Transition && Transition.running;
190
+ if (TransitionRunning && Transition.disposed.has(o))
191
+ continue;
192
+ if (TransitionRunning ? !o.tState : !o.state) {
193
+ if (o.pure)
194
+ Updates.push(o);
195
+ else
196
+ Effects.push(o);
197
+ if (o.observers)
198
+ markDownstream(o);
199
+ }
200
+ if (!TransitionRunning)
201
+ o.state = STALE;
202
+ else
203
+ o.tState = STALE;
204
+ }
205
+ if (Updates.length > 1e6) {
206
+ Updates = [];
207
+ if (IS_DEV)
208
+ ;
209
+ throw new Error;
210
+ }
211
+ }, false);
212
+ }
213
+ }
214
+ return value;
215
+ }
216
+ function updateComputation(node) {
217
+ if (!node.fn)
218
+ return;
219
+ cleanNode(node);
220
+ const time = ExecCount;
221
+ runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time);
222
+ if (Transition && !Transition.running && Transition.sources.has(node)) {
223
+ queueMicrotask(() => {
224
+ runUpdates(() => {
225
+ Transition && (Transition.running = true);
226
+ Listener = Owner = node;
227
+ runComputation(node, node.tValue, time);
228
+ Listener = Owner = null;
229
+ }, false);
230
+ });
231
+ }
232
+ }
233
+ function runComputation(node, value, time) {
234
+ let nextValue;
235
+ const owner = Owner, listener = Listener;
236
+ Listener = Owner = node;
237
+ try {
238
+ nextValue = node.fn(value);
239
+ } catch (err) {
240
+ if (node.pure) {
241
+ if (Transition && Transition.running) {
242
+ node.tState = STALE;
243
+ node.tOwned && node.tOwned.forEach(cleanNode);
244
+ node.tOwned = undefined;
245
+ } else {
246
+ node.state = STALE;
247
+ node.owned && node.owned.forEach(cleanNode);
248
+ node.owned = null;
249
+ }
250
+ }
251
+ node.updatedAt = time + 1;
252
+ return handleError(err);
253
+ } finally {
254
+ Listener = listener;
255
+ Owner = owner;
256
+ }
257
+ if (!node.updatedAt || node.updatedAt <= time) {
258
+ if (node.updatedAt != null && "observers" in node) {
259
+ writeSignal(node, nextValue, true);
260
+ } else if (Transition && Transition.running && node.pure) {
261
+ if (!Transition.sources.has(node))
262
+ node.value = nextValue;
263
+ Transition.sources.add(node);
264
+ node.tValue = nextValue;
265
+ } else
266
+ node.value = nextValue;
267
+ node.updatedAt = time;
268
+ }
269
+ }
270
+ function createComputation(fn, init, pure, state = STALE, options) {
271
+ const c = {
272
+ fn,
273
+ state,
274
+ updatedAt: null,
275
+ owned: null,
276
+ sources: null,
277
+ sourceSlots: null,
278
+ cleanups: null,
279
+ value: init,
280
+ owner: Owner,
281
+ context: Owner ? Owner.context : null,
282
+ pure
283
+ };
284
+ if (Transition && Transition.running) {
285
+ c.state = 0;
286
+ c.tState = state;
287
+ }
288
+ if (Owner === null)
289
+ ;
290
+ else if (Owner !== UNOWNED) {
291
+ if (Transition && Transition.running && Owner.pure) {
292
+ if (!Owner.tOwned)
293
+ Owner.tOwned = [c];
294
+ else
295
+ Owner.tOwned.push(c);
296
+ } else {
297
+ if (!Owner.owned)
298
+ Owner.owned = [c];
299
+ else
300
+ Owner.owned.push(c);
301
+ }
302
+ }
303
+ if (ExternalSourceConfig && c.fn) {
304
+ const sourceFn = c.fn;
305
+ const [track, trigger] = createSignal(undefined, {
306
+ equals: false
307
+ });
308
+ const ordinary = ExternalSourceConfig.factory(sourceFn, trigger);
309
+ onCleanup(() => ordinary.dispose());
310
+ let inTransition;
311
+ const triggerInTransition = () => startTransition(trigger).then(() => {
312
+ if (inTransition) {
313
+ inTransition.dispose();
314
+ inTransition = undefined;
315
+ }
316
+ });
317
+ c.fn = (x) => {
318
+ track();
319
+ if (Transition && Transition.running) {
320
+ if (!inTransition)
321
+ inTransition = ExternalSourceConfig.factory(sourceFn, triggerInTransition);
322
+ return inTransition.track(x);
323
+ }
324
+ return ordinary.track(x);
325
+ };
326
+ }
327
+ return c;
328
+ }
329
+ function runTop(node) {
330
+ const runningTransition = Transition && Transition.running;
331
+ if ((runningTransition ? node.tState : node.state) === 0)
332
+ return;
333
+ if ((runningTransition ? node.tState : node.state) === PENDING)
334
+ return lookUpstream(node);
335
+ if (node.suspense && untrack(node.suspense.inFallback))
336
+ return node.suspense.effects.push(node);
337
+ const ancestors = [node];
338
+ while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) {
339
+ if (runningTransition && Transition.disposed.has(node))
340
+ return;
341
+ if (runningTransition ? node.tState : node.state)
342
+ ancestors.push(node);
343
+ }
344
+ for (let i = ancestors.length - 1;i >= 0; i--) {
345
+ node = ancestors[i];
346
+ if (runningTransition) {
347
+ let top = node, prev = ancestors[i + 1];
348
+ while ((top = top.owner) && top !== prev) {
349
+ if (Transition.disposed.has(top))
350
+ return;
351
+ }
352
+ }
353
+ if ((runningTransition ? node.tState : node.state) === STALE) {
354
+ updateComputation(node);
355
+ } else if ((runningTransition ? node.tState : node.state) === PENDING) {
356
+ const updates = Updates;
357
+ Updates = null;
358
+ runUpdates(() => lookUpstream(node, ancestors[0]), false);
359
+ Updates = updates;
360
+ }
361
+ }
362
+ }
363
+ function runUpdates(fn, init) {
364
+ if (Updates)
365
+ return fn();
366
+ let wait = false;
367
+ if (!init)
368
+ Updates = [];
369
+ if (Effects)
370
+ wait = true;
371
+ else
372
+ Effects = [];
373
+ ExecCount++;
374
+ try {
375
+ const res = fn();
376
+ completeUpdates(wait);
377
+ return res;
378
+ } catch (err) {
379
+ if (!wait)
380
+ Effects = null;
381
+ Updates = null;
382
+ handleError(err);
383
+ }
384
+ }
385
+ function completeUpdates(wait) {
386
+ if (Updates) {
387
+ if (Scheduler && Transition && Transition.running)
388
+ scheduleQueue(Updates);
389
+ else
390
+ runQueue(Updates);
391
+ Updates = null;
392
+ }
393
+ if (wait)
394
+ return;
395
+ let res;
396
+ if (Transition) {
397
+ if (!Transition.promises.size && !Transition.queue.size) {
398
+ const sources = Transition.sources;
399
+ const disposed = Transition.disposed;
400
+ Effects.push.apply(Effects, Transition.effects);
401
+ res = Transition.resolve;
402
+ for (const e of Effects) {
403
+ "tState" in e && (e.state = e.tState);
404
+ delete e.tState;
405
+ }
406
+ Transition = null;
407
+ runUpdates(() => {
408
+ for (const d of disposed)
409
+ cleanNode(d);
410
+ for (const v of sources) {
411
+ v.value = v.tValue;
412
+ if (v.owned) {
413
+ for (let i = 0, len = v.owned.length;i < len; i++)
414
+ cleanNode(v.owned[i]);
415
+ }
416
+ if (v.tOwned)
417
+ v.owned = v.tOwned;
418
+ delete v.tValue;
419
+ delete v.tOwned;
420
+ v.tState = 0;
421
+ }
422
+ setTransPending(false);
423
+ }, false);
424
+ } else if (Transition.running) {
425
+ Transition.running = false;
426
+ Transition.effects.push.apply(Transition.effects, Effects);
427
+ Effects = null;
428
+ setTransPending(true);
429
+ return;
430
+ }
431
+ }
432
+ const e = Effects;
433
+ Effects = null;
434
+ if (e.length)
435
+ runUpdates(() => runEffects(e), false);
436
+ if (res)
437
+ res();
438
+ }
439
+ function runQueue(queue) {
440
+ for (let i = 0;i < queue.length; i++)
441
+ runTop(queue[i]);
442
+ }
443
+ function scheduleQueue(queue) {
444
+ for (let i = 0;i < queue.length; i++) {
445
+ const item = queue[i];
446
+ const tasks = Transition.queue;
447
+ if (!tasks.has(item)) {
448
+ tasks.add(item);
449
+ Scheduler(() => {
450
+ tasks.delete(item);
451
+ runUpdates(() => {
452
+ Transition.running = true;
453
+ runTop(item);
454
+ }, false);
455
+ Transition && (Transition.running = false);
456
+ });
457
+ }
458
+ }
459
+ }
460
+ function runUserEffects(queue) {
461
+ let i, userLength = 0;
462
+ for (i = 0;i < queue.length; i++) {
463
+ const e = queue[i];
464
+ if (!e.user)
465
+ runTop(e);
466
+ else
467
+ queue[userLength++] = e;
468
+ }
469
+ if (sharedConfig.context) {
470
+ if (sharedConfig.count) {
471
+ sharedConfig.effects || (sharedConfig.effects = []);
472
+ sharedConfig.effects.push(...queue.slice(0, userLength));
473
+ return;
474
+ }
475
+ setHydrateContext();
476
+ }
477
+ if (sharedConfig.effects && (sharedConfig.done || !sharedConfig.count)) {
478
+ queue = [...sharedConfig.effects, ...queue];
479
+ userLength += sharedConfig.effects.length;
480
+ delete sharedConfig.effects;
481
+ }
482
+ for (i = 0;i < userLength; i++)
483
+ runTop(queue[i]);
484
+ }
485
+ function lookUpstream(node, ignore) {
486
+ const runningTransition = Transition && Transition.running;
487
+ if (runningTransition)
488
+ node.tState = 0;
489
+ else
490
+ node.state = 0;
491
+ for (let i = 0;i < node.sources.length; i += 1) {
492
+ const source = node.sources[i];
493
+ if (source.sources) {
494
+ const state = runningTransition ? source.tState : source.state;
495
+ if (state === STALE) {
496
+ if (source !== ignore && (!source.updatedAt || source.updatedAt < ExecCount))
497
+ runTop(source);
498
+ } else if (state === PENDING)
499
+ lookUpstream(source, ignore);
500
+ }
501
+ }
502
+ }
503
+ function markDownstream(node) {
504
+ const runningTransition = Transition && Transition.running;
505
+ for (let i = 0;i < node.observers.length; i += 1) {
506
+ const o = node.observers[i];
507
+ if (runningTransition ? !o.tState : !o.state) {
508
+ if (runningTransition)
509
+ o.tState = PENDING;
510
+ else
511
+ o.state = PENDING;
512
+ if (o.pure)
513
+ Updates.push(o);
514
+ else
515
+ Effects.push(o);
516
+ o.observers && markDownstream(o);
517
+ }
518
+ }
519
+ }
520
+ function cleanNode(node) {
521
+ let i;
522
+ if (node.sources) {
523
+ while (node.sources.length) {
524
+ const source = node.sources.pop(), index = node.sourceSlots.pop(), obs = source.observers;
525
+ if (obs && obs.length) {
526
+ const n = obs.pop(), s = source.observerSlots.pop();
527
+ if (index < obs.length) {
528
+ n.sourceSlots[s] = index;
529
+ obs[index] = n;
530
+ source.observerSlots[index] = s;
531
+ }
532
+ }
533
+ }
534
+ }
535
+ if (node.tOwned) {
536
+ for (i = node.tOwned.length - 1;i >= 0; i--)
537
+ cleanNode(node.tOwned[i]);
538
+ delete node.tOwned;
539
+ }
540
+ if (Transition && Transition.running && node.pure) {
541
+ reset(node, true);
542
+ } else if (node.owned) {
543
+ for (i = node.owned.length - 1;i >= 0; i--)
544
+ cleanNode(node.owned[i]);
545
+ node.owned = null;
546
+ }
547
+ if (node.cleanups) {
548
+ for (i = node.cleanups.length - 1;i >= 0; i--)
549
+ node.cleanups[i]();
550
+ node.cleanups = null;
551
+ }
552
+ if (Transition && Transition.running)
553
+ node.tState = 0;
554
+ else
555
+ node.state = 0;
556
+ }
557
+ function reset(node, top) {
558
+ if (!top) {
559
+ node.tState = 0;
560
+ Transition.disposed.add(node);
561
+ }
562
+ if (node.owned) {
563
+ for (let i = 0;i < node.owned.length; i++)
564
+ reset(node.owned[i]);
565
+ }
566
+ }
567
+ function castError(err) {
568
+ if (err instanceof Error)
569
+ return err;
570
+ return new Error(typeof err === "string" ? err : "Unknown error", {
571
+ cause: err
572
+ });
573
+ }
574
+ function runErrors(err, fns, owner) {
575
+ try {
576
+ for (const f of fns)
577
+ f(err);
578
+ } catch (e) {
579
+ handleError(e, owner && owner.owner || null);
580
+ }
581
+ }
582
+ function handleError(err, owner = Owner) {
583
+ const fns = ERROR && owner && owner.context && owner.context[ERROR];
584
+ const error = castError(err);
585
+ if (!fns)
586
+ throw error;
587
+ if (Effects)
588
+ Effects.push({
589
+ fn() {
590
+ runErrors(error, fns, owner);
591
+ },
592
+ state: STALE
593
+ });
594
+ else
595
+ runErrors(error, fns, owner);
596
+ }
597
+ var FALLBACK = Symbol("fallback");
4
598
 
5
599
  // src/config/ConfigDefaults.ts
6
600
  class ConfigDefaults {
@@ -15,8 +609,11 @@ class ConfigDefaults {
15
609
  }
16
610
  static getDefaultConfig() {
17
611
  return {
612
+ enforceTodoTracker: true,
18
613
  planDirectory: "docs/_planning",
19
- skillMappings: this.getDefaultSkillMappings()
614
+ readOnlyTools: [],
615
+ skillMappings: this.getDefaultSkillMappings(),
616
+ writeCapableTools: []
20
617
  };
21
618
  }
22
619
  }
@@ -39,14 +636,23 @@ class ConfigTypes {
39
636
  class ConfigMerger {
40
637
  static merge(userConfig) {
41
638
  const config = ConfigDefaults.getDefaultConfig();
639
+ if (userConfig.enforceTodoTracker !== undefined) {
640
+ config.enforceTodoTracker = userConfig.enforceTodoTracker;
641
+ }
42
642
  if (userConfig.planDirectory !== undefined) {
43
643
  config.planDirectory = userConfig.planDirectory;
44
644
  }
645
+ if (userConfig.readOnlyTools !== undefined) {
646
+ config.readOnlyTools = userConfig.readOnlyTools;
647
+ }
45
648
  if (userConfig.skillMappings !== undefined) {
46
649
  for (const command of ConfigTypes.getSkillCommands()) {
47
650
  config.skillMappings[command] = userConfig.skillMappings[command] ?? config.skillMappings[command];
48
651
  }
49
652
  }
653
+ if (userConfig.writeCapableTools !== undefined) {
654
+ config.writeCapableTools = userConfig.writeCapableTools;
655
+ }
50
656
  return config;
51
657
  }
52
658
  }
@@ -58,6 +664,11 @@ class ConfigValidator {
58
664
  if (config.planDirectory.trim() === "") {
59
665
  errors.push("Invalid planDirectory: must be a non-empty string.");
60
666
  }
667
+ for (const [option, tools] of [["readOnlyTools", config.readOnlyTools], ["writeCapableTools", config.writeCapableTools]]) {
668
+ if (tools.some((tool) => tool.trim() === "")) {
669
+ errors.push(`Invalid ${option}: tool names must be non-empty strings.`);
670
+ }
671
+ }
61
672
  for (const command of ConfigTypes.getSkillCommands()) {
62
673
  if (config.skillMappings[command].trim() === "") {
63
674
  errors.push(`Invalid skillMappings.${command}: must be a non-empty string.`);
@@ -84,6 +695,23 @@ class ConfigLoader {
84
695
  const directory = this.stringOption(value)?.replace(/\/+$/, "");
85
696
  return directory === "" ? undefined : directory;
86
697
  }
698
+ static booleanOption(value) {
699
+ return typeof value === "boolean" ? value : undefined;
700
+ }
701
+ static toolNamesOption(value) {
702
+ if (!Array.isArray(value)) {
703
+ return;
704
+ }
705
+ const toolNames = [];
706
+ for (const valueTool of value) {
707
+ const tool = this.stringOption(valueTool);
708
+ if (tool === undefined) {
709
+ return;
710
+ }
711
+ toolNames.push(tool);
712
+ }
713
+ return toolNames;
714
+ }
87
715
  static skillMappingsOption(value) {
88
716
  if (typeof value !== "object" || value === null) {
89
717
  return;
@@ -99,11 +727,17 @@ class ConfigLoader {
99
727
  return skillMappings;
100
728
  }
101
729
  static getUserConfig(options) {
730
+ const enforceTodoTracker = this.booleanOption(options?.enforceTodoTracker);
102
731
  const planDirectory = this.planDirectoryOption(options?.planDirectory);
732
+ const readOnlyTools = this.toolNamesOption(options?.readOnlyTools);
103
733
  const skillMappings = this.skillMappingsOption(options?.skillMappings);
734
+ const writeCapableTools = this.toolNamesOption(options?.writeCapableTools);
104
735
  return {
736
+ ...enforceTodoTracker === undefined ? {} : { enforceTodoTracker },
105
737
  ...planDirectory === undefined ? {} : { planDirectory },
106
- ...skillMappings === undefined ? {} : { skillMappings }
738
+ ...readOnlyTools === undefined ? {} : { readOnlyTools },
739
+ ...skillMappings === undefined ? {} : { skillMappings },
740
+ ...writeCapableTools === undefined ? {} : { writeCapableTools }
107
741
  };
108
742
  }
109
743
  static loadConfig(options) {
@@ -319,22 +953,382 @@ class FilePickerCommands {
319
953
  }
320
954
  }
321
955
 
956
+ // src/lib/TodoTracker.ts
957
+ var todoTrackerInput = {
958
+ type: "object",
959
+ additionalProperties: false,
960
+ required: ["todos"],
961
+ properties: {
962
+ todos: {
963
+ type: "array",
964
+ items: {
965
+ type: "object",
966
+ additionalProperties: false,
967
+ required: ["content", "status", "priority"],
968
+ properties: {
969
+ content: { type: "string", minLength: 1, pattern: "\\S" },
970
+ status: { enum: ["pending", "in_progress", "completed", "cancelled"] },
971
+ priority: { enum: ["high", "medium", "low"] }
972
+ }
973
+ }
974
+ }
975
+ }
976
+ };
977
+ var todoTrackerName = "todotracker";
978
+ 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.";
979
+ var trackerRefreshMessage = "Update todotracker with the complete current todo list, including newly discovered work and current statuses, before continuing tool work.";
980
+ var todoStatuses = ["pending", "in_progress", "completed", "cancelled"];
981
+ var todoPriorities = ["high", "medium", "low"];
982
+ function isRecord(value) {
983
+ return typeof value === "object" && value !== null && !Array.isArray(value);
984
+ }
985
+ function hasOnlyKeys(value, keys) {
986
+ const expected = new Set(keys);
987
+ return Object.keys(value).length === keys.length && Object.keys(value).every((key) => expected.has(key));
988
+ }
989
+ function isTodoStatus(value) {
990
+ return typeof value === "string" && todoStatuses.includes(value);
991
+ }
992
+ function isTodoPriority(value) {
993
+ return typeof value === "string" && todoPriorities.includes(value);
994
+ }
995
+ function isTodo(value) {
996
+ if (!isRecord(value) || !hasOnlyKeys(value, ["content", "status", "priority"])) {
997
+ return false;
998
+ }
999
+ return typeof value.content === "string" && value.content.length >= 1 && /\S/.test(value.content) && isTodoStatus(value.status) && isTodoPriority(value.priority);
1000
+ }
1001
+ function isTodoList(value) {
1002
+ return Array.isArray(value) && value.every(isTodo);
1003
+ }
1004
+ function isTodoTrackerInput(value) {
1005
+ return isRecord(value) && hasOnlyKeys(value, ["todos"]) && isTodoList(value.todos);
1006
+ }
1007
+ function isTodoResultMetadata(value) {
1008
+ return isTodoTrackerInput(value);
1009
+ }
1010
+ function todosFromToolResult(metadata, content) {
1011
+ if (isTodoResultMetadata(metadata)) {
1012
+ return metadata.todos;
1013
+ }
1014
+ for (const item of content) {
1015
+ if (item.type !== "text" || item.text === undefined) {
1016
+ continue;
1017
+ }
1018
+ try {
1019
+ const todos = JSON.parse(item.text);
1020
+ if (isTodoList(todos)) {
1021
+ return todos;
1022
+ }
1023
+ } catch {}
1024
+ }
1025
+ return [];
1026
+ }
1027
+ function todosFromMessages(messages) {
1028
+ for (const message of messages.toReversed()) {
1029
+ if (message.type !== "assistant")
1030
+ continue;
1031
+ for (const part of message.content.toReversed()) {
1032
+ if (part.type === "tool" && part.name === todoTrackerName && part.state.status === "completed") {
1033
+ return todosFromToolResult(part.state.metadata, part.state.content);
1034
+ }
1035
+ }
1036
+ }
1037
+ return [];
1038
+ }
1039
+ var builtInReadOnlyTools = new Set(["glob", "grep", "lsp", "read", "skill", "webfetch", "websearch"]);
1040
+ var builtInWriteCapableTools = new Set(["edit", "patch", "shell", "subagent", "write"]);
1041
+ var compatibilityWriteCapableTools = new Set(["apply_patch", "bash"]);
1042
+ var readToolsBeforeRefresh = 10;
1043
+ function refreshedTrackerState() {
1044
+ return { kind: "tracked", readToolsSinceRefresh: 0, requiresRefreshBeforeWrite: false };
1045
+ }
1046
+ function isReadOnlyTool(tool, config) {
1047
+ return builtInReadOnlyTools.has(tool) || config.readOnlyTools.includes(tool);
1048
+ }
1049
+ function isWriteCapableTool(tool, config) {
1050
+ return builtInWriteCapableTools.has(tool) || compatibilityWriteCapableTools.has(tool) || config.writeCapableTools.includes(tool);
1051
+ }
1052
+ function requiresTrackerRefresh(state, tool, config) {
1053
+ if (state.kind === "untracked" || tool === todoTrackerName) {
1054
+ return false;
1055
+ }
1056
+ if (isWriteCapableTool(tool, config)) {
1057
+ return state.requiresRefreshBeforeWrite;
1058
+ }
1059
+ if (isReadOnlyTool(tool, config)) {
1060
+ return state.readToolsSinceRefresh >= readToolsBeforeRefresh;
1061
+ }
1062
+ return false;
1063
+ }
1064
+ function requiresTrackerRefreshBeforeMoreWork(state) {
1065
+ return state.kind === "tracked" && (state.requiresRefreshBeforeWrite || state.readToolsSinceRefresh >= readToolsBeforeRefresh);
1066
+ }
1067
+ function trackerStateFromMessages(messages, config) {
1068
+ let state = { kind: "untracked" };
1069
+ for (const message of messages) {
1070
+ if (message.type !== "assistant") {
1071
+ continue;
1072
+ }
1073
+ for (const part of message.content) {
1074
+ if (part.type !== "tool" || part.state.status !== "completed") {
1075
+ continue;
1076
+ }
1077
+ if (part.name === todoTrackerName) {
1078
+ state = refreshedTrackerState();
1079
+ } else if (state.kind === "tracked") {
1080
+ if (isWriteCapableTool(part.name, config)) {
1081
+ state.requiresRefreshBeforeWrite = true;
1082
+ } else if (isReadOnlyTool(part.name, config)) {
1083
+ state.readToolsSinceRefresh += 1;
1084
+ }
1085
+ }
1086
+ }
1087
+ }
1088
+ return state;
1089
+ }
1090
+
1091
+ class TodoTrackerEnforcement {
1092
+ context;
1093
+ config;
1094
+ states = new Map;
1095
+ constructor(context, config) {
1096
+ this.context = context;
1097
+ this.config = config;
1098
+ }
1099
+ async stateFor(sessionID) {
1100
+ const existing = this.states.get(sessionID);
1101
+ if (existing !== undefined) {
1102
+ return existing;
1103
+ }
1104
+ const state = trackerStateFromMessages(await this.context.session.context({ sessionID }), this.config);
1105
+ this.states.set(sessionID, state);
1106
+ return state;
1107
+ }
1108
+ async markWriteRequiresRefresh(sessionID) {
1109
+ const state = await this.stateFor(sessionID);
1110
+ if (state.kind === "tracked") {
1111
+ state.requiresRefreshBeforeWrite = true;
1112
+ }
1113
+ }
1114
+ async register() {
1115
+ let before;
1116
+ let after;
1117
+ let context;
1118
+ try {
1119
+ before = await this.context.tool.hook("execute.before", async (event) => {
1120
+ const state = await this.stateFor(event.sessionID);
1121
+ if (requiresTrackerRefresh(state, event.tool, this.config)) {
1122
+ throw new Error(trackerRefreshMessage);
1123
+ }
1124
+ if (state.kind === "tracked") {
1125
+ if (isWriteCapableTool(event.tool, this.config)) {
1126
+ state.requiresRefreshBeforeWrite = true;
1127
+ } else if (isReadOnlyTool(event.tool, this.config)) {
1128
+ state.readToolsSinceRefresh += 1;
1129
+ }
1130
+ }
1131
+ });
1132
+ after = await this.context.tool.hook("execute.after", async (event) => {
1133
+ if (event.status !== "completed") {
1134
+ return;
1135
+ }
1136
+ if (event.tool === todoTrackerName) {
1137
+ this.states.set(event.sessionID, refreshedTrackerState());
1138
+ return;
1139
+ }
1140
+ const state = await this.stateFor(event.sessionID);
1141
+ if (state.kind !== "tracked") {
1142
+ return;
1143
+ }
1144
+ });
1145
+ context = await this.context.session.hook("context", async (event) => {
1146
+ const state = await this.stateFor(event.sessionID);
1147
+ if (requiresTrackerRefreshBeforeMoreWork(state)) {
1148
+ event.system.push({ type: "text", text: trackerRefreshMessage });
1149
+ }
1150
+ });
1151
+ const beforeRegistration = before;
1152
+ const afterRegistration = after;
1153
+ const contextRegistration = context;
1154
+ const controller = new AbortController;
1155
+ const observer = this.observeSubagentCompletion(controller.signal);
1156
+ return {
1157
+ dispose: async () => {
1158
+ controller.abort();
1159
+ await Promise.all([
1160
+ beforeRegistration.dispose(),
1161
+ afterRegistration.dispose(),
1162
+ contextRegistration.dispose(),
1163
+ observer
1164
+ ]);
1165
+ }
1166
+ };
1167
+ } catch (error) {
1168
+ await Promise.all([before?.dispose(), after?.dispose(), context?.dispose()]);
1169
+ throw error;
1170
+ }
1171
+ }
1172
+ async observeSubagentCompletion(signal) {
1173
+ try {
1174
+ for await (const event of this.context.event.subscribe({ signal })) {
1175
+ if (event.type !== "session.idle") {
1176
+ continue;
1177
+ }
1178
+ const session = await this.context.session.get({ sessionID: event.data.sessionID });
1179
+ if (session.parentID !== undefined) {
1180
+ await this.markWriteRequiresRefresh(session.parentID);
1181
+ }
1182
+ }
1183
+ } catch (error) {
1184
+ if (!signal.aborted) {
1185
+ console.error("Unable to watch subagent completion for todo enforcement.", error);
1186
+ }
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ class TodoTracker {
1192
+ static marker(status) {
1193
+ switch (status) {
1194
+ case "pending":
1195
+ return "[ ]";
1196
+ case "in_progress":
1197
+ return "[~]";
1198
+ case "completed":
1199
+ return "[x]";
1200
+ case "cancelled":
1201
+ return "[-]";
1202
+ }
1203
+ }
1204
+ static sidebar(messages, completed = false) {
1205
+ const todos = todosFromMessages(messages);
1206
+ if (!completed) {
1207
+ return todos;
1208
+ }
1209
+ return todos.map((todo) => todo.status === "pending" || todo.status === "in_progress" ? { ...todo, status: "completed" } : todo);
1210
+ }
1211
+ static async register(context, config) {
1212
+ const tool = await context.tool.transform((tools) => {
1213
+ tools.add({
1214
+ name: todoTrackerName,
1215
+ description: todoTrackerDescription,
1216
+ input: todoTrackerInput,
1217
+ options: { codemode: false },
1218
+ async execute(input) {
1219
+ if (!isTodoTrackerInput(input)) {
1220
+ throw new Error("Invalid todotracker input.");
1221
+ }
1222
+ return { content: JSON.stringify(input.todos), metadata: { todos: input.todos } };
1223
+ }
1224
+ });
1225
+ });
1226
+ if (!config.enforceTodoTracker) {
1227
+ return tool;
1228
+ }
1229
+ try {
1230
+ const enforcement = await new TodoTrackerEnforcement(context, config).register();
1231
+ return {
1232
+ dispose: async () => {
1233
+ try {
1234
+ await enforcement.dispose();
1235
+ } finally {
1236
+ await tool.dispose();
1237
+ }
1238
+ }
1239
+ };
1240
+ } catch (error) {
1241
+ await tool.dispose();
1242
+ throw error;
1243
+ }
1244
+ }
1245
+ }
1246
+
322
1247
  // src/tui/index.tsx
323
- import { jsx, Fragment } from "@opentui/solid/jsx-runtime";
1248
+ import { jsx, jsxs, Fragment } from "@opentui/solid/jsx-runtime";
324
1249
  function PickerCommandLayer(props) {
325
1250
  FilePickerCommands.register(props.context, props.config);
326
1251
  return /* @__PURE__ */ jsx(Fragment, {});
327
1252
  }
1253
+ function TodoSidebar(props) {
1254
+ const todos = () => TodoTracker.sidebar(props.context.data.session.message.list(props.sessionID), props.context.data.session.status(props.sessionID) === "idle");
1255
+ const color = (status) => status === "in_progress" ? props.context.theme.text.feedback.warning.base : props.context.theme.text.muted;
1256
+ const line = (todo) => `${TodoTracker.marker(todo.status)} (${todo.priority}) ${todo.content}`;
1257
+ const plainContent = (items) => items.map(line).join(`
1258
+ `);
1259
+ const content = (items) => new StyledText(items.flatMap((todo, index) => [
1260
+ fg(color(todo.status))(`${line(todo)}${index === items.length - 1 ? "" : `
1261
+ `}`)
1262
+ ]));
1263
+ let heading;
1264
+ let list;
1265
+ const render = () => {
1266
+ const items = todos();
1267
+ if (heading !== undefined) {
1268
+ heading.content = items.length > 0 ? "Todos" : "";
1269
+ }
1270
+ if (list !== undefined) {
1271
+ list.content = content(items);
1272
+ }
1273
+ };
1274
+ const initialTodos = todos();
1275
+ onMount(() => {
1276
+ render();
1277
+ const unlisten = props.context.data.listen?.(() => {
1278
+ render();
1279
+ });
1280
+ onCleanup(() => unlisten?.());
1281
+ props.context.data.session.message.sync(props.sessionID).catch(() => {});
1282
+ });
1283
+ return /* @__PURE__ */ jsxs("box", {
1284
+ flexDirection: "column",
1285
+ children: [
1286
+ /* @__PURE__ */ jsx("text", {
1287
+ ref: (node) => {
1288
+ heading = node;
1289
+ },
1290
+ children: initialTodos.length > 0 ? "Todos" : ""
1291
+ }),
1292
+ /* @__PURE__ */ jsx("text", {
1293
+ ref: (node) => {
1294
+ list = node;
1295
+ },
1296
+ fg: props.context.theme.text.muted,
1297
+ content: plainContent(initialTodos)
1298
+ })
1299
+ ]
1300
+ });
1301
+ }
328
1302
  var tui_default = Plugin.define({
329
1303
  id: "@encoredigitalgroup/opencode",
330
1304
  setup(ctx) {
331
- return ctx.ui.slot({
1305
+ const appSlot = ctx.ui.slot({
332
1306
  append: "app",
333
1307
  render: () => /* @__PURE__ */ jsx(PickerCommandLayer, {
334
1308
  context: ctx,
335
1309
  config: ConfigLoader.loadConfig(ctx.options)
336
1310
  })
337
1311
  });
1312
+ let sidebarSlot;
1313
+ try {
1314
+ sidebarSlot = ctx.ui.slot({
1315
+ append: "sidebar.content",
1316
+ render: (input) => /* @__PURE__ */ jsx(TodoSidebar, {
1317
+ context: ctx,
1318
+ sessionID: input.sessionID
1319
+ })
1320
+ });
1321
+ } catch (error) {
1322
+ appSlot();
1323
+ throw error;
1324
+ }
1325
+ return () => {
1326
+ try {
1327
+ sidebarSlot();
1328
+ } finally {
1329
+ appSlot();
1330
+ }
1331
+ };
338
1332
  }
339
1333
  });
340
1334
  export {