opencode-docker-panel 0.5.0 → 0.6.0

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/events.js ADDED
@@ -0,0 +1,103 @@
1
+ import { spawn } from "node:child_process";
2
+ const COMMAND = "docker";
3
+ const ARGS = ["events", "--filter", "type=container", "--format", "{{json .}}"];
4
+
5
+ /**
6
+ * The actions that can change what the panel draws.
7
+ *
8
+ * `kill` is left out because it arrives as a pair ahead of `stop` and `die` and changes nothing on its
9
+ * own. `create` is out because the container stays `created` for the half second before `start` and the
10
+ * panel draws no row for that state. `stop` is out because `docker ps` already reports `exited` by the
11
+ * time it arrives. `restart` is out because a measured `docker restart` sends `die` and `start` first,
12
+ * which bracket the whole visible change, so it would repeat the frame `start` just produced.
13
+ * Everything with an `exec_` or `health_` prefix is the container's internals and never reaches the panel.
14
+ */
15
+ export const INTERESTING_ACTIONS = ["start", "die", "destroy", "pause", "unpause", "rename"];
16
+ /** Returns the container name an event is about, or null when the panel has no reason to react */
17
+ export function parseEventLine(line) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed) return null;
20
+ let raw;
21
+ try {
22
+ raw = JSON.parse(trimmed);
23
+ } catch {
24
+ return null;
25
+ }
26
+ if (typeof raw !== "object" || raw === null) return null;
27
+ const event = raw;
28
+ if (event.Type !== "container") return null;
29
+ const action = String(event.Action ?? event.status ?? "");
30
+ if (!INTERESTING_ACTIONS.includes(action)) return null;
31
+ const name = event.Actor?.Attributes?.name;
32
+ return typeof name === "string" && name.length > 0 ? name : null;
33
+ }
34
+ /**
35
+ * A long-lived `docker events` reader.
36
+ *
37
+ * The stream ends loudly rather than going quiet: stopping Docker Desktop leaves it silent for about
38
+ * thirty seconds and then writes `unexpected EOF` and exits with code 1. So the caller is told when the
39
+ * process is gone and decides on its own when reopening is worth the attempt.
40
+ */
41
+ export function createEventWatch() {
42
+ let child;
43
+ let buffer = "";
44
+ let closed = false;
45
+ let consumer;
46
+ let onDead;
47
+ const open = () => {
48
+ if (closed) return;
49
+ const spawned = spawn(COMMAND, ARGS, {
50
+ windowsHide: true,
51
+ stdio: ["ignore", "pipe", "ignore"]
52
+ });
53
+ child = spawned;
54
+ // A docker that is not on the PATH makes spawn emit an error, and an unheard one throws
55
+ spawned.on("error", () => {});
56
+ // A half-written line from the killed process would prepend itself to the new process's first
57
+ // chunk and take that first event down with it
58
+ buffer = "";
59
+ spawned.stdout?.setEncoding("utf8");
60
+ spawned.stdout?.on("data", chunk => {
61
+ // The last split piece is a partial line and has to survive to the next chunk
62
+ const lines = (buffer + chunk).split("\n");
63
+ buffer = lines.pop() ?? "";
64
+ for (const line of lines) {
65
+ if (!parseEventLine(line)) continue;
66
+ consumer?.();
67
+ }
68
+ });
69
+ // Only the process still in hand gets to speak for the stream, and killing one is asynchronous, so
70
+ // after a restart the previous child still reports in. Trusting that report would drop the live
71
+ // child's reference, orphan the process and fake a dead stream. The same check covers our own stop,
72
+ // which clears `child` before killing.
73
+ spawned.on("close", () => {
74
+ if (child !== spawned) return;
75
+ child = undefined;
76
+ onDead?.();
77
+ });
78
+ };
79
+ const stop = () => {
80
+ const current = child;
81
+ child = undefined;
82
+ current?.kill();
83
+ };
84
+ open();
85
+ return {
86
+ onEvent: onChange => {
87
+ consumer = onChange;
88
+ },
89
+ onDead: handler => {
90
+ onDead = handler;
91
+ },
92
+ restart: () => {
93
+ stop();
94
+ open();
95
+ },
96
+ close: () => {
97
+ closed = true;
98
+ consumer = undefined;
99
+ onDead = undefined;
100
+ stop();
101
+ }
102
+ };
103
+ }
package/dist/panel.js CHANGED
@@ -227,9 +227,10 @@ export function DockerPanel(props) {
227
227
  const engine = createEngineProbe();
228
228
  // The probe only has to be re-read when the poll changes its verdict: an available docker proves the
229
229
  // engine answers, and a probe on every poll would stack several four second calls on the same pipe,
230
- // while never re-reading it would leave a stale stopped engine once docker starts from the tray
230
+ // while never re-reading it would leave a stale stopped engine once docker starts from the tray, which
231
+ // poll.ts covers by keeping the poll fast while the last answer said stopped
231
232
  let lastKind = "";
232
- const polling = createDockerPolling(props.intervalMs, props.reload, next => {
233
+ const polling = createDockerPolling(props.reload, next => {
233
234
  refreshStack();
234
235
  if (next.kind === lastKind) return;
235
236
  lastKind = next.kind;
@@ -275,12 +276,12 @@ export function DockerPanel(props) {
275
276
  });
276
277
  await polling.refreshAfterAction();
277
278
  };
278
- /**
279
- * Opens the same actions for a container the sidebar does not show, and carries the one engine
280
- * action last.
281
- *
282
- * The engine entry stays available with no containers at all, because that is exactly when Docker is
283
- * down and it is the only way to turn it back on.
279
+ /**
280
+ * Opens the same actions for a container the sidebar does not show, and carries the one engine
281
+ * action last.
282
+ *
283
+ * The engine entry stays available with no containers at all, because that is exactly when Docker is
284
+ * down and it is the only way to turn it back on.
284
285
  */
285
286
  const pickContainer = async all => {
286
287
  const action = engine.state() === "running" ? "engine-stop" : undefined;
@@ -325,11 +326,11 @@ export function DockerPanel(props) {
325
326
  await pickContainer(polling.state().containers);
326
327
  };
327
328
 
328
- /**
329
- * Both engine actions ask first, and Stop names what it is about to end.
330
- *
331
- * Start confirms too: it brings up a virtual machine and starts consuming memory, so a misclick is
332
- * not free even though nothing is running that it would interrupt.
329
+ /**
330
+ * Both engine actions ask first, and Stop names what it is about to end.
331
+ *
332
+ * Start confirms too: it brings up a virtual machine and starts consuming memory, so a misclick is
333
+ * not free even though nothing is running that it would interrupt.
333
334
  */
334
335
  const runRuntime = async action => {
335
336
  if (busy()) return;
@@ -536,8 +537,9 @@ export function DockerPanel(props) {
536
537
  return _$memo(() => !!(!stopping() && !hasRows()))() && polling.state().containers.length > 0;
537
538
  },
538
539
  get children() {
539
- var _el$30 = _$createElement("text");
540
- _$insertNode(_el$30, _$createTextNode(` click for the list`));
540
+ var _el$30 = _$createElement("text"),
541
+ _el$31 = _$createTextNode(` click for the list`);
542
+ _$insertNode(_el$30, _el$31);
541
543
  _$setProp(_el$30, "onMouseUp", event => arm(openBrowse, event));
542
544
  _$effect(_$p => _$setProp(_el$30, "fg", theme().text.muted, _$p));
543
545
  return _el$30;
@@ -548,27 +550,27 @@ export function DockerPanel(props) {
548
550
  return _$memo(() => !!engineDown())() && !busy();
549
551
  },
550
552
  get children() {
551
- var _el$32 = _$createElement("text"),
552
- _el$33 = _$createElement("b"),
553
- _el$35 = _$createElement("span");
554
- _$insertNode(_el$32, _el$33);
555
- _$insertNode(_el$32, _el$35);
556
- _$setProp(_el$32, "onMouseUp", event => arm(() => void runRuntime("engine-start"), event));
557
- _$insertNode(_el$33, _$createTextNode(`Start Docker Desktop`));
558
- _$insertNode(_el$35, _$createTextNode(` · click to start`));
553
+ var _el$33 = _$createElement("text"),
554
+ _el$34 = _$createElement("b"),
555
+ _el$36 = _$createElement("span");
556
+ _$insertNode(_el$33, _el$34);
557
+ _$insertNode(_el$33, _el$36);
558
+ _$setProp(_el$33, "onMouseUp", event => arm(() => void runRuntime("engine-start"), event));
559
+ _$insertNode(_el$34, _$createTextNode(`Start Docker Desktop`));
560
+ _$insertNode(_el$36, _$createTextNode(` · click to start`));
559
561
  _$effect(_p$ => {
560
562
  var _v$6 = theme().text.base,
561
563
  _v$7 = {
562
564
  fg: theme().text.muted
563
565
  };
564
- _v$6 !== _p$.e && (_p$.e = _$setProp(_el$32, "fg", _v$6, _p$.e));
565
- _v$7 !== _p$.t && (_p$.t = _$setProp(_el$35, "style", _v$7, _p$.t));
566
+ _v$6 !== _p$.e && (_p$.e = _$setProp(_el$33, "fg", _v$6, _p$.e));
567
+ _v$7 !== _p$.t && (_p$.t = _$setProp(_el$36, "style", _v$7, _p$.t));
566
568
  return _p$;
567
569
  }, {
568
570
  e: undefined,
569
571
  t: undefined
570
572
  });
571
- return _el$32;
573
+ return _el$33;
572
574
  }
573
575
  }), null);
574
576
  _$insert(_el$18, _$createComponent(Show, {
@@ -576,12 +578,12 @@ export function DockerPanel(props) {
576
578
  return _$memo(() => !!(!engineDown() && polling.state().containers.length === 0))() && polling.state().detail.length > 0;
577
579
  },
578
580
  get children() {
579
- var _el$37 = _$createElement("text"),
580
- _el$38 = _$createTextNode(` `);
581
- _$insertNode(_el$37, _el$38);
582
- _$insert(_el$37, () => polling.state().detail, null);
583
- _$effect(_$p => _$setProp(_el$37, "fg", theme().text.muted, _$p));
584
- return _el$37;
581
+ var _el$38 = _$createElement("text"),
582
+ _el$39 = _$createTextNode(` `);
583
+ _$insertNode(_el$38, _el$39);
584
+ _$insert(_el$38, () => polling.state().detail, null);
585
+ _$effect(_$p => _$setProp(_el$38, "fg", theme().text.muted, _$p));
586
+ return _el$38;
585
587
  }
586
588
  }), null);
587
589
  _$insert(_el$18, _$createComponent(Show, {
@@ -594,86 +596,86 @@ export function DockerPanel(props) {
594
596
  return visible();
595
597
  },
596
598
  children: container => (() => {
597
- var _el$47 = _$createElement("box"),
598
- _el$48 = _$createElement("text"),
599
- _el$50 = _$createElement("text"),
600
- _el$51 = _$createTextNode(` `),
601
- _el$52 = _$createElement("span");
602
- _$insertNode(_el$47, _el$48);
603
- _$insertNode(_el$47, _el$50);
604
- _$setProp(_el$47, "flexDirection", "row");
605
- _$setProp(_el$47, "gap", 1);
606
- _$setProp(_el$47, "onMouseUp", event => arm(() => openMenu(container()), event));
607
- _$insertNode(_el$48, _$createTextNode(`•`));
608
- _$setProp(_el$48, "flexShrink", 0);
609
- _$insertNode(_el$50, _el$51);
610
- _$insertNode(_el$50, _el$52);
611
- _$setProp(_el$50, "wrapMode", "word");
612
- _$insert(_el$50, () => container().name, _el$51);
613
- _$insert(_el$52, () => detailLabel(container(), props.pinned));
599
+ var _el$48 = _$createElement("box"),
600
+ _el$49 = _$createElement("text"),
601
+ _el$51 = _$createElement("text"),
602
+ _el$52 = _$createTextNode(` `),
603
+ _el$53 = _$createElement("span");
604
+ _$insertNode(_el$48, _el$49);
605
+ _$insertNode(_el$48, _el$51);
606
+ _$setProp(_el$48, "flexDirection", "row");
607
+ _$setProp(_el$48, "gap", 1);
608
+ _$setProp(_el$48, "onMouseUp", event => arm(() => openMenu(container()), event));
609
+ _$insertNode(_el$49, _$createTextNode(`•`));
610
+ _$setProp(_el$49, "flexShrink", 0);
611
+ _$insertNode(_el$51, _el$52);
612
+ _$insertNode(_el$51, _el$53);
613
+ _$setProp(_el$51, "wrapMode", "word");
614
+ _$insert(_el$51, () => container().name, _el$52);
615
+ _$insert(_el$53, () => detailLabel(container(), props.pinned));
614
616
  _$effect(_p$ => {
615
617
  var _v$0 = dotColor(theme(), container().state),
616
618
  _v$1 = theme().text.base,
617
619
  _v$10 = {
618
620
  fg: theme().text.muted
619
621
  };
620
- _v$0 !== _p$.e && (_p$.e = _$setProp(_el$48, "fg", _v$0, _p$.e));
621
- _v$1 !== _p$.t && (_p$.t = _$setProp(_el$50, "fg", _v$1, _p$.t));
622
- _v$10 !== _p$.a && (_p$.a = _$setProp(_el$52, "style", _v$10, _p$.a));
622
+ _v$0 !== _p$.e && (_p$.e = _$setProp(_el$49, "fg", _v$0, _p$.e));
623
+ _v$1 !== _p$.t && (_p$.t = _$setProp(_el$51, "fg", _v$1, _p$.t));
624
+ _v$10 !== _p$.a && (_p$.a = _$setProp(_el$53, "style", _v$10, _p$.a));
623
625
  return _p$;
624
626
  }, {
625
627
  e: undefined,
626
628
  t: undefined,
627
629
  a: undefined
628
630
  });
629
- return _el$47;
631
+ return _el$48;
630
632
  })()
631
633
  }), _$createComponent(Show, {
632
634
  get when() {
633
635
  return hasRows();
634
636
  },
635
637
  get children() {
636
- var _el$39 = _$createElement("text"),
637
- _el$40 = _$createTextNode(` `);
638
- _$insertNode(_el$39, _el$40);
639
- _$setProp(_el$39, "onMouseUp", event => arm(openBrowse, event));
640
- _$insert(_el$39, browseLabel, null);
641
- _$effect(_$p => _$setProp(_el$39, "fg", theme().text.muted, _$p));
642
- return _el$39;
638
+ var _el$40 = _$createElement("text"),
639
+ _el$41 = _$createTextNode(` `);
640
+ _$insertNode(_el$40, _el$41);
641
+ _$setProp(_el$40, "onMouseUp", event => arm(openBrowse, event));
642
+ _$insert(_el$40, browseLabel, null);
643
+ _$effect(_$p => _$setProp(_el$40, "fg", theme().text.muted, _$p));
644
+ return _el$40;
643
645
  }
644
646
  })];
645
647
  }
646
648
  }), null);
647
649
  _$insert(_el$18, _$createComponent(Show, {
648
650
  get when() {
649
- return _$memo(() => !!pendingStack())() && !engineDown();
651
+ return _$memo(() => !!(pendingStack() && !engineDown()))() && !stopping();
650
652
  },
651
653
  get children() {
652
- var _el$41 = _$createElement("text"),
653
- _el$42 = _$createElement("b"),
654
- _el$44 = _$createElement("span"),
655
- _el$45 = _$createTextNode(` · `),
656
- _el$46 = _$createTextNode(` is not running here`);
657
- _$insertNode(_el$41, _el$42);
658
- _$insertNode(_el$41, _el$44);
659
- _$setProp(_el$41, "onMouseUp", event => arm(() => void openStack(), event));
660
- _$insertNode(_el$42, _$createTextNode(`Up stack`));
661
- _$insertNode(_el$44, _el$45);
662
- _$insertNode(_el$44, _el$46);
663
- _$insert(_el$44, () => pendingStack()?.project, _el$46);
654
+ var _el$42 = _$createElement("text"),
655
+ _el$43 = _$createElement("b"),
656
+ _el$45 = _$createElement("span"),
657
+ _el$46 = _$createTextNode(` · `),
658
+ _el$47 = _$createTextNode(` is not running here`);
659
+ _$insertNode(_el$42, _el$43);
660
+ _$insertNode(_el$42, _el$45);
661
+ _$setProp(_el$42, "onMouseUp", event => arm(() => void openStack(), event));
662
+ _$insertNode(_el$43, _$createTextNode(`Up stack`));
663
+ _$insertNode(_el$45, _el$46);
664
+ _$insertNode(_el$45, _el$47);
665
+ _$insert(_el$45, () => pendingStack()?.project, _el$47);
664
666
  _$effect(_p$ => {
665
667
  var _v$8 = theme().text.base,
666
668
  _v$9 = {
667
669
  fg: theme().text.muted
668
670
  };
669
- _v$8 !== _p$.e && (_p$.e = _$setProp(_el$41, "fg", _v$8, _p$.e));
670
- _v$9 !== _p$.t && (_p$.t = _$setProp(_el$44, "style", _v$9, _p$.t));
671
+ _v$8 !== _p$.e && (_p$.e = _$setProp(_el$42, "fg", _v$8, _p$.e));
672
+ _v$9 !== _p$.t && (_p$.t = _$setProp(_el$45, "style", _v$9, _p$.t));
671
673
  return _p$;
672
674
  }, {
673
675
  e: undefined,
674
676
  t: undefined
675
677
  });
676
- return _el$41;
678
+ return _el$42;
677
679
  }
678
680
  }), null);
679
681
  _$effect(_$p => _$setProp(_el$20, "fg", theme().text.base, _$p));
package/dist/poll.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createSignal, onCleanup } from "solid-js";
2
+ import { createEventWatch } from "./events.js";
2
3
  import { runDockerPs } from "./docker.js";
3
4
  import { runDesktopStatus } from "./runtime.js";
4
5
  const INITIAL = {
@@ -10,10 +11,26 @@ const INITIAL = {
10
11
  /** docker reports the old state for a moment after start or stop, so an action is polled twice */
11
12
  const SETTLE_MS = 1200;
12
13
 
13
- /**
14
- * A Docker Desktop cold start boots a virtual machine, which takes a minute and answers `unknown` while
15
- * it goes. Polling faster than the container loop is what makes the panel notice it came up at all,
16
- * and the deadline is what stops a dead engine from holding `working` forever.
14
+ /**
15
+ * Events drive the panel now, so this is only the net for the case where docker says nothing at all:
16
+ * a container changed outside anything we would hear about, or the stream is gone and the engine has
17
+ * come back since the last attempt.
18
+ */
19
+ export const FALLBACK_INTERVAL_MS = 30000;
20
+
21
+ /**
22
+ * Nothing can change while the engine is down, so a start from the tray is the one thing worth waiting
23
+ * on closely, and the engine probe has no timer of its own to notice it.
24
+ */
25
+ const ENGINE_DOWN_RETRY_MS = 3000;
26
+ export function fallbackDelayMs(kind) {
27
+ return kind === "stopped" ? ENGINE_DOWN_RETRY_MS : FALLBACK_INTERVAL_MS;
28
+ }
29
+
30
+ /**
31
+ * A Docker Desktop cold start boots a virtual machine, which takes a minute and answers `unknown` while
32
+ * it goes. Polling faster than the container loop is what makes the panel notice it came up at all,
33
+ * and the deadline is what stops a dead engine from holding `working` forever.
17
34
  */
18
35
  export const ENGINE_TRANSITION_MS = 2000;
19
36
  export const ENGINE_DEADLINE_MS = 120000;
@@ -27,11 +44,11 @@ export function keepContainers(previous, next) {
27
44
  /** The sidebar belongs to what runs right now, not to every container that exists */
28
45
  export const MAX_RUNNING = 5;
29
46
 
30
- /**
31
- * A pinned container is shown whatever its state and spends the running budget when it happens to
32
- * run, so pinning everything shows everything.
33
- *
34
- * Stopped containers get no row, the dialog is their only door.
47
+ /**
48
+ * A pinned container is shown whatever its state and spends the running budget when it happens to
49
+ * run, so pinning everything shows everything.
50
+ *
51
+ * Stopped containers get no row, the dialog is their only door.
35
52
  */
36
53
  export function selectRows(all, pinned) {
37
54
  const isPinned = item => pinned.includes(item.name);
@@ -39,20 +56,27 @@ export function selectRows(all, pinned) {
39
56
  const running = all.filter(item => !isPinned(item) && item.state === "running");
40
57
  return [...fixed, ...running.slice(0, Math.max(0, MAX_RUNNING - fixed.length))];
41
58
  }
42
- /**
43
- * The engine has its own probe because `docker ps` cannot report it: on a stopped engine the ps call
44
- * fails to connect and says only `unavailable`, which is also what a missing CLI or a locked socket
45
- * produces. `docker desktop status` is the only thing that tells those apart.
59
+ /**
60
+ * A stream that has closed is only worth reopening once docker answers again. Reopening while the
61
+ * engine is down would spawn a process that cannot work, every tick, for as long as the engine is off.
62
+ */
63
+ export function shouldReconnect(kind, streamAlive) {
64
+ return !streamAlive && (kind === "ok" || kind === "empty");
65
+ }
66
+ /**
67
+ * The engine has its own probe because `docker ps` cannot report it: on a stopped engine the ps call
68
+ * fails to connect and says only `unavailable`, which is also what a missing CLI or a locked socket
69
+ * produces. `docker desktop status` is the only thing that tells those apart.
46
70
  */
47
71
  export function createEngineProbe() {
48
72
  const [state, setState] = createSignal("unknown");
49
73
  let disposed = false;
50
74
  let probing = false;
51
75
 
52
- /**
53
- * A stopped engine takes over four seconds to answer, which is longer than the container poll, so a
54
- * probe per poll would leave several of them waiting on the same pipe at once. One at a time is the
55
- * whole guard: a call that arrives while one is in flight reads the last known state instead.
76
+ /**
77
+ * A stopped engine takes over four seconds to answer, which is longer than the container poll, so a
78
+ * probe per poll would leave several of them waiting on the same pipe at once. One at a time is the
79
+ * whole guard: a call that arrives while one is in flight reads the last known state instead.
56
80
  */
57
81
  const refresh = async () => {
58
82
  if (probing) return state();
@@ -93,17 +117,26 @@ function signature(containers) {
93
117
 
94
118
  /** A container in a crash loop changes state on every poll and each remount costs a fresh docker ps */
95
119
  const RELOAD_COOLDOWN_MS = 10000;
96
- export function createDockerPolling(intervalMs, onContentChange, onPoll) {
120
+ export function createDockerPolling(onContentChange, onPoll) {
97
121
  const [state, setState] = createSignal(INITIAL);
98
122
  let timer;
99
123
  let settle;
100
124
  let disposed = false;
101
- let last = "";
102
- let seen = false;
125
+ let shown = "";
126
+ let mounted = false;
103
127
  let reloadedAt = 0;
104
- const refresh = async () => {
105
- const next = await runDockerPs();
128
+
129
+ /**
130
+ * `shown` is what the host has been told, not what was last seen. A change that arrives inside the
131
+ * cooldown has to stay owed rather than be written off, because the signature would match on every
132
+ * later poll and nothing would ever deliver it.
133
+ */
134
+ const apply = next => {
106
135
  if (disposed) return;
136
+ if (shouldReconnect(next.kind, streamAlive)) {
137
+ streamAlive = true;
138
+ events.restart();
139
+ }
107
140
  const visible = keepContainers(state().containers, next);
108
141
  setState({
109
142
  ...next,
@@ -112,26 +145,58 @@ export function createDockerPolling(intervalMs, onContentChange, onPoll) {
112
145
  onPoll?.(next);
113
146
  if (next.kind === "unavailable" || next.kind === "stale") return;
114
147
  const current = signature(visible);
115
- if (current === last) return;
116
- last = current;
117
- if (!seen) {
118
- seen = true;
148
+ if (!mounted) {
149
+ mounted = true;
150
+ shown = current;
119
151
  return;
120
152
  }
153
+ if (current === shown) return;
121
154
  if (Date.now() - reloadedAt < RELOAD_COOLDOWN_MS) return;
155
+ shown = current;
122
156
  reloadedAt = Date.now();
123
157
  onContentChange();
124
158
  };
159
+
160
+ /**
161
+ * One `docker ps` at a time. Events arrive faster than a ps call returns, and without this the older
162
+ * result lands last and overwrites the newer one. A call that arrives while one is in flight asks for
163
+ * one more pass instead of starting a parallel one, the same guard `createEngineProbe` uses.
164
+ */
165
+ let reading = false;
166
+ let reread = false;
167
+ const refresh = async () => {
168
+ if (reading) {
169
+ reread = true;
170
+ return;
171
+ }
172
+ reading = true;
173
+ try {
174
+ do {
175
+ reread = false;
176
+ apply(await runDockerPs());
177
+ } while (reread && !disposed);
178
+ } finally {
179
+ reading = false;
180
+ }
181
+ };
182
+ const events = createEventWatch();
183
+ let streamAlive = true;
184
+ events.onEvent(() => void refresh());
185
+ events.onDead(() => {
186
+ streamAlive = false;
187
+ });
125
188
  const tick = async () => {
126
189
  await refresh();
127
190
  if (disposed) return;
128
- timer = setTimeout(tick, intervalMs);
191
+ timer = setTimeout(tick, fallbackDelayMs(state().kind));
129
192
  };
130
193
  void tick();
131
194
  onCleanup(() => {
132
195
  disposed = true;
133
196
  if (timer) clearTimeout(timer);
134
197
  if (settle) clearTimeout(settle);
198
+ // A docker events process outlives the panel otherwise and stays in the task list
199
+ events.close();
135
200
  });
136
201
  return {
137
202
  state,
package/dist/tui.js CHANGED
@@ -1,12 +1,8 @@
1
1
  import { createComponent as _$createComponent } from "@opentui/solid";
2
2
  import { DockerPanel } from "./panel.js";
3
- const MIN_INTERVAL = 1000;
4
- const MAX_INTERVAL = 60000;
5
3
  const plugin = {
6
4
  id: "docker.panel.cli",
7
5
  setup(context) {
8
- const requested = Number(context.options.intervalMs);
9
- const intervalMs = Number.isFinite(requested) ? Math.min(Math.max(requested, MIN_INTERVAL), MAX_INTERVAL) : 3000;
10
6
  const agentDir = context.location?.directory ?? process.cwd();
11
7
  const [pinned, setPinned] = context.storage.store("docker-panel.pinned", {
12
8
  initial: {
@@ -33,7 +29,6 @@ const plugin = {
33
29
  const claim = {
34
30
  append: "sidebar.content",
35
31
  render: () => _$createComponent(DockerPanel, {
36
- intervalMs: intervalMs,
37
32
  agentDir: agentDir,
38
33
  get pinned() {
39
34
  return pinned.names;