@slopus/happy-terminal 0.3.2 → 0.3.3

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
@@ -889,6 +889,9 @@ function delay2(milliseconds) {
889
889
  }
890
890
 
891
891
  // sources/daemon/runDaemonCommand.ts
892
+ import {
893
+ HappyAgentApiError
894
+ } from "@slopus/happy-agent-client";
892
895
  import { createRootContext as createRootContext2 } from "@steve.kite/stdlib";
893
896
 
894
897
  // sources/processes/killProcessTree.ts
@@ -1806,7 +1809,7 @@ async function delay3(ms, signal) {
1806
1809
  import { resolve } from "node:path";
1807
1810
  import { setTimeout as sleep } from "node:timers/promises";
1808
1811
  import {
1809
- HappyAgentApiError
1812
+ HappyAgentApiError as HappyAgentApiError2
1810
1813
  } from "@slopus/happy-agent-client";
1811
1814
  var WORKSPACE_INITIALIZATION_TIMEOUT_MS = 12e4;
1812
1815
  var WORKSPACE_INITIALIZATION_POLL_MS = 50;
@@ -1876,7 +1879,7 @@ async function waitForWorkspaceReady(client, workspaceId) {
1876
1879
  try {
1877
1880
  return (await client.getWorkspace(workspaceId)).workspace;
1878
1881
  } catch (error) {
1879
- if (!(error instanceof HappyAgentApiError) || error.code !== "not_initialized") {
1882
+ if (!(error instanceof HappyAgentApiError2) || error.code !== "not_initialized") {
1880
1883
  throw error;
1881
1884
  }
1882
1885
  if (Date.now() >= deadline) {
package/dist/main.js CHANGED
@@ -1799,6 +1799,9 @@ function delay2(milliseconds) {
1799
1799
  // sources/daemon/runDaemonCommand.ts
1800
1800
  init_HappyTerminalUserError();
1801
1801
  import { lstat as lstat2 } from "node:fs/promises";
1802
+ import {
1803
+ HappyAgentApiError
1804
+ } from "@slopus/happy-agent-client";
1802
1805
  import { createRootContext as createRootContext2 } from "@steve.kite/stdlib";
1803
1806
 
1804
1807
  // sources/processes/killProcessTree.ts
@@ -2768,7 +2771,7 @@ function errorCode(error) {
2768
2771
 
2769
2772
  // sources/daemon/runDaemonCommand.ts
2770
2773
  var DAEMON_SHUTDOWN_TIMEOUT_MS = 3e4;
2771
- var DAEMON_RELOAD_GRACE_TIMEOUT_MS = 5e3;
2774
+ var DRAIN_POLL_INTERVAL_MS = 100;
2772
2775
  async function runDaemonCommand(command, log = console.log, ctx = createRootContext2().named("daemon-command")) {
2773
2776
  const paths = getHappyDaemonPaths();
2774
2777
  if (command === "start") {
@@ -2791,10 +2794,7 @@ async function runDaemonCommand(command, log = console.log, ctx = createRootCont
2791
2794
  const observed = await observeLocalProtocolServer(paths);
2792
2795
  if (command === "reload") {
2793
2796
  if (observed !== void 0) {
2794
- await stopLocalProtocolServer(ctx, observed.client, paths, {
2795
- forceAfterMs: DAEMON_RELOAD_GRACE_TIMEOUT_MS,
2796
- onForce: log
2797
- });
2797
+ await stopLocalProtocolServer(ctx, observed.client, paths, log);
2798
2798
  } else await assertNoUnresponsiveDaemon(paths.pidPath);
2799
2799
  const connection = await ensureLocalProtocolServer();
2800
2800
  log(`Daemon is running at ${connection.paths.socketPath}`);
@@ -2812,8 +2812,13 @@ async function runDaemonCommand(command, log = console.log, ctx = createRootCont
2812
2812
  log("Daemon is not running.");
2813
2813
  }
2814
2814
  } else if (observed.health.ready) {
2815
- log(`Daemon is running at ${paths.socketPath}`);
2815
+ log(
2816
+ observed.health.draining === true ? `Daemon is draining at ${paths.socketPath}` : `Daemon is running at ${paths.socketPath}`
2817
+ );
2816
2818
  log(`Daemon PID: ${String(pid ?? "unknown")}`);
2819
+ if (observed.health.draining === true) {
2820
+ log(formatDrainProgress(observed.health.drainWaitingFor ?? []));
2821
+ }
2817
2822
  } else {
2818
2823
  log(`Daemon is starting at ${paths.socketPath}`);
2819
2824
  log(`Daemon PID: ${String(pid ?? "unknown")}`);
@@ -2828,28 +2833,25 @@ async function runDaemonCommand(command, log = console.log, ctx = createRootCont
2828
2833
  return;
2829
2834
  }
2830
2835
  log("Daemon is stopping.");
2831
- await stopLocalProtocolServer(ctx, observed.client, paths);
2836
+ await stopLocalProtocolServer(ctx, observed.client, paths, log);
2832
2837
  log("Daemon stopped.");
2833
2838
  }
2834
- async function stopLocalProtocolServer(ctx, client, paths, options = {}) {
2839
+ async function stopLocalProtocolServer(ctx, client, paths, report) {
2835
2840
  let pid = await readDaemonPid(paths.pidPath);
2841
+ await drainLocalProtocolServer(client, report);
2836
2842
  let requestError;
2837
2843
  try {
2838
2844
  pid = (await client.shutdown()).pid;
2839
2845
  } catch (error) {
2840
2846
  requestError = error;
2841
2847
  }
2842
- const timeoutMs = options.forceAfterMs ?? DAEMON_SHUTDOWN_TIMEOUT_MS;
2843
- const stopped = await waitForDaemonShutdown(ctx, paths.socketPath, pid, timeoutMs);
2848
+ const stopped = await waitForDaemonShutdown(
2849
+ ctx,
2850
+ paths.socketPath,
2851
+ pid,
2852
+ DAEMON_SHUTDOWN_TIMEOUT_MS
2853
+ );
2844
2854
  if (stopped.socketRemoved && stopped.processExited) return;
2845
- if (options.forceAfterMs !== void 0 && pid !== void 0) {
2846
- options.onForce?.(
2847
- `Daemon did not stop gracefully; forcing process ${String(pid)} to exit.`
2848
- );
2849
- if (!stopped.processExited) await killDaemonProcess(ctx, pid);
2850
- await removeDaemonPid(paths.pidPath, pid);
2851
- return;
2852
- }
2853
2855
  if (requestError !== void 0) {
2854
2856
  throw new Error(
2855
2857
  `Could not stop the existing local daemon: ${requestError instanceof Error ? requestError.message : String(requestError)}`
@@ -2864,6 +2866,45 @@ async function stopLocalProtocolServer(ctx, client, paths, options = {}) {
2864
2866
  );
2865
2867
  }
2866
2868
  }
2869
+ async function drainLocalProtocolServer(client, report) {
2870
+ try {
2871
+ await client.drain();
2872
+ } catch (error) {
2873
+ if (error instanceof HappyAgentApiError && (error.status === 403 || error.status === 404)) {
2874
+ report("This daemon does not support draining; stopping it directly.");
2875
+ return;
2876
+ }
2877
+ throw error;
2878
+ }
2879
+ let previous;
2880
+ for (; ; ) {
2881
+ const health = await client.getHealth();
2882
+ const waiting = health.drainWaitingFor ?? [];
2883
+ const current = JSON.stringify(waiting);
2884
+ if (current !== previous) {
2885
+ report(formatDrainProgress(waiting));
2886
+ previous = current;
2887
+ }
2888
+ if (waiting.length === 0) return;
2889
+ await new Promise((resolve4) => setTimeout(resolve4, DRAIN_POLL_INTERVAL_MS));
2890
+ }
2891
+ }
2892
+ function formatDrainProgress(waiting) {
2893
+ if (waiting.length === 0) return "Daemon drain is complete.";
2894
+ return `Draining: ${waiting.map(formatDrainWait).join("; ")}.`;
2895
+ }
2896
+ function formatDrainWait(wait) {
2897
+ const noun = wait.name === "api-mutations" ? plural(wait.count, "API mutation") : wait.name === "agent-system" ? plural(wait.count, "agent") : wait.name === "auto-agent-system" ? plural(wait.count, "permission reviewer") : `${String(wait.count)} ${wait.name.replaceAll("-", " ")}`;
2898
+ if (wait.agents === void 0 || wait.agents.length === 0) return noun;
2899
+ const agents = wait.agents.map((agent) => `${agent.id}: ${stageLabel(agent.stage)}`).join(", ");
2900
+ return `${noun} (${agents}${wait.truncated === true ? ", \u2026" : ""})`;
2901
+ }
2902
+ function stageLabel(stage) {
2903
+ return stage === "tools" ? "tool calls" : stage;
2904
+ }
2905
+ function plural(count2, noun) {
2906
+ return `${String(count2)} ${noun}${count2 === 1 ? "" : "s"}`;
2907
+ }
2867
2908
  async function waitForDaemonShutdown(ctx, socketPath, pid, timeoutMs) {
2868
2909
  const [socketRemoved, processExited] = await Promise.all([
2869
2910
  waitForSocketRemoval(socketPath, timeoutMs),
@@ -3017,7 +3058,7 @@ async function delay4(ms, signal) {
3017
3058
  import { resolve } from "node:path";
3018
3059
  import { setTimeout as sleep } from "node:timers/promises";
3019
3060
  import {
3020
- HappyAgentApiError
3061
+ HappyAgentApiError as HappyAgentApiError2
3021
3062
  } from "@slopus/happy-agent-client";
3022
3063
  var WORKSPACE_INITIALIZATION_TIMEOUT_MS = 12e4;
3023
3064
  var WORKSPACE_INITIALIZATION_POLL_MS = 50;
@@ -3087,7 +3128,7 @@ async function waitForWorkspaceReady(client, workspaceId) {
3087
3128
  try {
3088
3129
  return (await client.getWorkspace(workspaceId)).workspace;
3089
3130
  } catch (error) {
3090
- if (!(error instanceof HappyAgentApiError) || error.code !== "not_initialized") {
3131
+ if (!(error instanceof HappyAgentApiError2) || error.code !== "not_initialized") {
3091
3132
  throw error;
3092
3133
  }
3093
3134
  if (Date.now() >= deadline) {
@@ -9937,8 +9978,8 @@ var RESET13 = "\x1B[0m";
9937
9978
  var DIM10 = "\x1B[2m";
9938
9979
  function renderBackgroundTerminalSummary(count2, width) {
9939
9980
  if (count2 === 0) return void 0;
9940
- const plural = count2 === 1 ? "" : "s";
9941
- const summary = ` ${String(count2)} background terminal${plural} running \xB7 /ps to view \xB7 /stop to close`;
9981
+ const plural2 = count2 === 1 ? "" : "s";
9982
+ const summary = ` ${String(count2)} background terminal${plural2} running \xB7 /ps to view \xB7 /stop to close`;
9942
9983
  return truncateToWidth11(`${DIM10}${summary}${RESET13}`, Math.max(1, width), "", true);
9943
9984
  }
9944
9985
 
@@ -11189,8 +11230,8 @@ var RESET21 = "\x1B[0m";
11189
11230
  var DIM18 = "\x1B[2m";
11190
11231
  function renderSubagentSummary(options) {
11191
11232
  if (options.count === 0) return void 0;
11192
- const plural = options.count === 1 ? "" : "s";
11193
- const summary = ` ${String(options.count)} agent${plural} running \xB7 /agents to view \xB7 ${formatActivityElapsedTime(options.elapsedMs)} \xB7 ${formatCompactTokens(options.totalTokens)} context tokens`;
11233
+ const plural2 = options.count === 1 ? "" : "s";
11234
+ const summary = ` ${String(options.count)} agent${plural2} running \xB7 /agents to view \xB7 ${formatActivityElapsedTime(options.elapsedMs)} \xB7 ${formatCompactTokens(options.totalTokens)} context tokens`;
11194
11235
  return truncateToWidth23(`${DIM18}${summary}${RESET21}`, Math.max(1, options.width), "", true);
11195
11236
  }
11196
11237
 
@@ -11213,8 +11254,8 @@ var RESET23 = "\x1B[0m";
11213
11254
  var DIM20 = "\x1B[2m";
11214
11255
  function renderWorkflowSummary(count2, width) {
11215
11256
  if (count2 === 0) return void 0;
11216
- const plural = count2 === 1 ? "" : "s";
11217
- const summary = ` ${String(count2)} workflow${plural} running \xB7 /workflows to view`;
11257
+ const plural2 = count2 === 1 ? "" : "s";
11258
+ const summary = ` ${String(count2)} workflow${plural2} running \xB7 /workflows to view`;
11218
11259
  return truncateToWidth25(`${DIM20}${summary}${RESET23}`, Math.max(1, width), "", true);
11219
11260
  }
11220
11261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slopus/happy-terminal",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Happy Terminal, the embeddable terminal client for Happy Agent.",
5
5
  "keywords": [
6
6
  "coding-agent",
@@ -63,7 +63,7 @@
63
63
  "@sinclair/typebox": "^0.34.49",
64
64
  "@slopus/ghostty-wasm": "1.3.6",
65
65
  "@slopus/ghostty-web": "0.1.3",
66
- "@slopus/happy-agent-client": "0.0.17",
66
+ "@slopus/happy-agent-client": "0.0.20",
67
67
  "@slopus/happy-providers": "0.0.11",
68
68
  "@slopus/murmur": "0.4.4",
69
69
  "@steve.kite/stdlib": "0.0.12",