@wrongstack/tui 0.5.6 → 0.5.7

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/index.js CHANGED
@@ -4,6 +4,7 @@ import * as fs2 from 'fs/promises';
4
4
  import * as path2 from 'path';
5
5
  import { InputBuilder, DefaultSessionRewinder, formatTodosList, buildChildEnv } from '@wrongstack/core';
6
6
  import { routeImagesForModel } from '@wrongstack/runtime/vision';
7
+ import { getProcessRegistry } from '@wrongstack/tools';
7
8
  import { readClipboardImage } from '@wrongstack/runtime/clipboard';
8
9
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
10
  import { spawn } from 'child_process';
@@ -1630,7 +1631,8 @@ function StatusBar({
1630
1631
  git,
1631
1632
  subagentCount = 0,
1632
1633
  context,
1633
- projectName
1634
+ projectName,
1635
+ processCount
1634
1636
  }) {
1635
1637
  const usage = tokenCounter?.total();
1636
1638
  const cost = tokenCounter?.estimateCost();
@@ -1695,6 +1697,15 @@ function StatusBar({
1695
1697
  queueCount
1696
1698
  ] })
1697
1699
  ] }) : null,
1700
+ typeof processCount === "number" && processCount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1701
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1702
+ /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1703
+ "\u26A1 ",
1704
+ processCount,
1705
+ " process",
1706
+ processCount === 1 ? "" : "es"
1707
+ ] })
1708
+ ] }) : null,
1698
1709
  hint ? /* @__PURE__ */ jsxs(Fragment, { children: [
1699
1710
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1700
1711
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint })
@@ -2092,6 +2103,101 @@ function oneLine(s, max) {
2092
2103
  const collapsed = s.replace(/\s+/g, " ").trim();
2093
2104
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
2094
2105
  }
2106
+ var USAGE2 = "Usage:\n /kill \u2014 list active processes + breaker state\n /kill list \u2014 same as /kill\n /kill all \u2014 kill all tracked processes (SIGTERM \u2192 SIGKILL)\n /kill force \u2014 kill all with SIGKILL immediately\n /kill reset \u2014 reset the circuit breaker to closed\n /kill <pid> \u2014 kill a specific process by PID";
2107
+ function createKillSlashCommand() {
2108
+ return {
2109
+ name: "kill",
2110
+ description: "List or kill active bash/exec processes managed by the process registry.",
2111
+ async run(args) {
2112
+ const trimmed = args.trim();
2113
+ const parts = trimmed.split(/\s+/);
2114
+ const sub = parts[0]?.toLowerCase() ?? "";
2115
+ if (sub === "" || sub === "list") {
2116
+ return { message: renderList2() };
2117
+ }
2118
+ if (sub === "all") {
2119
+ const pids = getProcessRegistry().killAll();
2120
+ if (pids.length === 0) return { message: "No processes to kill." };
2121
+ return { message: `Killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}` };
2122
+ }
2123
+ if (sub === "force") {
2124
+ getProcessRegistry().forceBreakerOpen();
2125
+ const pids = getProcessRegistry().killAll({ force: true });
2126
+ if (pids.length === 0) return { message: "Circuit breaker forced open. No processes to kill." };
2127
+ return { message: `Force-killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}` };
2128
+ }
2129
+ if (sub === "reset") {
2130
+ getProcessRegistry().forceBreakerReset();
2131
+ return { message: "Circuit breaker reset to closed. Bash/exec calls allowed." };
2132
+ }
2133
+ const pid = Number.parseInt(sub, 10);
2134
+ if (!Number.isNaN(pid) && pid > 0) {
2135
+ const found = getProcessRegistry().kill(pid);
2136
+ if (found) return { message: `Killed process ${pid}.` };
2137
+ return { message: `Process ${pid} not found in registry.` };
2138
+ }
2139
+ return { message: `Unknown subcommand "${sub}".
2140
+ ${USAGE2}` };
2141
+ }
2142
+ };
2143
+ }
2144
+ function renderList2() {
2145
+ const registry = getProcessRegistry();
2146
+ const stats = registry.stats();
2147
+ const all = registry.list();
2148
+ const breaker = stats.breaker;
2149
+ const stateLabel = breaker.state === "closed" ? "\u{1F7E2} closed" : breaker.state === "half-open" ? "\u{1F7E1} half-open" : `\u{1F534} open (cooldown ${breaker.cooldownRemainingMs !== null ? `${(breaker.cooldownRemainingMs / 1e3).toFixed(0)}s` : "\u2014"})`;
2150
+ const breakerLine = [
2151
+ ` Circuit breaker: ${stateLabel}`,
2152
+ ` consecutive failures: ${breaker.consecutiveFailures}/5`,
2153
+ ` slow calls in window: ${breaker.slowCallsInWindow}/3`,
2154
+ ` calls in window: ${breaker.callsInWindow}/30`
2155
+ ].join("\n");
2156
+ if (all.length === 0) {
2157
+ return `No active processes.
2158
+
2159
+ ${breakerLine}`;
2160
+ }
2161
+ const now = Date.now();
2162
+ const lines = [`Active processes (${all.length}):`];
2163
+ for (const p of all) {
2164
+ const age = ((now - p.startedAt) / 1e3).toFixed(1);
2165
+ const killedTag = p.killed ? " [killed]" : "";
2166
+ const cmd = p.command.length > 80 ? p.command.slice(0, 77) + "\u2026" : p.command;
2167
+ lines.push(` ${p.pid} ${p.name} ${age}s ${cmd}${killedTag}`);
2168
+ }
2169
+ return [...lines, "", breakerLine].join("\n");
2170
+ }
2171
+ function createPsSlashCommand() {
2172
+ return {
2173
+ name: "ps",
2174
+ description: "List all active bash/exec processes tracked by the process registry.",
2175
+ async run(_args) {
2176
+ return { message: renderList3() };
2177
+ }
2178
+ };
2179
+ }
2180
+ function renderList3() {
2181
+ const registry = getProcessRegistry();
2182
+ const stats = registry.stats();
2183
+ const all = registry.list();
2184
+ if (all.length === 0) return "No active processes.";
2185
+ const breaker = stats.breaker;
2186
+ const stateLabel = breaker.state === "closed" ? "\u{1F7E2} closed" : breaker.state === "half-open" ? "\u{1F7E1} half-open" : `\u{1F534} open`;
2187
+ const now = Date.now();
2188
+ const lines = [
2189
+ `Active processes (${all.length}) \u2014 breaker ${stateLabel}`,
2190
+ ` failure=${breaker.consecutiveFailures}/5 slow=${breaker.slowCallsInWindow}/3 rate=${breaker.callsInWindow}/30`,
2191
+ ""
2192
+ ];
2193
+ for (const p of all) {
2194
+ const age = ((now - p.startedAt) / 1e3).toFixed(1);
2195
+ const killedTag = p.killed ? " [killed]" : "";
2196
+ const cmd = p.command.length > 80 ? p.command.slice(0, 77) + "\u2026" : p.command;
2197
+ lines.push(` ${p.pid} ${p.name} ${age}s ${cmd}${killedTag}`);
2198
+ }
2199
+ return lines.join("\n");
2200
+ }
2095
2201
  function selectedSlashCommandLine(picker) {
2096
2202
  if (!picker.open || picker.matches.length === 0) return null;
2097
2203
  const picked = picker.matches[picker.selected];
@@ -3072,6 +3178,19 @@ function App({
3072
3178
  slashRegistry.unregister("queue");
3073
3179
  };
3074
3180
  }, [slashRegistry]);
3181
+ useEffect(() => {
3182
+ slashRegistry.register(createKillSlashCommand());
3183
+ slashRegistry.register(createPsSlashCommand());
3184
+ return () => {
3185
+ slashRegistry.unregister("kill");
3186
+ slashRegistry.unregister("ps");
3187
+ };
3188
+ }, [slashRegistry]);
3189
+ useEffect(() => {
3190
+ return () => {
3191
+ getProcessRegistry().killAll();
3192
+ };
3193
+ }, []);
3075
3194
  useEffect(() => {
3076
3195
  const ALT_OFF = "\x1B[?1049l";
3077
3196
  const ALT_ON = "\x1B[?1049h";
@@ -3651,6 +3770,7 @@ function App({
3651
3770
  const onSigint = () => {
3652
3771
  const current = stateRef.current;
3653
3772
  if (current.interrupts >= 1) {
3773
+ getProcessRegistry().killAll({ force: true });
3654
3774
  if (current.interrupts >= 2) {
3655
3775
  process.exit(130);
3656
3776
  }
@@ -3688,6 +3808,8 @@ function App({
3688
3808
  });
3689
3809
  void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3690
3810
  }
3811
+ const killed = getProcessRegistry().killAll();
3812
+ const procTag = killed.length > 0 ? ` + killed ${killed.length} process${killed.length === 1 ? "" : "es"}` : "";
3691
3813
  const droppedCount = stateRef.current.queue.length;
3692
3814
  if (droppedCount > 0) {
3693
3815
  dispatch({ type: "queueClear" });
@@ -3695,7 +3817,7 @@ function App({
3695
3817
  type: "addEntry",
3696
3818
  entry: {
3697
3819
  kind: "warn",
3698
- text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
3820
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}${procTag}. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
3699
3821
  }
3700
3822
  });
3701
3823
  } else {
@@ -3703,14 +3825,16 @@ function App({
3703
3825
  type: "addEntry",
3704
3826
  entry: {
3705
3827
  kind: "warn",
3706
- text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Press Ctrl+C again to exit.`
3828
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}${procTag}. Press Ctrl+C again to exit.`
3707
3829
  }
3708
3830
  });
3709
3831
  }
3710
3832
  } else {
3833
+ const killed = getProcessRegistry().killAll();
3834
+ const procTag = killed.length > 0 ? ` Killed ${killed.length} process${killed.length === 1 ? "" : "es"}.` : "";
3711
3835
  dispatch({
3712
3836
  type: "addEntry",
3713
- entry: { kind: "warn", text: "Press Ctrl+C again to exit." }
3837
+ entry: { kind: "warn", text: `Press Ctrl+C again to exit.${procTag}` }
3714
3838
  });
3715
3839
  }
3716
3840
  };
@@ -4325,7 +4449,8 @@ User message:
4325
4449
  git: gitInfo,
4326
4450
  context: contextWindow,
4327
4451
  projectName,
4328
- subagentCount: Object.keys(state.fleet).length
4452
+ subagentCount: Object.keys(state.fleet).length,
4453
+ processCount: getProcessRegistry().activeCount
4329
4454
  }
4330
4455
  ),
4331
4456
  director ? /* @__PURE__ */ jsx(FleetPanel, { entries: state.fleet, totalCost: state.fleetCost, roster: fleetRoster }) : null