@tridha643/hestia 1.1.0 → 1.2.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/cli.js CHANGED
@@ -11249,6 +11249,87 @@ var init_ingress = __esm(() => {
11249
11249
  init_local_http_router();
11250
11250
  });
11251
11251
 
11252
+ // packages/engine/src/tunnel/orphans.ts
11253
+ import { execFileSync as execFileSync4 } from "child_process";
11254
+ import { join as join21 } from "path";
11255
+ function hestiaTunnelMarker(uuid) {
11256
+ return join21(hestiaHome(), "tunnel", uuid);
11257
+ }
11258
+ function parseLocalHestiaConnectors(psOutput, marker) {
11259
+ const rows = [];
11260
+ for (const line of psOutput.split(`
11261
+ `)) {
11262
+ const trimmed = line.trim();
11263
+ if (trimmed === "")
11264
+ continue;
11265
+ const m = trimmed.match(/^(\d+)\s+(\d+)\s+(.*)$/);
11266
+ if (m === null)
11267
+ continue;
11268
+ const command = m[3];
11269
+ if (!command.includes(marker))
11270
+ continue;
11271
+ if (!/(^|[\/\s])cloudflared(\s|$)/.test(command))
11272
+ continue;
11273
+ rows.push({ pid: Number(m[1]), pgid: Number(m[2]), command });
11274
+ }
11275
+ return rows;
11276
+ }
11277
+ function listLocalHestiaConnectors(uuid) {
11278
+ let out;
11279
+ try {
11280
+ out = execFileSync4("ps", ["-Ao", "pid=,pgid=,command="], {
11281
+ encoding: "utf8",
11282
+ maxBuffer: 16 * 1024 * 1024,
11283
+ env: { ...process.env, LC_ALL: "C", LANG: "C" }
11284
+ });
11285
+ } catch {
11286
+ return [];
11287
+ }
11288
+ return parseLocalHestiaConnectors(out, hestiaTunnelMarker(uuid));
11289
+ }
11290
+ async function reapOrphanConnectors(uuid, keep) {
11291
+ const procs = listLocalHestiaConnectors(uuid).filter((p) => {
11292
+ if (keep === undefined)
11293
+ return true;
11294
+ return p.pgid !== keep.pgid && p.pid !== keep.pid;
11295
+ });
11296
+ if (procs.length === 0)
11297
+ return { reapedGroups: 0, pids: [] };
11298
+ const groups = new Set(procs.map((p) => p.pgid));
11299
+ const pids = procs.map((p) => p.pid);
11300
+ for (const pgid of groups) {
11301
+ try {
11302
+ process.kill(-pgid, "SIGTERM");
11303
+ } catch {}
11304
+ }
11305
+ const deadline = Date.now() + REAP_GRACE_MS;
11306
+ while (Date.now() < deadline) {
11307
+ const still = listLocalHestiaConnectors(uuid).filter((p) => {
11308
+ if (keep === undefined)
11309
+ return true;
11310
+ return p.pgid !== keep.pgid && p.pid !== keep.pid;
11311
+ });
11312
+ if (still.length === 0)
11313
+ return { reapedGroups: groups.size, pids };
11314
+ await new Promise((r) => setTimeout(r, REAP_POLL_MS));
11315
+ }
11316
+ for (const p of listLocalHestiaConnectors(uuid)) {
11317
+ if (keep !== undefined && (p.pgid === keep.pgid || p.pid === keep.pid))
11318
+ continue;
11319
+ try {
11320
+ process.kill(-p.pgid, "SIGKILL");
11321
+ } catch {}
11322
+ try {
11323
+ process.kill(p.pid, "SIGKILL");
11324
+ } catch {}
11325
+ }
11326
+ return { reapedGroups: groups.size, pids };
11327
+ }
11328
+ var REAP_GRACE_MS = 5000, REAP_POLL_MS = 100;
11329
+ var init_orphans = __esm(() => {
11330
+ init_state();
11331
+ });
11332
+
11252
11333
  // packages/engine/src/tunnel/verify.ts
11253
11334
  async function get2(port, path) {
11254
11335
  try {
@@ -11298,15 +11379,15 @@ import {
11298
11379
  readFileSync as readFileSync17,
11299
11380
  readdirSync as readdirSync7
11300
11381
  } from "fs";
11301
- import { join as join21 } from "path";
11382
+ import { join as join22 } from "path";
11302
11383
  function tunnelDir(uuid) {
11303
- return join21(hestiaHome(), "tunnel", uuid);
11384
+ return join22(hestiaHome(), "tunnel", uuid);
11304
11385
  }
11305
11386
  function configPath(uuid) {
11306
- return join21(tunnelDir(uuid), "config.yml");
11387
+ return join22(tunnelDir(uuid), "config.yml");
11307
11388
  }
11308
11389
  function adoptedMarker(uuid) {
11309
- return join21(tunnelDir(uuid), "adopted.json");
11390
+ return join22(tunnelDir(uuid), "adopted.json");
11310
11391
  }
11311
11392
  function isAdopted(uuid) {
11312
11393
  return existsSync19(adoptedMarker(uuid));
@@ -11325,10 +11406,10 @@ function readAdopted(uuid) {
11325
11406
  return { uuid, name: marker.name, credFile: marker.credFile, reconstructed: false };
11326
11407
  }
11327
11408
  let name;
11328
- const stacksDir = join21(hestiaHome(), "stacks");
11409
+ const stacksDir = join22(hestiaHome(), "stacks");
11329
11410
  if (existsSync19(stacksDir)) {
11330
11411
  for (const project of readdirSync7(stacksDir)) {
11331
- const sp = join21(stacksDir, project, "stack.json");
11412
+ const sp = join22(stacksDir, project, "stack.json");
11332
11413
  if (!existsSync19(sp))
11333
11414
  continue;
11334
11415
  try {
@@ -11348,19 +11429,19 @@ function readAdopted(uuid) {
11348
11429
  };
11349
11430
  }
11350
11431
  function listAdopted() {
11351
- const root = join21(hestiaHome(), "tunnel");
11432
+ const root = join22(hestiaHome(), "tunnel");
11352
11433
  if (!existsSync19(root))
11353
11434
  return [];
11354
11435
  return readdirSync7(root).filter((uuid) => isAdopted(uuid));
11355
11436
  }
11356
11437
  function collectDynamicRules(uuid) {
11357
- const stacksDir = join21(hestiaHome(), "stacks");
11438
+ const stacksDir = join22(hestiaHome(), "stacks");
11358
11439
  const rules = [];
11359
11440
  const dropped = [];
11360
11441
  if (!existsSync19(stacksDir))
11361
11442
  return { rules, dropped };
11362
11443
  for (const project of readdirSync7(stacksDir)) {
11363
- const p = join21(stacksDir, project, "stack.json");
11444
+ const p = join22(stacksDir, project, "stack.json");
11364
11445
  if (!existsSync19(p))
11365
11446
  continue;
11366
11447
  let record;
@@ -11405,6 +11486,10 @@ async function reconcileTunnel(ref, opts) {
11405
11486
  });
11406
11487
  const pf = readPidfile(dir, CONNECTOR);
11407
11488
  const live = pf !== null && isLive(pf);
11489
+ const { reapedGroups } = await reapOrphanConnectors(ref.uuid, live ? pf : undefined);
11490
+ if (reapedGroups > 0) {
11491
+ warnings.push(`reaped ${reapedGroups} orphan hestia connector process group(s) for this tunnel`);
11492
+ }
11408
11493
  const currentConfig = existsSync19(cfgPath) ? readFileSync17(cfgPath, "utf8") : null;
11409
11494
  if (live && currentConfig === nextConfig) {
11410
11495
  return { restarted: false, metricsPort: pf.port, ready: true, warnings };
@@ -11481,6 +11566,7 @@ var init_registry = __esm(() => {
11481
11566
  init_shutdown();
11482
11567
  init_ingress();
11483
11568
  init_shared();
11569
+ init_orphans();
11484
11570
  init_atomic_json_file();
11485
11571
  });
11486
11572
 
@@ -12142,7 +12228,7 @@ var init_shared_arbiter = __esm(() => {
12142
12228
  // packages/engine/src/doctor.ts
12143
12229
  import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync8 } from "fs";
12144
12230
  import { execFile as execFile9 } from "child_process";
12145
- import { dirname as dirname6, join as join22 } from "path";
12231
+ import { dirname as dirname6, join as join23 } from "path";
12146
12232
  function row(check, level, detail) {
12147
12233
  return { check, level, detail };
12148
12234
  }
@@ -12183,8 +12269,8 @@ async function envChecks(ctx) {
12183
12269
  }
12184
12270
  if (ctx !== null) {
12185
12271
  const ignored = await exec("git", ["-C", ctx.worktreeRoot, "check-ignore", "-q", ".hestia/"]);
12186
- rows.push(ignored.ok ? row("state-ignore", "ok", ".hestia/ is ignored") : row("state-ignore", "error", `add the exact line ".hestia/" to ${join22(ctx.worktreeRoot, ".gitignore")}`));
12187
- const schema = existsSync20(join22(ctx.worktreeRoot, ".env.schema"));
12272
+ rows.push(ignored.ok ? row("state-ignore", "ok", ".hestia/ is ignored") : row("state-ignore", "error", `add the exact line ".hestia/" to ${join23(ctx.worktreeRoot, ".gitignore")}`));
12273
+ const schema = existsSync20(join23(ctx.worktreeRoot, ".env.schema"));
12188
12274
  if (schema) {
12189
12275
  rows.push(detectVarlock(ctx.worktreeRoot) !== null ? row("varlock", "ok", "local varlock found \u2014 worker env composition active") : row("varlock", "warn", ".env.schema present but no local varlock (bun install?)"));
12190
12276
  }
@@ -12193,7 +12279,7 @@ async function envChecks(ctx) {
12193
12279
  const missing = workers.filter((w) => {
12194
12280
  let dir = dirname6(w.configPath);
12195
12281
  for (;; ) {
12196
- if (existsSync20(join22(dir, "node_modules", ".bin", "wrangler")))
12282
+ if (existsSync20(join23(dir, "node_modules", ".bin", "wrangler")))
12197
12283
  return false;
12198
12284
  if (dir === ctx.worktreeRoot || dirname6(dir) === dir)
12199
12285
  return true;
@@ -12252,7 +12338,7 @@ async function stateChecks(ctx) {
12252
12338
  rows.push(row(`exposure:${exp.service}`, "error", `ingress rule points at port ${exp.originPort} but the service owns ` + `${publishedPort} \u2014 a recycled port serves ANOTHER worktree; ` + `run any hestia command to resync, this should not persist`));
12253
12339
  }
12254
12340
  }
12255
- const lockPath2 = join22(hestiaDir(worktreeRoot), "lock");
12341
+ const lockPath2 = join23(hestiaDir(worktreeRoot), "lock");
12256
12342
  if (existsSync20(lockPath2)) {
12257
12343
  try {
12258
12344
  const holder = JSON.parse(readFileSync18(lockPath2, "utf8"));
@@ -12263,7 +12349,7 @@ async function stateChecks(ctx) {
12263
12349
  rows.push(row("lock", "warn", "unreadable worktree lock file"));
12264
12350
  }
12265
12351
  }
12266
- const mirror = join22(hestiaHome(), "stacks", record.project, "stack.json");
12352
+ const mirror = join23(hestiaHome(), "stacks", record.project, "stack.json");
12267
12353
  if (!existsSync20(mirror)) {
12268
12354
  rows.push(row("mirror", "warn", "no ~/.hestia mirror \u2014 `down --project` would not work after worktree deletion"));
12269
12355
  }
@@ -12271,11 +12357,11 @@ async function stateChecks(ctx) {
12271
12357
  }
12272
12358
  async function machineChecks() {
12273
12359
  const rows = [];
12274
- const stacksDir = join22(hestiaHome(), "stacks");
12360
+ const stacksDir = join23(hestiaHome(), "stacks");
12275
12361
  const mirrored = new Set;
12276
12362
  if (existsSync20(stacksDir)) {
12277
12363
  for (const project of readdirSync8(stacksDir)) {
12278
- const p = join22(stacksDir, project, "stack.json");
12364
+ const p = join23(stacksDir, project, "stack.json");
12279
12365
  if (!existsSync20(p))
12280
12366
  continue;
12281
12367
  mirrored.add(project);
@@ -12328,12 +12414,18 @@ async function tunnelChecks() {
12328
12414
  } catch {
12329
12415
  connectors = null;
12330
12416
  }
12417
+ const local = listLocalHestiaConnectors(uuid);
12418
+ const localOrphans = live && pf !== null ? local.filter((p) => p.pgid !== pf.pgid && p.pid !== pf.pid) : local;
12419
+ if (localOrphans.length > 0) {
12420
+ rows.push(row(`${label}:local-orphans`, "error", `${localOrphans.length} untracked hestia connector(s) still running ` + `(pids ${localOrphans.map((p) => p.pid).join(", ")}) \u2014 the daemon sweep ` + `or next hestia command reaps them; do not start another cloudflared`));
12421
+ }
12331
12422
  if (connectors === null) {
12332
12423
  rows.push(row(`${label}:connectors`, "unknown", "cloudflare unreachable \u2014 cannot count connectors"));
12333
12424
  } else {
12334
12425
  const expected = live ? 1 : 0;
12335
12426
  if (connectors > expected) {
12336
- rows.push(row(`${label}:connectors`, "error", `${connectors} connector(s) registered but hestia runs ${expected} \u2014 a foreign ` + `replica cross-wires worktrees; stop the other cloudflared`));
12427
+ const detail = localOrphans.length > 0 ? `${connectors} connector(s) registered but hestia runs ${expected} \u2014 ` + `${localOrphans.length} are local hestia-owned orphans (auto-reaped on next sweep); ` + `any remainder is a foreign replica` : `${connectors} connector(s) registered but hestia runs ${expected} \u2014 a foreign ` + `replica cross-wires worktrees; stop the other cloudflared (only processes whose ` + `argv lacks ~/.hestia/tunnel/<uuid>/ are truly foreign)`;
12428
+ rows.push(row(`${label}:connectors`, "error", detail));
12337
12429
  }
12338
12430
  }
12339
12431
  }
@@ -12435,6 +12527,7 @@ var init_doctor = __esm(() => {
12435
12527
  init_discover();
12436
12528
  init_registry();
12437
12529
  init_cloudflared();
12530
+ init_orphans();
12438
12531
  init_client();
12439
12532
  init_routes();
12440
12533
  init_launchd();
@@ -12447,7 +12540,7 @@ var init_doctor = __esm(() => {
12447
12540
  import { execFile as execFile10 } from "child_process";
12448
12541
  import { promisify as promisify8 } from "util";
12449
12542
  import { existsSync as existsSync21, readFileSync as readFileSync19, readdirSync as readdirSync9 } from "fs";
12450
- import { join as join23 } from "path";
12543
+ import { join as join24 } from "path";
12451
12544
 
12452
12545
  class LatestFleetChannel {
12453
12546
  #pending;
@@ -12490,11 +12583,11 @@ class LatestFleetChannel {
12490
12583
  function readManagedMirrors() {
12491
12584
  const records = [];
12492
12585
  const warnings = [];
12493
- const stacksDirectory = join23(hestiaHome(), "stacks");
12586
+ const stacksDirectory = join24(hestiaHome(), "stacks");
12494
12587
  if (!existsSync21(stacksDirectory))
12495
12588
  return { records, warnings };
12496
12589
  for (const project of readdirSync9(stacksDirectory).sort()) {
12497
- const path = join23(stacksDirectory, project, "stack.json");
12590
+ const path = join24(stacksDirectory, project, "stack.json");
12498
12591
  try {
12499
12592
  const source = readFileSync19(path);
12500
12593
  if (source.byteLength > MAX_MIRROR_BYTES2) {
@@ -12982,7 +13075,7 @@ var init_init_config = __esm(() => {
12982
13075
  import { existsSync as existsSync22, rmSync as rmSync10 } from "fs";
12983
13076
  import { execFile as execFile11 } from "child_process";
12984
13077
  import { promisify as promisify9 } from "util";
12985
- import { join as join24, resolve as resolve2 } from "path";
13078
+ import { join as join25, resolve as resolve2 } from "path";
12986
13079
  import { createHash as createHash7 } from "crypto";
12987
13080
  import { resolve4, resolve6, resolveCname } from "dns/promises";
12988
13081
  function composeUnsupported(message) {
@@ -13092,7 +13185,7 @@ async function assertHestiaStateIgnored(worktreeRoot) {
13092
13185
  try {
13093
13186
  await pexec9("git", ["-C", worktreeRoot, "check-ignore", "-q", ".hestia/"], { timeout: 5000 });
13094
13187
  } catch {
13095
- throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${join24(worktreeRoot, ".gitignore")}`, { remedy: `.hestia/`, path: join24(worktreeRoot, ".gitignore") });
13188
+ throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${join25(worktreeRoot, ".gitignore")}`, { remedy: `.hestia/`, path: join25(worktreeRoot, ".gitignore") });
13096
13189
  }
13097
13190
  }
13098
13191
  async function prepareCompose(worktreeRoot, project, repo, branch, opts, configuredBaseFile) {
@@ -13109,7 +13202,7 @@ async function prepareCompose(worktreeRoot, project, repo, branch, opts, configu
13109
13202
  services
13110
13203
  });
13111
13204
  ensureDir(hestiaDir(worktreeRoot));
13112
- const overridePath = join24(hestiaDir(worktreeRoot), OVERRIDE_FILE);
13205
+ const overridePath = join25(hestiaDir(worktreeRoot), OVERRIDE_FILE);
13113
13206
  writeAtomicTextFile(overridePath, yaml);
13114
13207
  return {
13115
13208
  ctx: {
@@ -13142,7 +13235,7 @@ function writeDockerfileComposeModel(worktreeRoot, workloads, conventionalCompos
13142
13235
  ...conventionalComposeFile === undefined ? {} : { include: [{ path: conventionalComposeFile }] },
13143
13236
  services
13144
13237
  };
13145
- const path = join24(hestiaDir(worktreeRoot), DOCKERFILE_COMPOSE_FILE);
13238
+ const path = join25(hestiaDir(worktreeRoot), DOCKERFILE_COMPOSE_FILE);
13146
13239
  writeAtomicTextFile(path, $stringify(model));
13147
13240
  return path;
13148
13241
  }
@@ -13162,7 +13255,7 @@ function freshRecord(project, repoId, repo, branch, worktree) {
13162
13255
  };
13163
13256
  }
13164
13257
  function assertCurrentStackIdentity(record, current) {
13165
- const path = join24(hestiaDir(current.worktreeRoot), "stack.json");
13258
+ const path = join25(hestiaDir(current.worktreeRoot), "stack.json");
13166
13259
  assertMutableStackRecord(record, path);
13167
13260
  const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch && resolve2(record.worktree) === resolve2(current.worktreeRoot) && record.project === projectName(current.repoId, current.repo, current.branch, current.worktreeRoot);
13168
13261
  if (!matches) {
@@ -13341,7 +13434,7 @@ function matchesExpectedStack(record, expected) {
13341
13434
  return record !== null && (record.repoId === undefined || record.repoId === expected.repoId) && record.worktree === expected.worktree && record.createdAt === expected.createdAt;
13342
13435
  }
13343
13436
  function projectMutationRoot(project) {
13344
- return join24(hestiaHome(), "project-locks", project);
13437
+ return join25(hestiaHome(), "project-locks", project);
13345
13438
  }
13346
13439
  async function assertNamedTunnelDns(hostname, tunnelUuid) {
13347
13440
  const expected = `${tunnelUuid}.cfargotunnel.com`;
@@ -13948,7 +14041,7 @@ class ComposeEngine {
13948
14041
  const composeFile = record?.composeFile ?? tryLoadConfig(worktreeRoot)?.composeFile;
13949
14042
  if (composeFile !== undefined) {
13950
14043
  const project2 = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
13951
- const overrideFile = record?.overrideFile ?? join24(hestiaDir(worktreeRoot), OVERRIDE_FILE);
14044
+ const overrideFile = record?.overrideFile ?? join25(hestiaDir(worktreeRoot), OVERRIDE_FILE);
13952
14045
  if (existsSync22(overrideFile)) {
13953
14046
  await composeDown({ project: project2, baseFile: composeFile, overrideFile, cwd: worktreeRoot }, opts?.destroy ?? false);
13954
14047
  } else if (record?.composeFile !== undefined) {
@@ -14181,7 +14274,7 @@ class ComposeEngine {
14181
14274
  const alias = selection.endpoint.alias ?? selection.endpoint.name;
14182
14275
  const authority = internalEndpointAuthority(record.project, alias);
14183
14276
  const name = `aux-quick-${createHash7("sha256").update(alias).digest("hex").slice(0, 10)}`;
14184
- const quickCfg = join24(hestiaDir(worktreeRoot), `quick-${name}.yml`);
14277
+ const quickCfg = join25(hestiaDir(worktreeRoot), `quick-${name}.yml`);
14185
14278
  writeAtomicTextFile(quickCfg, `ingress:
14186
14279
  - service: ${JSON.stringify(`unix:${publicGatewaySocketPath()}`)}
14187
14280
  ` + ` originRequest:
@@ -14802,6 +14895,14 @@ var init_fleet_source = __esm(() => {
14802
14895
  init_src2();
14803
14896
  });
14804
14897
 
14898
+ // packages/tui/src/fleet-endpoints.ts
14899
+ function serviceEndpoints(service) {
14900
+ return service.endpoints ?? (service.endpoint === undefined ? [] : [service.endpoint]);
14901
+ }
14902
+ function endpointReach(endpoint) {
14903
+ return endpoint.localUrl ?? endpoint.publicUrl ?? endpoint.url ?? `${endpoint.host}:${endpoint.port}`;
14904
+ }
14905
+
14805
14906
  // packages/tui/src/fleet-controller.ts
14806
14907
  function createFleetUiState() {
14807
14908
  return {
@@ -14839,7 +14940,7 @@ function reconcileFleetSelection(previous, snapshot, preferredProject) {
14839
14940
  const previousIndex = snapshot.stacks.findIndex((stack2) => stack2.project === previous.project);
14840
14941
  const stack = snapshot.stacks.find((candidate) => candidate.project === previous.project) ?? snapshot.stacks.find((candidate) => candidate.project === preferredProject) ?? nearestItem(snapshot.stacks, previousIndex < 0 ? 0 : previousIndex) ?? snapshot.stacks[0];
14841
14942
  const service = stack.services.find((candidate) => candidate.name === previous.service) ?? stack.services[0];
14842
- const endpoints = service?.endpoints ?? (service?.endpoint === undefined ? [] : [service.endpoint]);
14943
+ const endpoints = service === undefined ? [] : serviceEndpoints(service);
14843
14944
  const endpoint = endpoints.find((candidate) => candidate.name === previous.endpoint);
14844
14945
  return { project: stack.project, service: service?.name, endpoint: endpoint?.name };
14845
14946
  }
@@ -14927,7 +15028,7 @@ function reduceFleetUiState(state, action) {
14927
15028
  case "select-endpoint": {
14928
15029
  const stack = action.snapshot.stacks.find((candidate) => candidate.project === state.selection.project);
14929
15030
  const service = stack?.services.find((candidate) => candidate.name === action.service);
14930
- if (!service?.endpoints?.some((endpoint) => endpoint.name === action.endpoint))
15031
+ if (service === undefined || !serviceEndpoints(service).some((endpoint) => endpoint.name === action.endpoint))
14931
15032
  return state;
14932
15033
  return {
14933
15034
  ...state,
@@ -14959,12 +15060,10 @@ function reduceFleetUiState(state, action) {
14959
15060
  logOffset: action.follow ? 0 : state.logOffset,
14960
15061
  unseenLines: action.follow ? 0 : state.unseenLines
14961
15062
  };
14962
- case "scroll-logs":
14963
- return {
14964
- ...state,
14965
- follow: action.delta > 0 ? state.follow : false,
14966
- logOffset: Math.max(0, state.logOffset - action.delta)
14967
- };
15063
+ case "scroll-logs": {
15064
+ const offset = Math.min(action.maxOffset ?? Number.MAX_SAFE_INTEGER, Math.max(0, state.logOffset - action.delta));
15065
+ return action.delta > 0 && offset === 0 ? { ...state, follow: true, logOffset: 0, unseenLines: 0 } : { ...state, follow: action.delta > 0 ? state.follow : false, logOffset: offset };
15066
+ }
14968
15067
  case "new-lines":
14969
15068
  return state.follow ? state : {
14970
15069
  ...state,
@@ -14992,11 +15091,24 @@ function reduceFleetUiState(state, action) {
14992
15091
  return { ...state, downPending: action.pending };
14993
15092
  }
14994
15093
  }
15094
+ var init_fleet_controller = () => {};
14995
15095
 
14996
15096
  // packages/tui/src/terminal-text.ts
15097
+ function collapseCarriageReturns(text) {
15098
+ if (!text.includes("\r"))
15099
+ return text;
15100
+ return text.split(`
15101
+ `).map((line) => {
15102
+ const parts = line.replace(/\r+$/, "").split("\r");
15103
+ return parts[parts.length - 1] ?? "";
15104
+ }).join(`
15105
+ `);
15106
+ }
14997
15107
  function sanitizeFleetTerminalText(text, preserveNewlines = false) {
15108
+ const withoutEscapes = text.replace(ESCAPE_CONTROL_SEQUENCE, "").replace(C1_CONTROL_SEQUENCE, "");
15109
+ const collapsed = collapseCarriageReturns(withoutEscapes);
14998
15110
  const controls = preserveNewlines ? /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g : /[\x00-\x1f\x7f-\x9f]/g;
14999
- return text.replace(ESCAPE_CONTROL_SEQUENCE, "").replace(C1_CONTROL_SEQUENCE, "").replace(controls, "");
15111
+ return collapsed.replace(controls, "");
15000
15112
  }
15001
15113
  var ESCAPE_CONTROL_SEQUENCE, C1_CONTROL_SEQUENCE;
15002
15114
  var init_terminal_text = __esm(() => {
@@ -15004,24 +15116,6 @@ var init_terminal_text = __esm(() => {
15004
15116
  C1_CONTROL_SEQUENCE = /(?:[\x90\x98\x9d\x9e\x9f][\s\S]*?(?:\x07|\x9c)|\x9b[0-?]*[ -/]*[@-~])/g;
15005
15117
  });
15006
15118
 
15007
- // packages/tui/src/fleet-theme.ts
15008
- var fleetTheme;
15009
- var init_fleet_theme = __esm(() => {
15010
- fleetTheme = {
15011
- background: "#0d1117",
15012
- panel: "#161b22",
15013
- panelAlt: "#21262d",
15014
- border: "#30363d",
15015
- text: "#c9d1d9",
15016
- muted: "#8b949e",
15017
- accent: "#58a6ff",
15018
- healthy: "#3fb950",
15019
- warning: "#d29922",
15020
- danger: "#f85149",
15021
- selected: "#1f6feb"
15022
- };
15023
- });
15024
-
15025
15119
  // node_modules/.bun/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
15026
15120
  function ansiRegex({ onlyFirst = false } = {}) {
15027
15121
  const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
@@ -15204,229 +15298,623 @@ function padFleetText(text, width) {
15204
15298
  const fitted = fitFleetText(text, width);
15205
15299
  return fitted + " ".repeat(Math.max(0, width - stringWidth(fitted)));
15206
15300
  }
15301
+ function wrapFleetText(text, width, maxLines = 3) {
15302
+ if (width <= 0)
15303
+ return [];
15304
+ if (text === "")
15305
+ return [""];
15306
+ if (stringWidth(text) <= width)
15307
+ return [text];
15308
+ const characters = Array.from(text);
15309
+ const lines = [];
15310
+ let index = 0;
15311
+ while (index < characters.length && lines.length < maxLines) {
15312
+ const last = lines.length === maxLines - 1;
15313
+ if (last) {
15314
+ lines.push(fitFleetText(characters.slice(index).join(""), width));
15315
+ break;
15316
+ }
15317
+ let line = "";
15318
+ let taken = 0;
15319
+ while (index + taken < characters.length) {
15320
+ const next = characters[index + taken];
15321
+ if (stringWidth(line + next) > width)
15322
+ break;
15323
+ line += next;
15324
+ taken += 1;
15325
+ }
15326
+ if (taken === 0) {
15327
+ lines.push(characters[index]);
15328
+ index += 1;
15329
+ continue;
15330
+ }
15331
+ lines.push(line);
15332
+ index += taken;
15333
+ }
15334
+ return lines;
15335
+ }
15207
15336
  var init_fleet_text = __esm(() => {
15208
15337
  init_string_width();
15209
15338
  });
15210
15339
 
15211
- // packages/tui/src/components/StackSidebar.tsx
15212
- import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
15213
- function phaseColor(phase) {
15340
+ // packages/tui/src/fleet-theme.ts
15341
+ function blendHex(fg, bg, amount) {
15342
+ const parse = (hex2) => [
15343
+ Number.parseInt(hex2.slice(1, 3), 16),
15344
+ Number.parseInt(hex2.slice(3, 5), 16),
15345
+ Number.parseInt(hex2.slice(5, 7), 16)
15346
+ ];
15347
+ const [fr, fg2, fb] = parse(fg);
15348
+ const [br, bg2, bb] = parse(bg);
15349
+ const channel = (f, b) => Math.round(b + (f - b) * amount);
15350
+ const hex = (value) => value.toString(16).padStart(2, "0");
15351
+ return `#${hex(channel(fr, br))}${hex(channel(fg2, bg2))}${hex(channel(fb, bb))}`;
15352
+ }
15353
+ function backendColor(backend) {
15354
+ if (backend === "docker")
15355
+ return fleetTheme.backendDocker;
15356
+ if (backend === "tunnel")
15357
+ return fleetTheme.backendTunnel;
15358
+ if (backend === "wrangler" || backend === "proc")
15359
+ return fleetTheme.backendProc;
15360
+ return fleetTheme.muted;
15361
+ }
15362
+ function serviceStateColor(state) {
15363
+ if (state === "healthy")
15364
+ return fleetTheme.healthy;
15365
+ if (state === "unknown" || state === "unhealthy" || state === "starting")
15366
+ return fleetTheme.warning;
15367
+ return fleetTheme.danger;
15368
+ }
15369
+ function serviceStateGlyph(state) {
15370
+ if (state === "healthy")
15371
+ return "\u25CF";
15372
+ if (state === "unhealthy")
15373
+ return "\u25C6";
15374
+ if (state === "starting")
15375
+ return "\u25CC";
15376
+ return "\u25CB";
15377
+ }
15378
+ function stackPhaseColor(phase) {
15214
15379
  if (phase === "up")
15215
15380
  return fleetTheme.healthy;
15216
- if (phase === "degraded" || phase === "unknown")
15381
+ if (phase === "degraded" || phase === "unknown" || phase === "starting")
15217
15382
  return fleetTheme.warning;
15383
+ if (phase === "queued" || phase === "reserved")
15384
+ return fleetTheme.queued;
15218
15385
  if (phase === "stopped")
15219
- return fleetTheme.danger;
15386
+ return fleetTheme.faint;
15220
15387
  return fleetTheme.accent;
15221
15388
  }
15222
- function StackSidebar({
15223
- stacks,
15224
- selectedProject,
15225
- width,
15226
- onSelectProject
15227
- }) {
15228
- return /* @__PURE__ */ jsxDEV("box", {
15229
- style: { width, height: "100%", flexDirection: "column", border: true, borderColor: fleetTheme.border },
15230
- children: [
15231
- /* @__PURE__ */ jsxDEV("box", {
15232
- style: { height: 1, paddingLeft: 1, backgroundColor: fleetTheme.panelAlt },
15233
- children: /* @__PURE__ */ jsxDEV("text", {
15234
- fg: fleetTheme.text,
15235
- children: "Fleet"
15236
- }, undefined, false, undefined, this)
15237
- }, undefined, false, undefined, this),
15238
- stacks.length === 0 ? /* @__PURE__ */ jsxDEV("box", {
15239
- style: { paddingLeft: 1, paddingTop: 1 },
15240
- children: /* @__PURE__ */ jsxDEV("text", {
15241
- fg: fleetTheme.muted,
15242
- children: "No managed stacks"
15243
- }, undefined, false, undefined, this)
15244
- }, undefined, false, undefined, this) : stacks.map((stack) => {
15245
- const selected = stack.project === selectedProject;
15246
- const branch = sanitizeFleetTerminalText(stack.branch);
15247
- return /* @__PURE__ */ jsxDEV("box", {
15248
- onMouseDown: (event) => {
15249
- if (event.button !== 0)
15250
- return;
15251
- event.preventDefault();
15252
- event.stopPropagation();
15253
- onSelectProject?.(stack.project);
15254
- },
15255
- style: {
15256
- height: 1,
15257
- paddingLeft: 1,
15258
- paddingRight: 1,
15259
- flexDirection: "row",
15260
- backgroundColor: selected ? fleetTheme.selected : fleetTheme.background
15261
- },
15262
- children: [
15263
- /* @__PURE__ */ jsxDEV("text", {
15264
- fg: selected ? "#ffffff" : phaseColor(stack.phase),
15265
- children: selected ? "\u25B8 " : " "
15266
- }, undefined, false, undefined, this),
15267
- /* @__PURE__ */ jsxDEV("text", {
15268
- fg: selected ? "#ffffff" : fleetTheme.text,
15269
- children: padFleetText(branch, Math.max(4, width - 13))
15270
- }, undefined, false, undefined, this),
15271
- /* @__PURE__ */ jsxDEV("text", {
15272
- fg: selected ? "#ffffff" : phaseColor(stack.phase),
15273
- children: padFleetText(stack.phase, 9)
15274
- }, undefined, false, undefined, this)
15275
- ]
15276
- }, stack.project, true, undefined, this);
15277
- })
15278
- ]
15279
- }, undefined, true, undefined, this);
15389
+ function stackPhaseGlyph(phase, spinnerFrame = 0) {
15390
+ if (phase === "up")
15391
+ return "\u25CF";
15392
+ if (phase === "degraded")
15393
+ return "\u25C6";
15394
+ if (phase === "starting")
15395
+ return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
15396
+ if (phase === "queued" || phase === "reserved")
15397
+ return "\u25CC";
15398
+ if (phase === "stopped")
15399
+ return "\u25CB";
15400
+ return "\u25CB";
15280
15401
  }
15281
- var init_StackSidebar = __esm(() => {
15282
- init_fleet_text();
15283
- init_fleet_theme();
15284
- init_terminal_text();
15402
+ var background = "#0d1117", text = "#c9d1d9", accent = "#58a6ff", fleetTheme, SPINNER_FRAMES;
15403
+ var init_fleet_theme = __esm(() => {
15404
+ fleetTheme = {
15405
+ background,
15406
+ panel: blendHex(text, background, 0.04),
15407
+ panelAlt: blendHex(text, background, 0.1),
15408
+ border: blendHex(text, background, 0.18),
15409
+ text,
15410
+ bright: "#e6edf3",
15411
+ muted: "#8b949e",
15412
+ faint: "#6e7681",
15413
+ accent,
15414
+ selectedBg: blendHex(accent, background, 0.16),
15415
+ stripe: accent,
15416
+ keycapBg: blendHex(text, background, 0.22),
15417
+ healthy: "#3fb950",
15418
+ warning: "#d29922",
15419
+ danger: "#f85149",
15420
+ queued: "#bc8cff",
15421
+ backendDocker: "#58a6ff",
15422
+ backendProc: "#bc8cff",
15423
+ backendWrangler: "#bc8cff",
15424
+ backendTunnel: "#ffa657",
15425
+ publicBadge: "#ffa657"
15426
+ };
15427
+ SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
15285
15428
  });
15286
15429
 
15287
- // packages/tui/src/components/ServicePane.tsx
15288
- import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
15289
- function serviceColor(service) {
15290
- if (service.state === "healthy")
15291
- return fleetTheme.healthy;
15292
- if (service.state === "unknown" || service.state === "unhealthy")
15293
- return fleetTheme.warning;
15294
- return fleetTheme.danger;
15430
+ // packages/tui/src/fleet-keys.ts
15431
+ function isEscapeKey(key) {
15432
+ const name = key.name.toLowerCase();
15433
+ return name === "escape" || name === "esc" || key.sequence === "\x1B";
15295
15434
  }
15296
- function endpointReach(endpoint) {
15297
- return endpoint.localUrl ?? endpoint.publicUrl ?? endpoint.url ?? `${endpoint.host}:${endpoint.port}`;
15435
+ function isEnterKey(key) {
15436
+ return key.name === "return" || key.name === "enter";
15298
15437
  }
15299
- function ServicePane({
15300
- stack,
15301
- selectedService,
15302
- selectedEndpoint,
15303
- onSelectService,
15304
- onSelectEndpoint
15305
- }) {
15306
- return /* @__PURE__ */ jsxDEV2("box", {
15307
- style: { height: "100%", width: "100%", flexDirection: "column", border: true, borderColor: fleetTheme.border },
15308
- children: [
15309
- /* @__PURE__ */ jsxDEV2("box", {
15310
- style: { height: 1, paddingLeft: 1, backgroundColor: fleetTheme.panelAlt },
15311
- children: /* @__PURE__ */ jsxDEV2("text", {
15312
- fg: fleetTheme.text,
15313
- children: stack === undefined ? "Workloads" : `Workloads \u2014 ${sanitizeFleetTerminalText(stack.branch)}`
15314
- }, undefined, false, undefined, this)
15315
- }, undefined, false, undefined, this),
15316
- stack?.services.length ? stack.services.flatMap((service) => {
15317
- const workloadSelected = service.name === selectedService && selectedEndpoint === undefined;
15318
- const endpoints = service.endpoints ?? (service.endpoint === undefined ? [] : [service.endpoint]);
15319
- const workloadRow = /* @__PURE__ */ jsxDEV2("box", {
15320
- onMouseDown: (event) => {
15321
- if (event.button !== 0)
15322
- return;
15323
- event.preventDefault();
15324
- event.stopPropagation();
15325
- onSelectService?.(service.name);
15326
- },
15327
- style: {
15328
- height: 1,
15329
- paddingLeft: 1,
15330
- paddingRight: 1,
15331
- flexDirection: "row",
15332
- backgroundColor: workloadSelected ? fleetTheme.selected : fleetTheme.background
15333
- },
15438
+ function isPlainKey(key, value) {
15439
+ return !key.ctrl && !key.meta && !key.option && !key.super && !key.hyper && (key.name === value || key.sequence === value);
15440
+ }
15441
+ function isUpKey(key) {
15442
+ return key.name === "up" || isPlainKey(key, "k");
15443
+ }
15444
+ function isDownKey(key) {
15445
+ return key.name === "down" || isPlainKey(key, "j");
15446
+ }
15447
+ function isShiftedKey(key, lower) {
15448
+ return key.sequence === lower.toUpperCase() || isPlainKey(key, lower) && key.shift === true;
15449
+ }
15450
+ function isFollowBottomKey(key) {
15451
+ return isShiftedKey(key, "g");
15452
+ }
15453
+ function isScrollTopKey(key) {
15454
+ return !isShiftedKey(key, "g") && (key.sequence === "g" || isPlainKey(key, "g"));
15455
+ }
15456
+ function isDoctorKey(key) {
15457
+ return isShiftedKey(key, "d");
15458
+ }
15459
+
15460
+ // packages/tui/src/fleet-layout.ts
15461
+ function resolveFleetLayout(mode, terminalWidth) {
15462
+ if (mode === "auto")
15463
+ return terminalWidth >= SPLIT_MIN_WIDTH ? "split" : "stack";
15464
+ return mode;
15465
+ }
15466
+ function fleetPaneWidths(layout, terminalWidth) {
15467
+ if (layout === "stack")
15468
+ return { sidebar: terminalWidth, main: terminalWidth };
15469
+ const sidebar = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, Math.floor(terminalWidth * 0.26)));
15470
+ return { sidebar, main: Math.max(20, terminalWidth - sidebar) };
15471
+ }
15472
+ function servicePaneHeight(rows, maxHeight) {
15473
+ const content = Math.max(2, rows + 1);
15474
+ return Math.min(maxHeight, content + 3);
15475
+ }
15476
+ function stackSidebarHeight(stackCount) {
15477
+ return Math.min(9, Math.max(4, stackCount + 3));
15478
+ }
15479
+ var SPLIT_MIN_WIDTH = 110, SIDEBAR_MIN_WIDTH = 28, SIDEBAR_MAX_WIDTH = 44;
15480
+
15481
+ // packages/tui/src/fleet-log-rows.ts
15482
+ function lineId(line) {
15483
+ let id = lineIds.get(line);
15484
+ if (id === undefined) {
15485
+ id = nextLineId++;
15486
+ lineIds.set(line, id);
15487
+ }
15488
+ return id;
15489
+ }
15490
+ function wrappedBody(line, bodyWidth) {
15491
+ const cached = wrapCache.get(line);
15492
+ if (cached !== undefined && cached.width === bodyWidth)
15493
+ return cached.wrapped;
15494
+ const wrapped = wrapFleetText(line.text, bodyWidth, 3);
15495
+ wrapCache.set(line, { width: bodyWidth, wrapped });
15496
+ return wrapped;
15497
+ }
15498
+ function buildFleetLogRows(lines, contentWidth) {
15499
+ const rows = [];
15500
+ for (const line of lines) {
15501
+ const meta = line.meta === true;
15502
+ const bodyWidth = meta ? Math.max(1, contentWidth - META_TAG.length) : contentWidth;
15503
+ const wrapped = wrappedBody(line, bodyWidth);
15504
+ const id = lineId(line);
15505
+ for (let part = 0;part < wrapped.length; part += 1) {
15506
+ rows.push({
15507
+ key: `${id}:${part}`,
15508
+ text: wrapped[part],
15509
+ meta,
15510
+ tag: !meta ? undefined : part === 0 ? META_TAG : " ".repeat(META_TAG.length)
15511
+ });
15512
+ }
15513
+ }
15514
+ return rows;
15515
+ }
15516
+ var META_TAG = "hestia \u2502 ", nextLineId = 0, lineIds, wrapCache;
15517
+ var init_fleet_log_rows = __esm(() => {
15518
+ init_fleet_text();
15519
+ lineIds = new WeakMap;
15520
+ wrapCache = new WeakMap;
15521
+ });
15522
+
15523
+ // packages/tui/src/fleet-format.ts
15524
+ function formatUptime(createdAt, now) {
15525
+ const born = Date.parse(createdAt);
15526
+ if (!Number.isFinite(born))
15527
+ return;
15528
+ const seconds = Math.max(0, Math.floor((now - born) / 1000));
15529
+ if (seconds < 60)
15530
+ return `${seconds}s`;
15531
+ const minutes = Math.floor(seconds / 60);
15532
+ if (minutes < 60)
15533
+ return `${minutes}m`;
15534
+ const hours = Math.floor(minutes / 60);
15535
+ if (hours < 24)
15536
+ return `${hours}h${minutes % 60 === 0 ? "" : `${minutes % 60}m`}`;
15537
+ const days = Math.floor(hours / 24);
15538
+ return `${days}d${hours % 24 === 0 ? "" : `${hours % 24}h`}`;
15539
+ }
15540
+ function buildEnvBlock(stack) {
15541
+ const lines = [];
15542
+ for (const service of stack.services) {
15543
+ if (service.backend === "tunnel")
15544
+ continue;
15545
+ if (service.publishedPort !== undefined) {
15546
+ lines.push(`HESTIA_${envKey(service.name)}_PORT=${service.publishedPort}`);
15547
+ if (service.backend === "docker") {
15548
+ lines.push(`HESTIA_${envKey(service.name)}_MAIN_TCP_PORT=${service.publishedPort}`);
15549
+ }
15550
+ }
15551
+ for (const endpoint of serviceEndpoints(service)) {
15552
+ const alias = envKey(endpoint.name);
15553
+ lines.push(`HESTIA_${alias}_PORT=${endpoint.port}`);
15554
+ if (endpoint.publicUrl !== undefined)
15555
+ lines.push(`HESTIA_${alias}_URL=${endpoint.publicUrl}`);
15556
+ if (endpoint.localUrl !== undefined)
15557
+ lines.push(`HESTIA_${alias}_LOCAL_URL=${endpoint.localUrl}`);
15558
+ if (endpoint.url !== undefined)
15559
+ lines.push(`HESTIA_${alias}_DIRECT_URL=${endpoint.url}`);
15560
+ }
15561
+ }
15562
+ return [...new Set(lines)].join(`
15563
+ `);
15564
+ }
15565
+ var init_fleet_format = __esm(() => {
15566
+ init_src2();
15567
+ });
15568
+
15569
+ // packages/tui/src/fleet-status.ts
15570
+ function fleetCapacitySummary(capacity, connected) {
15571
+ if (!connected)
15572
+ return "daemon unreachable";
15573
+ const parts = [`${capacity.live}/${capacity.maxStacks}`];
15574
+ if (capacity.reserved > 0)
15575
+ parts.push(`${capacity.reserved} reserved`);
15576
+ if (capacity.queued > 0)
15577
+ parts.push(`${capacity.queued} queued`);
15578
+ parts.push("daemon ok");
15579
+ return parts.join(" \xB7 ");
15580
+ }
15581
+ function resolveStatusNotice(connectionNotice, toast) {
15582
+ return toast ?? connectionNotice;
15583
+ }
15584
+ var FLEET_KEY_HINTS;
15585
+ var init_fleet_status = __esm(() => {
15586
+ FLEET_KEY_HINTS = [
15587
+ { keys: ", .", label: "stack" },
15588
+ { keys: "[ ]", label: "svc" },
15589
+ { keys: "o", label: "open" },
15590
+ { keys: "y", label: "yank" },
15591
+ { keys: "?", label: "help" }
15592
+ ];
15593
+ });
15594
+
15595
+ // packages/tui/src/components/StackSidebar.tsx
15596
+ import { memo } from "react";
15597
+ import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
15598
+ var StackSidebar;
15599
+ var init_StackSidebar = __esm(() => {
15600
+ init_string_width();
15601
+ init_fleet_text();
15602
+ init_fleet_theme();
15603
+ init_terminal_text();
15604
+ StackSidebar = memo(function StackSidebar2({
15605
+ stacks,
15606
+ capacity,
15607
+ selectedProject,
15608
+ width,
15609
+ focused = false,
15610
+ spinnerFrame = 0,
15611
+ onSelectProject
15612
+ }) {
15613
+ const branchWidth = Math.max(6, width - 17);
15614
+ const slots = capacity.maxStacks > 0 ? ` \xB7 ${capacity.live}/${capacity.maxStacks} slots` : "";
15615
+ return /* @__PURE__ */ jsxDEV("box", {
15616
+ style: {
15617
+ width,
15618
+ height: "100%",
15619
+ flexDirection: "column",
15620
+ border: true,
15621
+ borderColor: focused ? fleetTheme.accent : fleetTheme.border,
15622
+ backgroundColor: fleetTheme.background
15623
+ },
15624
+ children: [
15625
+ /* @__PURE__ */ jsxDEV("box", {
15626
+ style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
15334
15627
  children: [
15335
- /* @__PURE__ */ jsxDEV2("text", {
15336
- fg: workloadSelected ? "#ffffff" : serviceColor(service),
15337
- children: workloadSelected ? "\u25B8 " : " "
15338
- }, undefined, false, undefined, this),
15339
- /* @__PURE__ */ jsxDEV2("text", {
15340
- fg: workloadSelected ? "#ffffff" : fleetTheme.text,
15341
- children: padFleetText(sanitizeFleetTerminalText(service.name), 20)
15628
+ /* @__PURE__ */ jsxDEV("text", {
15629
+ fg: focused ? fleetTheme.accent : fleetTheme.text,
15630
+ children: "Stacks"
15342
15631
  }, undefined, false, undefined, this),
15343
- /* @__PURE__ */ jsxDEV2("text", {
15344
- fg: workloadSelected ? "#ffffff" : fleetTheme.muted,
15345
- children: padFleetText(service.backend, 10)
15632
+ /* @__PURE__ */ jsxDEV("text", {
15633
+ fg: fleetTheme.faint,
15634
+ children: slots
15635
+ }, undefined, false, undefined, this)
15636
+ ]
15637
+ }, undefined, true, undefined, this),
15638
+ stacks.length === 0 ? /* @__PURE__ */ jsxDEV("box", {
15639
+ style: { paddingLeft: 1, paddingTop: 1, flexDirection: "column" },
15640
+ children: [
15641
+ /* @__PURE__ */ jsxDEV("text", {
15642
+ fg: fleetTheme.muted,
15643
+ children: "No managed stacks."
15346
15644
  }, undefined, false, undefined, this),
15347
- /* @__PURE__ */ jsxDEV2("text", {
15348
- fg: workloadSelected ? "#ffffff" : serviceColor(service),
15349
- children: service.state
15645
+ /* @__PURE__ */ jsxDEV("text", {
15646
+ fg: fleetTheme.faint,
15647
+ children: "`hestia up` starts one here."
15350
15648
  }, undefined, false, undefined, this)
15351
15649
  ]
15352
- }, `workload:${service.name}`, true, undefined, this);
15353
- const endpointRows = endpoints.map((endpoint, index) => {
15354
- const endpointSelected = service.name === selectedService && endpoint.name === selectedEndpoint;
15355
- const connector = index === endpoints.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
15356
- const kind = (endpoint.kind ?? "http").toUpperCase();
15357
- const selector = `${endpoint.workload ?? service.name}:${endpoint.binding ?? endpoint.port}`;
15358
- return /* @__PURE__ */ jsxDEV2("box", {
15650
+ }, undefined, true, undefined, this) : stacks.map((stack) => {
15651
+ const selected = stack.project === selectedProject;
15652
+ const branch = sanitizeFleetTerminalText(stack.branch);
15653
+ const phase = sanitizeFleetTerminalText(stack.phase);
15654
+ const glyph = stackPhaseGlyph(stack.phase, spinnerFrame);
15655
+ const meta = stack.services.length > 0 ? ` \xB7${stack.services.length}` : "";
15656
+ const warn = stack.warning === undefined ? "" : " \u26A0";
15657
+ const available = Math.max(4, branchWidth - stringWidth(meta) - stringWidth(warn));
15658
+ const rowBg = selected ? fleetTheme.selectedBg : fleetTheme.background;
15659
+ return /* @__PURE__ */ jsxDEV("box", {
15359
15660
  onMouseDown: (event) => {
15360
15661
  if (event.button !== 0)
15361
15662
  return;
15362
15663
  event.preventDefault();
15363
15664
  event.stopPropagation();
15364
- onSelectEndpoint?.(service.name, endpoint.name);
15365
- },
15366
- style: {
15367
- height: 1,
15368
- paddingLeft: 2,
15369
- paddingRight: 1,
15370
- flexDirection: "row",
15371
- backgroundColor: endpointSelected ? fleetTheme.selected : fleetTheme.background
15665
+ onSelectProject?.(stack.project);
15372
15666
  },
15667
+ style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: rowBg },
15373
15668
  children: [
15374
- /* @__PURE__ */ jsxDEV2("text", {
15375
- fg: endpointSelected ? "#ffffff" : fleetTheme.muted,
15376
- children: endpointSelected ? "\u25B8 " : `${connector} `
15669
+ /* @__PURE__ */ jsxDEV("text", {
15670
+ fg: fleetTheme.stripe,
15671
+ children: selected ? "\u258E" : " "
15377
15672
  }, undefined, false, undefined, this),
15378
- /* @__PURE__ */ jsxDEV2("text", {
15379
- fg: endpointSelected ? "#ffffff" : fleetTheme.accent,
15380
- children: padFleetText(sanitizeFleetTerminalText(endpoint.name), 18)
15673
+ /* @__PURE__ */ jsxDEV("text", {
15674
+ fg: stackPhaseColor(stack.phase),
15675
+ children: [
15676
+ glyph,
15677
+ " "
15678
+ ]
15679
+ }, undefined, true, undefined, this),
15680
+ /* @__PURE__ */ jsxDEV("text", {
15681
+ fg: selected ? fleetTheme.bright : fleetTheme.text,
15682
+ children: padFleetText(fitFleetText(branch, available), available)
15381
15683
  }, undefined, false, undefined, this),
15382
- /* @__PURE__ */ jsxDEV2("text", {
15383
- fg: endpointSelected ? "#ffffff" : fleetTheme.muted,
15384
- children: padFleetText(kind, 8)
15684
+ /* @__PURE__ */ jsxDEV("text", {
15685
+ fg: fleetTheme.warning,
15686
+ children: warn
15385
15687
  }, undefined, false, undefined, this),
15386
- /* @__PURE__ */ jsxDEV2("text", {
15387
- fg: endpointSelected ? "#ffffff" : fleetTheme.muted,
15388
- children: padFleetText(sanitizeFleetTerminalText(selector), 20)
15688
+ /* @__PURE__ */ jsxDEV("text", {
15689
+ fg: fleetTheme.faint,
15690
+ children: meta
15389
15691
  }, undefined, false, undefined, this),
15390
- /* @__PURE__ */ jsxDEV2("text", {
15391
- fg: endpointSelected ? "#ffffff" : fleetTheme.text,
15392
- children: sanitizeFleetTerminalText(endpointReach(endpoint))
15692
+ /* @__PURE__ */ jsxDEV("text", {
15693
+ fg: stackPhaseColor(stack.phase),
15694
+ children: ` ${padFleetText(phase, 8)}`
15393
15695
  }, undefined, false, undefined, this)
15394
15696
  ]
15395
- }, `endpoint:${service.name}:${endpoint.name}`, true, undefined, this);
15396
- });
15397
- return [workloadRow, ...endpointRows];
15398
- }) : /* @__PURE__ */ jsxDEV2("box", {
15399
- style: { paddingLeft: 1, paddingTop: 1 },
15400
- children: /* @__PURE__ */ jsxDEV2("text", {
15401
- fg: fleetTheme.muted,
15402
- children: "No workloads recorded"
15403
- }, undefined, false, undefined, this)
15404
- }, undefined, false, undefined, this)
15405
- ]
15406
- }, undefined, true, undefined, this);
15407
- }
15697
+ }, stack.project, true, undefined, this);
15698
+ })
15699
+ ]
15700
+ }, undefined, true, undefined, this);
15701
+ });
15702
+ });
15703
+
15704
+ // packages/tui/src/components/ServicePane.tsx
15705
+ import { memo as memo2 } from "react";
15706
+ import { jsxDEV as jsxDEV2, Fragment } from "@opentui/react/jsx-dev-runtime";
15707
+ function portLabel(service) {
15708
+ if (service.publishedPort === undefined)
15709
+ return "\u2014";
15710
+ return `:${service.publishedPort}`;
15711
+ }
15712
+ var ServicePane;
15408
15713
  var init_ServicePane = __esm(() => {
15409
15714
  init_fleet_text();
15410
15715
  init_fleet_theme();
15411
15716
  init_terminal_text();
15717
+ ServicePane = memo2(function ServicePane2({
15718
+ stack,
15719
+ uptime,
15720
+ selectedService,
15721
+ selectedEndpoint,
15722
+ width = 80,
15723
+ focused = false,
15724
+ onSelectService,
15725
+ onSelectEndpoint
15726
+ }) {
15727
+ const nameWidth = Math.min(24, Math.max(12, Math.floor(width * 0.28)));
15728
+ const backendWidth = 10;
15729
+ const stateWidth = 10;
15730
+ const portWidth = 7;
15731
+ const title = stack === undefined ? "Workloads" : `Workloads \u2014 ${sanitizeFleetTerminalText(stack.branch)}`;
15732
+ const warning = stack?.warning === undefined ? undefined : sanitizeFleetTerminalText(stack.warning);
15733
+ return /* @__PURE__ */ jsxDEV2("box", {
15734
+ style: {
15735
+ height: "100%",
15736
+ width: "100%",
15737
+ flexDirection: "column",
15738
+ border: true,
15739
+ borderColor: focused ? fleetTheme.accent : fleetTheme.border,
15740
+ backgroundColor: fleetTheme.background
15741
+ },
15742
+ children: [
15743
+ /* @__PURE__ */ jsxDEV2("box", {
15744
+ style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
15745
+ children: [
15746
+ /* @__PURE__ */ jsxDEV2("text", {
15747
+ fg: focused ? fleetTheme.accent : fleetTheme.text,
15748
+ children: fitFleetText(title, Math.max(8, width - 14))
15749
+ }, undefined, false, undefined, this),
15750
+ uptime === undefined ? null : /* @__PURE__ */ jsxDEV2("text", {
15751
+ fg: fleetTheme.faint,
15752
+ children: [
15753
+ " \xB7 up ",
15754
+ uptime
15755
+ ]
15756
+ }, undefined, true, undefined, this)
15757
+ ]
15758
+ }, undefined, true, undefined, this),
15759
+ warning === undefined ? null : /* @__PURE__ */ jsxDEV2("box", {
15760
+ style: { height: 1, paddingLeft: 1, paddingRight: 1 },
15761
+ children: /* @__PURE__ */ jsxDEV2("text", {
15762
+ fg: fleetTheme.warning,
15763
+ children: fitFleetText(`\u26A0 ${warning}`, Math.max(8, width - 4))
15764
+ }, undefined, false, undefined, this)
15765
+ }, undefined, false, undefined, this),
15766
+ stack?.services.length ? /* @__PURE__ */ jsxDEV2(Fragment, {
15767
+ children: [
15768
+ /* @__PURE__ */ jsxDEV2("box", {
15769
+ style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row" },
15770
+ children: [
15771
+ /* @__PURE__ */ jsxDEV2("text", {
15772
+ fg: fleetTheme.faint,
15773
+ children: padFleetText("", 4)
15774
+ }, undefined, false, undefined, this),
15775
+ /* @__PURE__ */ jsxDEV2("text", {
15776
+ fg: fleetTheme.faint,
15777
+ children: padFleetText("NAME", nameWidth)
15778
+ }, undefined, false, undefined, this),
15779
+ /* @__PURE__ */ jsxDEV2("text", {
15780
+ fg: fleetTheme.faint,
15781
+ children: padFleetText("BACKEND", backendWidth)
15782
+ }, undefined, false, undefined, this),
15783
+ /* @__PURE__ */ jsxDEV2("text", {
15784
+ fg: fleetTheme.faint,
15785
+ children: padFleetText("STATE", stateWidth)
15786
+ }, undefined, false, undefined, this),
15787
+ /* @__PURE__ */ jsxDEV2("text", {
15788
+ fg: fleetTheme.faint,
15789
+ children: padFleetText("PORT", portWidth)
15790
+ }, undefined, false, undefined, this)
15791
+ ]
15792
+ }, undefined, true, undefined, this),
15793
+ stack.services.flatMap((service) => {
15794
+ const workloadSelected = service.name === selectedService && selectedEndpoint === undefined;
15795
+ const endpoints = serviceEndpoints(service);
15796
+ const state = sanitizeFleetTerminalText(service.state);
15797
+ const workloadBg = workloadSelected ? fleetTheme.selectedBg : fleetTheme.background;
15798
+ const workloadRow = /* @__PURE__ */ jsxDEV2("box", {
15799
+ onMouseDown: (event) => {
15800
+ if (event.button !== 0)
15801
+ return;
15802
+ event.preventDefault();
15803
+ event.stopPropagation();
15804
+ onSelectService?.(service.name);
15805
+ },
15806
+ style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: workloadBg },
15807
+ children: [
15808
+ /* @__PURE__ */ jsxDEV2("text", {
15809
+ fg: fleetTheme.stripe,
15810
+ children: workloadSelected ? "\u258E" : " "
15811
+ }, undefined, false, undefined, this),
15812
+ /* @__PURE__ */ jsxDEV2("text", {
15813
+ fg: serviceStateColor(service.state),
15814
+ children: [
15815
+ " ",
15816
+ serviceStateGlyph(service.state),
15817
+ " "
15818
+ ]
15819
+ }, undefined, true, undefined, this),
15820
+ /* @__PURE__ */ jsxDEV2("text", {
15821
+ fg: workloadSelected ? fleetTheme.bright : fleetTheme.text,
15822
+ children: padFleetText(sanitizeFleetTerminalText(service.name), nameWidth)
15823
+ }, undefined, false, undefined, this),
15824
+ /* @__PURE__ */ jsxDEV2("text", {
15825
+ fg: backendColor(service.backend),
15826
+ children: padFleetText(service.backend, backendWidth)
15827
+ }, undefined, false, undefined, this),
15828
+ /* @__PURE__ */ jsxDEV2("text", {
15829
+ fg: serviceStateColor(service.state),
15830
+ children: padFleetText(state, stateWidth)
15831
+ }, undefined, false, undefined, this),
15832
+ /* @__PURE__ */ jsxDEV2("text", {
15833
+ fg: fleetTheme.muted,
15834
+ children: padFleetText(portLabel(service), portWidth)
15835
+ }, undefined, false, undefined, this)
15836
+ ]
15837
+ }, `workload:${service.name}`, true, undefined, this);
15838
+ const endpointRows = endpoints.map((endpoint, index) => {
15839
+ const endpointSelected = service.name === selectedService && endpoint.name === selectedEndpoint;
15840
+ const connector = index === endpoints.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
15841
+ const kind = (endpoint.kind ?? "http").toUpperCase();
15842
+ const reach = sanitizeFleetTerminalText(endpointReach(endpoint));
15843
+ const aliasWidth = Math.min(18, Math.max(10, Math.floor(width * 0.22)));
15844
+ const pub = endpoint.publicUrl === undefined ? "" : " pub";
15845
+ const endpointBg = endpointSelected ? fleetTheme.selectedBg : fleetTheme.background;
15846
+ return /* @__PURE__ */ jsxDEV2("box", {
15847
+ onMouseDown: (event) => {
15848
+ if (event.button !== 0)
15849
+ return;
15850
+ event.preventDefault();
15851
+ event.stopPropagation();
15852
+ onSelectEndpoint?.(service.name, endpoint.name);
15853
+ },
15854
+ style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: endpointBg },
15855
+ children: [
15856
+ /* @__PURE__ */ jsxDEV2("text", {
15857
+ fg: fleetTheme.stripe,
15858
+ children: endpointSelected ? "\u258E" : " "
15859
+ }, undefined, false, undefined, this),
15860
+ /* @__PURE__ */ jsxDEV2("text", {
15861
+ fg: fleetTheme.faint,
15862
+ children: ` ${connector} `
15863
+ }, undefined, false, undefined, this),
15864
+ /* @__PURE__ */ jsxDEV2("text", {
15865
+ fg: endpointSelected ? fleetTheme.bright : fleetTheme.accent,
15866
+ children: padFleetText(sanitizeFleetTerminalText(endpoint.name), aliasWidth)
15867
+ }, undefined, false, undefined, this),
15868
+ /* @__PURE__ */ jsxDEV2("text", {
15869
+ fg: fleetTheme.muted,
15870
+ children: padFleetText(kind, 6)
15871
+ }, undefined, false, undefined, this),
15872
+ /* @__PURE__ */ jsxDEV2("text", {
15873
+ fg: fleetTheme.text,
15874
+ children: fitFleetText(reach, Math.max(12, width - aliasWidth - 19 - pub.length))
15875
+ }, undefined, false, undefined, this),
15876
+ /* @__PURE__ */ jsxDEV2("text", {
15877
+ fg: fleetTheme.publicBadge,
15878
+ children: pub
15879
+ }, undefined, false, undefined, this)
15880
+ ]
15881
+ }, `endpoint:${service.name}:${endpoint.name}`, true, undefined, this);
15882
+ });
15883
+ return [workloadRow, ...endpointRows];
15884
+ })
15885
+ ]
15886
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV2("box", {
15887
+ style: { paddingLeft: 1, paddingTop: 1 },
15888
+ children: /* @__PURE__ */ jsxDEV2("text", {
15889
+ fg: fleetTheme.muted,
15890
+ children: stack?.phase === "queued" || stack?.phase === "reserved" ? "Waiting for a machine slot \u2014 services appear once startup begins." : "No workloads recorded"
15891
+ }, undefined, false, undefined, this)
15892
+ }, undefined, false, undefined, this)
15893
+ ]
15894
+ }, undefined, true, undefined, this);
15895
+ });
15412
15896
  });
15413
15897
 
15414
15898
  // packages/tui/src/components/LogPane.tsx
15415
15899
  import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
15416
15900
  function LogPane({
15417
- lines,
15901
+ rows,
15418
15902
  height,
15419
15903
  width,
15420
15904
  offset,
15421
15905
  follow,
15422
15906
  unseen,
15423
15907
  label,
15908
+ focused = false,
15424
15909
  onScroll
15425
15910
  }) {
15426
15911
  const viewportRows = Math.max(1, height - 3);
15427
- const end = Math.max(0, lines.length - offset);
15428
- const start = Math.max(0, end - viewportRows - 2);
15429
- const visible = lines.slice(start, end).slice(-viewportRows);
15912
+ const maxOffset = Math.max(0, rows.length - viewportRows);
15913
+ const clamped = Math.min(offset, maxOffset);
15914
+ const end = Math.max(0, rows.length - clamped);
15915
+ const start = Math.max(0, end - viewportRows);
15916
+ const visible = rows.slice(start, end);
15917
+ const title = fitFleetText(`Logs \u2014 ${label}`, Math.max(8, width - 28));
15430
15918
  return /* @__PURE__ */ jsxDEV3("box", {
15431
15919
  onMouseScroll: (event) => {
15432
15920
  if (event.button !== 4 && event.button !== 5)
@@ -15435,14 +15923,21 @@ function LogPane({
15435
15923
  event.stopPropagation();
15436
15924
  onScroll?.(event.button === 4 ? -3 : 3);
15437
15925
  },
15438
- style: { height: "100%", width: "100%", flexDirection: "column", border: true, borderColor: fleetTheme.border },
15926
+ style: {
15927
+ height: "100%",
15928
+ width: "100%",
15929
+ flexDirection: "column",
15930
+ border: true,
15931
+ borderColor: focused ? fleetTheme.accent : fleetTheme.border,
15932
+ backgroundColor: fleetTheme.background
15933
+ },
15439
15934
  children: [
15440
15935
  /* @__PURE__ */ jsxDEV3("box", {
15441
15936
  style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
15442
15937
  children: [
15443
15938
  /* @__PURE__ */ jsxDEV3("text", {
15444
- fg: fleetTheme.text,
15445
- children: padFleetText(`Logs \u2014 ${label}`, Math.max(1, width - 24))
15939
+ fg: focused ? fleetTheme.accent : fleetTheme.text,
15940
+ children: padFleetText(title, Math.max(1, width - 24))
15446
15941
  }, undefined, false, undefined, this),
15447
15942
  /* @__PURE__ */ jsxDEV3("text", {
15448
15943
  fg: follow ? fleetTheme.healthy : fleetTheme.warning,
@@ -15454,15 +15949,21 @@ function LogPane({
15454
15949
  style: { paddingLeft: 1, paddingTop: 1 },
15455
15950
  children: /* @__PURE__ */ jsxDEV3("text", {
15456
15951
  fg: fleetTheme.muted,
15457
- children: "No log output yet \u2014 service is still being followed."
15952
+ children: "No log output yet \u2014 waiting for this workload."
15458
15953
  }, undefined, false, undefined, this)
15459
- }, undefined, false, undefined, this) : visible.map((line, index) => /* @__PURE__ */ jsxDEV3("box", {
15460
- style: { height: 1, paddingLeft: 1 },
15461
- children: /* @__PURE__ */ jsxDEV3("text", {
15462
- fg: line.meta ? fleetTheme.warning : fleetTheme.text,
15463
- children: fitFleetText(line.meta ? `[hestia] ${line.text}` : line.text, Math.max(1, width - 4))
15464
- }, undefined, false, undefined, this)
15465
- }, `${start + index}-${line.text}`, false, undefined, this))
15954
+ }, undefined, false, undefined, this) : visible.map((row2) => /* @__PURE__ */ jsxDEV3("box", {
15955
+ style: { height: 1, paddingLeft: 1, flexDirection: "row" },
15956
+ children: [
15957
+ row2.tag === undefined ? null : /* @__PURE__ */ jsxDEV3("text", {
15958
+ fg: fleetTheme.accent,
15959
+ children: row2.tag
15960
+ }, undefined, false, undefined, this),
15961
+ /* @__PURE__ */ jsxDEV3("text", {
15962
+ fg: row2.meta ? fleetTheme.warning : fleetTheme.text,
15963
+ children: row2.text
15964
+ }, undefined, false, undefined, this)
15965
+ ]
15966
+ }, row2.key, true, undefined, this))
15466
15967
  ]
15467
15968
  }, undefined, true, undefined, this);
15468
15969
  }
@@ -15472,16 +15973,16 @@ var init_LogPane = __esm(() => {
15472
15973
  });
15473
15974
 
15474
15975
  // packages/tui/src/components/FleetModal.tsx
15475
- import { jsxDEV as jsxDEV4, Fragment } from "@opentui/react/jsx-dev-runtime";
15976
+ import { jsxDEV as jsxDEV4, Fragment as Fragment2 } from "@opentui/react/jsx-dev-runtime";
15476
15977
  function FleetModal({
15477
15978
  title,
15478
15979
  terminalWidth,
15479
15980
  terminalHeight,
15480
15981
  children
15481
15982
  }) {
15482
- const width = Math.min(72, Math.max(30, terminalWidth - 4));
15483
- const height = Math.min(18, Math.max(7, terminalHeight - 4));
15484
- return /* @__PURE__ */ jsxDEV4(Fragment, {
15983
+ const width = Math.min(78, Math.max(36, terminalWidth - 4));
15984
+ const height = Math.min(22, Math.max(8, terminalHeight - 4));
15985
+ return /* @__PURE__ */ jsxDEV4(Fragment2, {
15485
15986
  children: [
15486
15987
  /* @__PURE__ */ jsxDEV4("box", {
15487
15988
  style: { position: "absolute", top: 0, left: 0, width: terminalWidth, height: terminalHeight, zIndex: 50 }
@@ -15503,10 +16004,22 @@ function FleetModal({
15503
16004
  paddingTop: 1
15504
16005
  },
15505
16006
  children: [
15506
- /* @__PURE__ */ jsxDEV4("text", {
15507
- fg: fleetTheme.accent,
15508
- children: title
15509
- }, undefined, false, undefined, this),
16007
+ /* @__PURE__ */ jsxDEV4("box", {
16008
+ style: { height: 1, flexDirection: "row" },
16009
+ children: [
16010
+ /* @__PURE__ */ jsxDEV4("box", {
16011
+ style: { flexGrow: 1 },
16012
+ children: /* @__PURE__ */ jsxDEV4("text", {
16013
+ fg: fleetTheme.accent,
16014
+ children: title
16015
+ }, undefined, false, undefined, this)
16016
+ }, undefined, false, undefined, this),
16017
+ /* @__PURE__ */ jsxDEV4("text", {
16018
+ fg: fleetTheme.faint,
16019
+ children: "Esc closes"
16020
+ }, undefined, false, undefined, this)
16021
+ ]
16022
+ }, undefined, true, undefined, this),
15510
16023
  /* @__PURE__ */ jsxDEV4("box", {
15511
16024
  style: { height: 1 }
15512
16025
  }, undefined, false, undefined, this),
@@ -15521,10 +16034,10 @@ var init_FleetModal = __esm(() => {
15521
16034
  });
15522
16035
 
15523
16036
  // packages/tui/src/FleetApp.tsx
15524
- import { useEffect, useMemo, useReducer, useRef, useState } from "react";
16037
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
15525
16038
  import { existsSync as existsSync23 } from "fs";
15526
16039
  import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
15527
- import { jsxDEV as jsxDEV5, Fragment as Fragment2 } from "@opentui/react/jsx-dev-runtime";
16040
+ import { jsxDEV as jsxDEV5, Fragment as Fragment3 } from "@opentui/react/jsx-dev-runtime";
15528
16041
  function middleTruncateWorktreePath(path, width) {
15529
16042
  if (path.length <= width)
15530
16043
  return path;
@@ -15542,32 +16055,24 @@ function middleTruncateWorktreePath(path, width) {
15542
16055
  function emptySnapshot(repoId) {
15543
16056
  return { repoId, observedAt: new Date(0).toISOString(), capacity: EMPTY_CAPACITY, stacks: [], shared: [], warnings: [] };
15544
16057
  }
15545
- function isEscape(key) {
15546
- const name = key.name.toLowerCase();
15547
- return name === "escape" || name === "esc" || key.sequence === "\x1B";
15548
- }
15549
- function isFleetKey(key, value) {
15550
- return !key.ctrl && !key.meta && !key.option && !key.super && !key.hyper && (key.name === value || key.sequence === value);
15551
- }
15552
16058
  function selectedStack(snapshot, project) {
15553
16059
  return snapshot.stacks.find((stack) => stack.project === project);
15554
16060
  }
15555
16061
  function fleetServiceRowCount(stack) {
15556
- return stack?.services.reduce((count, service) => count + 1 + (service.endpoints?.length ?? (service.endpoint === undefined ? 0 : 1)), 0) ?? 0;
16062
+ return stack?.services.reduce((count, service) => count + 1 + serviceEndpoints(service).length, 0) ?? 0;
15557
16063
  }
15558
16064
  function selectedFleetEndpoint(stack, serviceName, endpointName) {
15559
16065
  const service = stack?.services.find((candidate) => candidate.name === serviceName);
15560
- const endpoints = service?.endpoints ?? (service?.endpoint === undefined ? [] : [service.endpoint]);
16066
+ const endpoints = service === undefined ? [] : serviceEndpoints(service);
15561
16067
  return endpoints.find((endpoint) => endpoint.name === endpointName) ?? endpoints[0];
15562
16068
  }
15563
16069
  function usableEndpoint(stack, serviceName, endpointName) {
15564
16070
  const endpoint = selectedFleetEndpoint(stack, serviceName, endpointName);
15565
- if (endpoint === undefined)
15566
- return;
15567
- return endpoint.localUrl ?? endpoint.publicUrl ?? endpoint.url ?? `${endpoint.host}:${endpoint.port}`;
16071
+ return endpoint === undefined ? undefined : endpointReach(endpoint);
15568
16072
  }
15569
- function copyableEndpoint(stack, serviceName, endpointName) {
15570
- return selectedFleetEndpoint(stack, serviceName, endpointName);
16073
+ function downTargetChanged(confirmed, snapshot) {
16074
+ const current = snapshot.stacks.find((candidate) => candidate.project === confirmed.project);
16075
+ return current?.repoId !== confirmed.repoId || current.worktree !== confirmed.worktree || current.createdAt !== confirmed.createdAt;
15571
16076
  }
15572
16077
  function fleetLogSelectionKey(stack, serviceName) {
15573
16078
  if (stack === undefined || serviceName === undefined)
@@ -15593,6 +16098,20 @@ function doctorOmissionSummary(rows, visibleRows = 10) {
15593
16098
  const warnings = omitted.filter((row2) => row2.level === "warn").length;
15594
16099
  return `\u2026 ${omitted.length} more (${errors2} errors, ${warnings} warnings)`;
15595
16100
  }
16101
+ function budgetDoctorReport(rows, width, lineBudget) {
16102
+ const entries = [];
16103
+ let used = 0;
16104
+ for (const row2 of rows) {
16105
+ const detail = sanitizeFleetTerminalText(row2.detail);
16106
+ const detailLines = detail === "" ? [] : wrapFleetText(detail, Math.max(8, width - 6), 2);
16107
+ const cost = 1 + detailLines.length;
16108
+ if (used + cost > lineBudget)
16109
+ break;
16110
+ entries.push({ row: row2, detailLines });
16111
+ used += cost;
16112
+ }
16113
+ return { entries, shown: entries.length };
16114
+ }
15596
16115
  function FleetApp({
15597
16116
  source,
15598
16117
  preferredProject,
@@ -15605,13 +16124,29 @@ function FleetApp({
15605
16124
  const [state, dispatch] = useReducer(reduceFleetUiState, undefined, createFleetUiState);
15606
16125
  const [logs, setLogs] = useState([]);
15607
16126
  const [connected, setConnected] = useState(false);
15608
- const [notice, setNotice] = useState("connecting to hestiad\u2026");
16127
+ const [connectionNotice, setConnectionNotice] = useState("connecting to hestiad\u2026");
16128
+ const [toast, setToast] = useState(undefined);
15609
16129
  const [doctorRows, setDoctorRows] = useState([]);
15610
16130
  const [doctorRunning, setDoctorRunning] = useState(false);
15611
16131
  const [sharedBusy, setSharedBusy] = useState(false);
16132
+ const [spinnerFrame, setSpinnerFrame] = useState(0);
16133
+ const [now, setNow] = useState(() => Date.now());
15612
16134
  const lastFrameAt = useRef(Date.now());
16135
+ const toastTimer = useRef(undefined);
15613
16136
  const stateRef = useRef(state);
15614
16137
  stateRef.current = state;
16138
+ const showToast = (text2, ttl = TOAST_TTL_MS) => {
16139
+ if (toastTimer.current !== undefined)
16140
+ clearTimeout(toastTimer.current);
16141
+ setToast(text2);
16142
+ toastTimer.current = setTimeout(() => {
16143
+ setToast((current) => current === text2 ? undefined : current);
16144
+ }, ttl);
16145
+ };
16146
+ useEffect(() => () => {
16147
+ if (toastTimer.current !== undefined)
16148
+ clearTimeout(toastTimer.current);
16149
+ }, []);
15615
16150
  useEffect(() => {
15616
16151
  const controller = new AbortController;
15617
16152
  (async () => {
@@ -15619,23 +16154,24 @@ function FleetApp({
15619
16154
  lastFrameAt.current = Date.now();
15620
16155
  if (frame.type === "snapshot") {
15621
16156
  setConnected(true);
15622
- setNotice((current) => current === "connecting to hestiad\u2026" || current.startsWith("disconnected:") || current.startsWith("hestiad heartbeat") ? "" : current);
16157
+ setConnectionNotice(undefined);
15623
16158
  setSnapshot(frame.snapshot);
15624
16159
  dispatch({ type: "reconcile", snapshot: frame.snapshot, preferredProject });
15625
16160
  } else if (frame.sequence === -1) {
15626
16161
  setConnected(false);
15627
- setNotice(frame.at);
16162
+ setConnectionNotice(frame.at);
15628
16163
  } else {
15629
16164
  setConnected(true);
16165
+ setConnectionNotice(undefined);
15630
16166
  }
15631
16167
  }
15632
16168
  })();
15633
16169
  const staleTimer = setInterval(() => {
15634
16170
  if (Date.now() - lastFrameAt.current > 20000) {
15635
16171
  setConnected(false);
15636
- setNotice("hestiad heartbeat is stale; reconnecting\u2026");
16172
+ setConnectionNotice("hestiad heartbeat is stale; reconnecting\u2026");
15637
16173
  }
15638
- }, 1000);
16174
+ }, 5000);
15639
16175
  return () => {
15640
16176
  clearInterval(staleTimer);
15641
16177
  controller.abort();
@@ -15643,8 +16179,12 @@ function FleetApp({
15643
16179
  }, [preferredProject, source]);
15644
16180
  const selectedLogStack = selectedStack(snapshot, state.selection.project);
15645
16181
  const selectionKey = fleetLogSelectionKey(selectedLogStack, state.selection.service);
16182
+ const appendedLines = useRef(0);
16183
+ const seenAppendedLines = useRef(0);
15646
16184
  useEffect(() => {
15647
16185
  setLogs([]);
16186
+ appendedLines.current = 0;
16187
+ seenAppendedLines.current = 0;
15648
16188
  if (selectionKey === undefined)
15649
16189
  return;
15650
16190
  const [project, service] = selectionKey.split("\x00");
@@ -15657,57 +16197,90 @@ function FleetApp({
15657
16197
  service: sanitizeFleetTerminalText(line.service),
15658
16198
  text: sanitizeFleetTerminalText(line.text)
15659
16199
  };
16200
+ appendedLines.current += 1;
15660
16201
  setLogs((current) => [...current, sanitized].slice(-LOG_RING_CAPACITY));
15661
- dispatch({ type: "new-lines", count: 1, maxOffset: LOG_RING_CAPACITY - 1 });
15662
16202
  }
15663
16203
  })();
15664
16204
  return () => controller.abort();
15665
16205
  }, [selectionKey, source]);
15666
16206
  const stacks = useMemo(() => visibleFleetStacks(snapshot, state.filter), [snapshot, state.filter]);
15667
16207
  const stack = selectedStack(snapshot, state.selection.project);
15668
- const context = {
16208
+ const context = useMemo(() => ({
15669
16209
  repo: sanitizeFleetTerminalText(stack?.repo ?? invokingRepository.repo),
15670
16210
  branch: sanitizeFleetTerminalText(stack?.branch ?? invokingRepository.branch),
15671
16211
  worktree: sanitizeFleetTerminalText(stack?.worktree ?? invokingRepository.worktree),
15672
16212
  project: sanitizeFleetTerminalText(stack?.project ?? "\u2014")
15673
- };
15674
- const endpointView = copyableEndpoint(stack, state.selection.service, state.selection.endpoint);
16213
+ }), [stack, invokingRepository]);
16214
+ const endpointView = selectedFleetEndpoint(stack, state.selection.service, state.selection.endpoint);
15675
16215
  const endpoint = usableEndpoint(stack, state.selection.service, state.selection.endpoint);
15676
- const effectiveLayout = state.layout === "auto" ? terminal.width >= 110 ? "split" : "stack" : state.layout;
15677
- const focusOrder = ["stacks", "services", "logs"];
16216
+ const effectiveLayout = resolveFleetLayout(state.layout, terminal.width);
16217
+ const paneWidths = fleetPaneWidths(effectiveLayout, terminal.width);
16218
+ const headerRows = 1;
16219
+ const contextRows = 2;
16220
+ const footerRows = 1;
16221
+ const warningRows = stack?.warning === undefined ? 0 : 1;
16222
+ const svcRows = fleetServiceRowCount(stack) + warningRows;
16223
+ const stackedSidebarHeight = stackSidebarHeight(stacks.length);
16224
+ const svcPaneH = servicePaneHeight(svcRows, effectiveLayout === "split" ? 14 : 12);
16225
+ const logHeight = Math.max(5, terminal.height - headerRows - contextRows - footerRows - svcPaneH - (effectiveLayout === "stack" ? stackedSidebarHeight : 0));
16226
+ const logWidth = effectiveLayout === "split" ? paneWidths.main : terminal.width;
16227
+ const logViewportRows = Math.max(1, logHeight - 3);
16228
+ const logRows = useMemo(() => buildFleetLogRows(logs, Math.max(1, logWidth - 4)), [logs, logWidth]);
16229
+ const maxLogOffset = Math.max(0, logRows.length - logViewportRows);
16230
+ useEffect(() => {
16231
+ const appended = appendedLines.current - seenAppendedLines.current;
16232
+ seenAppendedLines.current = appendedLines.current;
16233
+ if (appended > 0 && !stateRef.current.follow) {
16234
+ const fresh = buildFleetLogRows(logs.slice(-Math.min(appended, logs.length)), Math.max(1, logWidth - 4)).length;
16235
+ dispatch({ type: "new-lines", count: fresh, maxOffset: maxLogOffset });
16236
+ }
16237
+ }, [logs, logWidth, maxLogOffset]);
16238
+ const anyStarting = snapshot.stacks.some((candidate) => candidate.phase === "starting");
16239
+ useEffect(() => {
16240
+ if (!anyStarting)
16241
+ return;
16242
+ const timer = setInterval(() => setSpinnerFrame((frame) => (frame + 1) % SPINNER_FRAMES.length), 120);
16243
+ return () => clearInterval(timer);
16244
+ }, [anyStarting]);
16245
+ useEffect(() => {
16246
+ const timer = setInterval(() => setNow(Date.now()), 30000);
16247
+ return () => clearInterval(timer);
16248
+ }, []);
15678
16249
  useEffect(() => {
15679
16250
  const confirmed = state.confirmDown;
15680
- const currentIncarnation = snapshot.stacks.find((candidate) => candidate.project === confirmed?.project);
15681
- if (confirmed !== undefined && !state.downPending && (currentIncarnation?.repoId !== confirmed.repoId || currentIncarnation.worktree !== confirmed.worktree || currentIncarnation.createdAt !== confirmed.createdAt)) {
16251
+ if (confirmed !== undefined && !state.downPending && downTargetChanged(confirmed, snapshot)) {
15682
16252
  dispatch({ type: "confirm-down" });
15683
- setNotice(`${confirmed.project} changed; down cancelled`);
16253
+ showToast(`${confirmed.project} changed; down cancelled`);
15684
16254
  }
15685
16255
  }, [snapshot, state.confirmDown, state.downPending]);
16256
+ const copyToClipboard = (value, label) => {
16257
+ const copied = renderer.copyToClipboardOSC52(value);
16258
+ showToast(copied ? `copied ${label ?? value}` : `copy unavailable: ${label ?? value}`);
16259
+ };
15686
16260
  useKeyboard((key) => {
15687
16261
  const current = stateRef.current;
15688
16262
  if (current.confirmDown !== undefined) {
15689
16263
  key.preventDefault();
15690
16264
  key.stopPropagation();
15691
- if (isEscape(key) && !current.downPending) {
16265
+ if (isEscapeKey(key) && !current.downPending) {
15692
16266
  dispatch({ type: "confirm-down" });
15693
- } else if (!current.downPending && (isFleetKey(key, "d") || key.name === "return" || key.name === "enter")) {
16267
+ } else if (!current.downPending && (isPlainKey(key, "d") || isEnterKey(key))) {
15694
16268
  const confirmed = current.confirmDown;
15695
- const currentIncarnation = snapshot.stacks.find((candidate) => candidate.project === confirmed.project);
15696
- if (currentIncarnation?.repoId !== confirmed.repoId || currentIncarnation.worktree !== confirmed.worktree || currentIncarnation.createdAt !== confirmed.createdAt) {
16269
+ if (downTargetChanged(confirmed, snapshot)) {
15697
16270
  dispatch({ type: "confirm-down" });
15698
- setNotice(`${confirmed.project} changed; down cancelled`);
16271
+ showToast(`${confirmed.project} changed; down cancelled`);
15699
16272
  return;
15700
16273
  }
15701
16274
  const project = confirmed.project;
15702
16275
  dispatch({ type: "down-pending", pending: true });
15703
- setNotice(`tearing down ${project}\u2026`);
16276
+ showToast(`tearing down ${project}\u2026`);
15704
16277
  source.down(confirmed).then(() => {
15705
16278
  dispatch({ type: "down-pending", pending: false });
15706
16279
  dispatch({ type: "confirm-down" });
15707
- setNotice(`${project} is down; named volumes retained`);
16280
+ showToast(`${project} is down; named volumes retained`);
15708
16281
  }).catch((error) => {
15709
16282
  dispatch({ type: "down-pending", pending: false });
15710
- setNotice(`down failed: ${error.message}`);
16283
+ showToast(`down failed: ${error.message}`);
15711
16284
  });
15712
16285
  }
15713
16286
  return;
@@ -15715,7 +16288,7 @@ function FleetApp({
15715
16288
  if (current.helpOpen || current.doctorOpen) {
15716
16289
  key.preventDefault();
15717
16290
  key.stopPropagation();
15718
- if (isEscape(key) || isFleetKey(key, "?")) {
16291
+ if (isEscapeKey(key) || isPlainKey(key, "?")) {
15719
16292
  dispatch({ type: current.helpOpen ? "help" : "doctor", open: false });
15720
16293
  }
15721
16294
  return;
@@ -15723,45 +16296,45 @@ function FleetApp({
15723
16296
  if (current.sharedOpen) {
15724
16297
  key.preventDefault();
15725
16298
  key.stopPropagation();
15726
- if (isEscape(key) || isFleetKey(key, "s"))
16299
+ if (isEscapeKey(key) || isPlainKey(key, "s"))
15727
16300
  return dispatch({ type: "shared", open: false });
15728
16301
  const list = snapshot.shared;
15729
16302
  if (list.length === 0)
15730
16303
  return;
15731
- if (key.name === "down" || isFleetKey(key, "j"))
16304
+ if (isDownKey(key))
15732
16305
  return dispatch({ type: "move-shared", delta: 1, count: list.length });
15733
- if (key.name === "up" || isFleetKey(key, "k"))
16306
+ if (isUpKey(key))
15734
16307
  return dispatch({ type: "move-shared", delta: -1, count: list.length });
15735
16308
  const selected = list[Math.min(current.sharedSelection, list.length - 1)];
15736
16309
  if (selected === undefined || sharedBusy)
15737
16310
  return;
15738
16311
  const runShared = (label, action) => {
15739
16312
  setSharedBusy(true);
15740
- setNotice(`${label} ${selected.name}\u2026`);
15741
- action.then(() => setNotice(`${label} ${selected.name}: done`)).catch((error) => setNotice(`${label} ${selected.name} failed: ${error.message}`)).finally(() => setSharedBusy(false));
16313
+ showToast(`${label} ${selected.name}\u2026`);
16314
+ action.then(() => showToast(`${label} ${selected.name}: done`)).catch((error) => showToast(`${label} ${selected.name} failed: ${error.message}`)).finally(() => setSharedBusy(false));
15742
16315
  };
15743
- if (isFleetKey(key, "a") || isFleetKey(key, "x") || isFleetKey(key, "r")) {
16316
+ if (isPlainKey(key, "a") || isPlainKey(key, "x") || isPlainKey(key, "r")) {
15744
16317
  if (selected.holder?.mine !== true) {
15745
- setNotice(`this worktree does not hold ${selected.name}`);
16318
+ showToast(`this worktree does not hold ${selected.name}`);
15746
16319
  return;
15747
16320
  }
15748
16321
  const worktree = selected.holder.worktree;
15749
- if (isFleetKey(key, "a"))
16322
+ if (isPlainKey(key, "a"))
15750
16323
  runShared("allowing", source.allowShared(worktree, selected.name));
15751
- else if (isFleetKey(key, "x"))
16324
+ else if (isPlainKey(key, "x"))
15752
16325
  runShared("denying", source.denyShared(worktree, selected.name));
15753
16326
  else
15754
16327
  runShared("releasing", source.releaseShared(worktree, selected.name));
15755
16328
  return;
15756
16329
  }
15757
- if (isFleetKey(key, "c")) {
16330
+ if (isPlainKey(key, "c")) {
15758
16331
  const acting = selectedStack(snapshot, current.selection.project);
15759
16332
  if (acting === undefined) {
15760
- setNotice("select a stack (in the fleet list) to claim as");
16333
+ showToast("select a stack (in the fleet list) to claim as");
15761
16334
  return;
15762
16335
  }
15763
16336
  if (selected.holder?.project === acting.project) {
15764
- setNotice(`${acting.project} already holds ${selected.name}`);
16337
+ showToast(`${acting.project} already holds ${selected.name}`);
15765
16338
  return;
15766
16339
  }
15767
16340
  runShared(`claiming as ${acting.project},`, source.claimShared(acting.worktree, selected.name));
@@ -15770,41 +16343,59 @@ function FleetApp({
15770
16343
  return;
15771
16344
  }
15772
16345
  if (current.focus === "filter") {
15773
- if (isEscape(key) || key.name === "return" || key.name === "enter") {
16346
+ if (isEscapeKey(key)) {
16347
+ key.preventDefault();
16348
+ key.stopPropagation();
16349
+ if (current.filter !== "")
16350
+ dispatch({ type: "filter", filter: "", snapshot });
16351
+ else
16352
+ dispatch({ type: "focus", focus: "stacks" });
16353
+ return;
16354
+ }
16355
+ if (isEnterKey(key)) {
15774
16356
  key.preventDefault();
15775
16357
  key.stopPropagation();
15776
16358
  dispatch({ type: "focus", focus: "stacks" });
15777
16359
  }
15778
16360
  return;
15779
16361
  }
15780
- if (isFleetKey(key, "q"))
16362
+ if (isPlainKey(key, "q"))
15781
16363
  return onQuit();
15782
- if (isFleetKey(key, "?"))
16364
+ if (isPlainKey(key, "?"))
15783
16365
  return dispatch({ type: "help", open: true });
15784
- if (isFleetKey(key, "s"))
16366
+ if (isPlainKey(key, "s"))
15785
16367
  return dispatch({ type: "shared", open: true });
15786
- if (isFleetKey(key, "/"))
16368
+ if (isPlainKey(key, "/"))
15787
16369
  return dispatch({ type: "focus", focus: "filter" });
15788
- if (isFleetKey(key, "0"))
16370
+ if (isPlainKey(key, "0"))
15789
16371
  return dispatch({ type: "layout", layout: "auto" });
15790
- if (isFleetKey(key, "1"))
16372
+ if (isPlainKey(key, "1"))
15791
16373
  return dispatch({ type: "layout", layout: "split" });
15792
- if (isFleetKey(key, "2"))
16374
+ if (isPlainKey(key, "2"))
15793
16375
  return dispatch({ type: "layout", layout: "stack" });
15794
16376
  if (key.name === "tab") {
16377
+ const focusOrder = ["stacks", "services", "logs"];
15795
16378
  const index = focusOrder.indexOf(current.focus);
15796
16379
  const delta2 = key.shift ? -1 : 1;
15797
16380
  const next = (index + delta2 + focusOrder.length) % focusOrder.length;
15798
16381
  return dispatch({ type: "focus", focus: focusOrder[next] });
15799
16382
  }
15800
- if (isFleetKey(key, "["))
16383
+ if (isPlainKey(key, ","))
16384
+ return dispatch({ type: "move-stack", delta: -1, snapshot });
16385
+ if (isPlainKey(key, "."))
16386
+ return dispatch({ type: "move-stack", delta: 1, snapshot });
16387
+ if (isPlainKey(key, "["))
15801
16388
  return dispatch({ type: "move-service", delta: -1, snapshot });
15802
- if (isFleetKey(key, "]"))
16389
+ if (isPlainKey(key, "]"))
15803
16390
  return dispatch({ type: "move-service", delta: 1, snapshot });
15804
- if (isFleetKey(key, "g") && key.shift || key.sequence === "G") {
16391
+ if (isPlainKey(key, "f"))
16392
+ return dispatch({ type: "follow", follow: !current.follow });
16393
+ if (isFollowBottomKey(key))
15805
16394
  return dispatch({ type: "follow", follow: true });
16395
+ if (isScrollTopKey(key)) {
16396
+ return dispatch({ type: "scroll-logs", delta: -logRows.length, maxOffset: maxLogOffset });
15806
16397
  }
15807
- if (key.sequence === "D" || isFleetKey(key, "d") && key.shift) {
16398
+ if (isDoctorKey(key)) {
15808
16399
  if (doctorRunning)
15809
16400
  return;
15810
16401
  dispatch({ type: "doctor", open: true });
@@ -15812,40 +16403,52 @@ function FleetApp({
15812
16403
  setDoctorRows([]);
15813
16404
  const worktree = stack !== undefined && existsSync23(stack.worktree) ? stack.worktree : process.cwd();
15814
16405
  source.diagnose(worktree).then(setDoctorRows).catch((error) => {
15815
- setNotice(`doctor failed: ${error.message}`);
16406
+ showToast(`doctor failed: ${error.message}`);
15816
16407
  }).finally(() => setDoctorRunning(false));
15817
16408
  return;
15818
16409
  }
15819
- if (isFleetKey(key, "d")) {
16410
+ if (isPlainKey(key, "d")) {
15820
16411
  if (stack?.phase === "queued" || stack?.phase === "reserved") {
15821
- setNotice(`${stack.project} is ${stack.phase}; down is available after startup begins`);
16412
+ showToast(`${stack.project} is ${stack.phase}; down is available after startup begins`);
15822
16413
  } else if (stack !== undefined) {
15823
16414
  dispatch({ type: "confirm-down", stack });
15824
16415
  }
15825
16416
  return;
15826
16417
  }
15827
- if (isFleetKey(key, "o") && endpoint !== undefined) {
16418
+ if (isPlainKey(key, "o") && endpoint !== undefined) {
15828
16419
  openFleetUrl(endpoint);
15829
- setNotice(endpoint);
16420
+ showToast(endpoint);
15830
16421
  return;
15831
16422
  }
15832
- if (isFleetKey(key, "y") && endpoint !== undefined) {
15833
- const copied = renderer.copyToClipboardOSC52(endpoint);
15834
- setNotice(copied ? `copied ${endpoint}` : `copy unavailable: ${endpoint}`);
16423
+ if (isShiftedKey(key, "y")) {
16424
+ if (stack === undefined)
16425
+ return;
16426
+ const env = buildEnvBlock(stack);
16427
+ if (env === "")
16428
+ showToast(`no env surface recorded for ${stack.project}`);
16429
+ else
16430
+ copyToClipboard(env, `env block (${env.split(`
16431
+ `).length} keys)`);
15835
16432
  return;
15836
16433
  }
15837
- const copySurfaceRequested = ["c", "l", "p"].some((name) => isFleetKey(key, name));
15838
- const surface = isFleetKey(key, "c") ? endpointView?.url : isFleetKey(key, "l") ? endpointView?.localUrl : isFleetKey(key, "p") ? endpointView?.publicUrl : undefined;
15839
- if (surface !== undefined) {
15840
- const copied = renderer.copyToClipboardOSC52(surface);
15841
- setNotice(copied ? `copied ${surface}` : `copy unavailable: ${surface}`);
16434
+ if (isPlainKey(key, "y") && endpoint !== undefined) {
16435
+ copyToClipboard(endpoint);
15842
16436
  return;
15843
16437
  }
15844
- if (copySurfaceRequested) {
15845
- setNotice("selected URL surface is unavailable");
16438
+ const copySurfaces = [
16439
+ ["c", endpointView?.url],
16440
+ ["l", endpointView?.localUrl],
16441
+ ["p", endpointView?.publicUrl]
16442
+ ];
16443
+ const copySurface = copySurfaces.find(([name]) => isPlainKey(key, name));
16444
+ if (copySurface !== undefined) {
16445
+ if (copySurface[1] !== undefined)
16446
+ copyToClipboard(copySurface[1]);
16447
+ else
16448
+ showToast("selected URL surface is unavailable");
15846
16449
  return;
15847
16450
  }
15848
- const delta = key.name === "down" || isFleetKey(key, "j") ? 1 : key.name === "up" || isFleetKey(key, "k") ? -1 : 0;
16451
+ const delta = isDownKey(key) ? 1 : isUpKey(key) ? -1 : 0;
15849
16452
  if (delta === 0)
15850
16453
  return;
15851
16454
  if (current.focus === "stacks")
@@ -15853,11 +16456,46 @@ function FleetApp({
15853
16456
  else if (current.focus === "services")
15854
16457
  dispatch({ type: "move-service", delta, snapshot });
15855
16458
  else
15856
- dispatch({ type: "scroll-logs", delta });
16459
+ dispatch({ type: "scroll-logs", delta, maxOffset: maxLogOffset });
15857
16460
  });
15858
- const capacity = snapshot.capacity;
15859
- const status = connected ? `${capacity.live}/${capacity.maxStacks} live \xB7 ${capacity.reserved} reserved \xB7 ${capacity.queued} queued` : "disconnected";
16461
+ const uptime = stack?.createdAt !== undefined && (stack.phase === "up" || stack.phase === "degraded" || stack.phase === "starting") ? formatUptime(stack.createdAt, now) : undefined;
15860
16462
  const logLabel = state.selection.service ?? "select a service";
16463
+ const statusNotice = resolveStatusNotice(connectionNotice, toast);
16464
+ const doctorWidth = Math.min(72, Math.max(30, terminal.width - 6));
16465
+ const wideFooter = terminal.width >= 100;
16466
+ const onSelectProject = useCallback((project) => dispatch({ type: "select-stack", project, snapshot }), [snapshot]);
16467
+ const onSelectService = useCallback((service) => dispatch({ type: "select-service", service, snapshot }), [snapshot]);
16468
+ const onSelectEndpoint = useCallback((service, endpointName) => dispatch({ type: "select-endpoint", service, endpoint: endpointName, snapshot }), [snapshot]);
16469
+ const servicePane = /* @__PURE__ */ jsxDEV5(ServicePane, {
16470
+ stack,
16471
+ uptime,
16472
+ selectedService: state.selection.service,
16473
+ selectedEndpoint: state.selection.endpoint,
16474
+ width: logWidth,
16475
+ focused: state.focus === "services",
16476
+ onSelectService,
16477
+ onSelectEndpoint
16478
+ }, undefined, false, undefined, this);
16479
+ const sidebar = (width) => /* @__PURE__ */ jsxDEV5(StackSidebar, {
16480
+ stacks,
16481
+ capacity: snapshot.capacity,
16482
+ selectedProject: state.selection.project,
16483
+ width,
16484
+ focused: state.focus === "stacks",
16485
+ spinnerFrame,
16486
+ onSelectProject
16487
+ }, undefined, false, undefined, this);
16488
+ const logPane = /* @__PURE__ */ jsxDEV5(LogPane, {
16489
+ rows: logRows,
16490
+ height: logHeight,
16491
+ width: logWidth,
16492
+ offset: state.logOffset,
16493
+ follow: state.follow,
16494
+ unseen: state.unseenLines,
16495
+ label: logLabel,
16496
+ focused: state.focus === "logs",
16497
+ onScroll: (delta) => dispatch({ type: "scroll-logs", delta, maxOffset: maxLogOffset })
16498
+ }, undefined, false, undefined, this);
15861
16499
  return /* @__PURE__ */ jsxDEV5("box", {
15862
16500
  style: { width: "100%", height: "100%", flexDirection: "column", backgroundColor: fleetTheme.background },
15863
16501
  children: [
@@ -15866,70 +16504,64 @@ function FleetApp({
15866
16504
  children: [
15867
16505
  /* @__PURE__ */ jsxDEV5("text", {
15868
16506
  fg: fleetTheme.accent,
15869
- children: [
15870
- "Hestia Fleet \u2014 ",
15871
- context.repo
15872
- ]
15873
- }, undefined, true, undefined, this),
15874
- /* @__PURE__ */ jsxDEV5("text", {
15875
- fg: fleetTheme.muted,
15876
- children: [
15877
- " ",
15878
- status
15879
- ]
15880
- }, undefined, true, undefined, this)
15881
- ]
15882
- }, undefined, true, undefined, this),
15883
- terminal.width >= 90 ? /* @__PURE__ */ jsxDEV5("box", {
15884
- style: { height: 4, paddingLeft: 1, flexDirection: "column", backgroundColor: fleetTheme.background },
15885
- children: [
15886
- /* @__PURE__ */ jsxDEV5("text", {
15887
- fg: fleetTheme.text,
15888
- children: [
15889
- "Repository ",
15890
- context.repo
15891
- ]
15892
- }, undefined, true, undefined, this),
16507
+ children: "Hestia Fleet"
16508
+ }, undefined, false, undefined, this),
15893
16509
  /* @__PURE__ */ jsxDEV5("text", {
15894
- fg: fleetTheme.text,
15895
- children: [
15896
- "Branch ",
15897
- context.branch
15898
- ]
15899
- }, undefined, true, undefined, this),
16510
+ fg: fleetTheme.faint,
16511
+ children: " \xB7 "
16512
+ }, undefined, false, undefined, this),
15900
16513
  /* @__PURE__ */ jsxDEV5("text", {
15901
16514
  fg: fleetTheme.text,
15902
- children: [
15903
- "Worktree ",
15904
- middleTruncateWorktreePath(context.worktree, Math.max(16, terminal.width - 13))
15905
- ]
15906
- }, undefined, true, undefined, this),
16515
+ children: fitFleetText(context.repo, Math.max(8, terminal.width - 30))
16516
+ }, undefined, false, undefined, this),
16517
+ /* @__PURE__ */ jsxDEV5("box", {
16518
+ style: { flexGrow: 1 }
16519
+ }, undefined, false, undefined, this),
15907
16520
  /* @__PURE__ */ jsxDEV5("text", {
15908
- fg: fleetTheme.muted,
15909
- children: [
15910
- "Project ",
15911
- context.project
15912
- ]
15913
- }, undefined, true, undefined, this)
16521
+ fg: connected ? fleetTheme.healthy : fleetTheme.warning,
16522
+ children: connected ? "\u25CF connected" : "\u25CB reconnecting"
16523
+ }, undefined, false, undefined, this)
15914
16524
  ]
15915
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV5("box", {
16525
+ }, undefined, true, undefined, this),
16526
+ /* @__PURE__ */ jsxDEV5("box", {
15916
16527
  style: { height: 2, paddingLeft: 1, flexDirection: "column", backgroundColor: fleetTheme.background },
15917
16528
  children: [
15918
- /* @__PURE__ */ jsxDEV5("text", {
15919
- fg: fleetTheme.text,
16529
+ /* @__PURE__ */ jsxDEV5("box", {
16530
+ style: { height: 1, flexDirection: "row" },
15920
16531
  children: [
15921
- context.repo,
15922
- " \xB7 ",
15923
- context.branch,
15924
- " \xB7 ",
15925
- context.project
16532
+ /* @__PURE__ */ jsxDEV5("text", {
16533
+ fg: fleetTheme.muted,
16534
+ children: fitFleetText(context.repo, 24)
16535
+ }, undefined, false, undefined, this),
16536
+ /* @__PURE__ */ jsxDEV5("text", {
16537
+ fg: fleetTheme.faint,
16538
+ children: " / "
16539
+ }, undefined, false, undefined, this),
16540
+ /* @__PURE__ */ jsxDEV5("text", {
16541
+ fg: fleetTheme.text,
16542
+ children: context.branch
16543
+ }, undefined, false, undefined, this),
16544
+ /* @__PURE__ */ jsxDEV5("text", {
16545
+ fg: fleetTheme.faint,
16546
+ children: " \xB7 "
16547
+ }, undefined, false, undefined, this),
16548
+ /* @__PURE__ */ jsxDEV5("text", {
16549
+ fg: fleetTheme.muted,
16550
+ children: context.project
16551
+ }, undefined, false, undefined, this)
15926
16552
  ]
15927
16553
  }, undefined, true, undefined, this),
15928
- /* @__PURE__ */ jsxDEV5("text", {
15929
- fg: fleetTheme.muted,
16554
+ /* @__PURE__ */ jsxDEV5("box", {
16555
+ style: { height: 1, flexDirection: "row" },
15930
16556
  children: [
15931
- "worktree ",
15932
- middleTruncateWorktreePath(context.worktree, Math.max(12, terminal.width - 10))
16557
+ /* @__PURE__ */ jsxDEV5("text", {
16558
+ fg: fleetTheme.faint,
16559
+ children: "worktree "
16560
+ }, undefined, false, undefined, this),
16561
+ /* @__PURE__ */ jsxDEV5("text", {
16562
+ fg: fleetTheme.muted,
16563
+ children: middleTruncateWorktreePath(context.worktree, Math.max(16, terminal.width - 11))
16564
+ }, undefined, false, undefined, this)
15933
16565
  ]
15934
16566
  }, undefined, true, undefined, this)
15935
16567
  ]
@@ -15937,37 +16569,17 @@ function FleetApp({
15937
16569
  effectiveLayout === "split" ? /* @__PURE__ */ jsxDEV5("box", {
15938
16570
  style: { flexGrow: 1, flexDirection: "row" },
15939
16571
  children: [
15940
- /* @__PURE__ */ jsxDEV5(StackSidebar, {
15941
- stacks,
15942
- selectedProject: state.selection.project,
15943
- width: Math.max(28, Math.floor(terminal.width * 0.28)),
15944
- onSelectProject: (project) => dispatch({ type: "select-stack", project, snapshot })
15945
- }, undefined, false, undefined, this),
16572
+ sidebar(paneWidths.sidebar),
15946
16573
  /* @__PURE__ */ jsxDEV5("box", {
15947
16574
  style: { flexGrow: 1, flexDirection: "column" },
15948
16575
  children: [
15949
16576
  /* @__PURE__ */ jsxDEV5("box", {
15950
- style: { height: Math.min(12, Math.max(5, fleetServiceRowCount(stack) + 2)) },
15951
- children: /* @__PURE__ */ jsxDEV5(ServicePane, {
15952
- stack,
15953
- selectedService: state.selection.service,
15954
- selectedEndpoint: state.selection.endpoint,
15955
- onSelectService: (service) => dispatch({ type: "select-service", service, snapshot }),
15956
- onSelectEndpoint: (service, endpoint2) => dispatch({ type: "select-endpoint", service, endpoint: endpoint2, snapshot })
15957
- }, undefined, false, undefined, this)
16577
+ style: { height: svcPaneH },
16578
+ children: servicePane
15958
16579
  }, undefined, false, undefined, this),
15959
16580
  /* @__PURE__ */ jsxDEV5("box", {
15960
16581
  style: { flexGrow: 1 },
15961
- children: /* @__PURE__ */ jsxDEV5(LogPane, {
15962
- lines: logs,
15963
- height: Math.max(5, terminal.height - 13),
15964
- width: Math.max(20, Math.floor(terminal.width * 0.72)),
15965
- offset: state.logOffset,
15966
- follow: state.follow,
15967
- unseen: state.unseenLines,
15968
- label: logLabel,
15969
- onScroll: (delta) => dispatch({ type: "scroll-logs", delta })
15970
- }, undefined, false, undefined, this)
16582
+ children: logPane
15971
16583
  }, undefined, false, undefined, this)
15972
16584
  ]
15973
16585
  }, undefined, true, undefined, this)
@@ -15976,42 +16588,22 @@ function FleetApp({
15976
16588
  style: { flexGrow: 1, flexDirection: "column" },
15977
16589
  children: [
15978
16590
  /* @__PURE__ */ jsxDEV5("box", {
15979
- style: { height: Math.min(7, Math.max(4, stacks.length + 2)) },
15980
- children: /* @__PURE__ */ jsxDEV5(StackSidebar, {
15981
- stacks,
15982
- selectedProject: state.selection.project,
15983
- width: terminal.width,
15984
- onSelectProject: (project) => dispatch({ type: "select-stack", project, snapshot })
15985
- }, undefined, false, undefined, this)
16591
+ style: { height: stackedSidebarHeight },
16592
+ children: sidebar(terminal.width)
15986
16593
  }, undefined, false, undefined, this),
15987
16594
  /* @__PURE__ */ jsxDEV5("box", {
15988
- style: { height: Math.min(10, Math.max(4, fleetServiceRowCount(stack) + 2)) },
15989
- children: /* @__PURE__ */ jsxDEV5(ServicePane, {
15990
- stack,
15991
- selectedService: state.selection.service,
15992
- selectedEndpoint: state.selection.endpoint,
15993
- onSelectService: (service) => dispatch({ type: "select-service", service, snapshot }),
15994
- onSelectEndpoint: (service, endpoint2) => dispatch({ type: "select-endpoint", service, endpoint: endpoint2, snapshot })
15995
- }, undefined, false, undefined, this)
16595
+ style: { height: svcPaneH },
16596
+ children: servicePane
15996
16597
  }, undefined, false, undefined, this),
15997
16598
  /* @__PURE__ */ jsxDEV5("box", {
15998
16599
  style: { flexGrow: 1 },
15999
- children: /* @__PURE__ */ jsxDEV5(LogPane, {
16000
- lines: logs,
16001
- height: Math.max(5, terminal.height - 17),
16002
- width: terminal.width,
16003
- offset: state.logOffset,
16004
- follow: state.follow,
16005
- unseen: state.unseenLines,
16006
- label: logLabel,
16007
- onScroll: (delta) => dispatch({ type: "scroll-logs", delta })
16008
- }, undefined, false, undefined, this)
16600
+ children: logPane
16009
16601
  }, undefined, false, undefined, this)
16010
16602
  ]
16011
16603
  }, undefined, true, undefined, this),
16012
16604
  /* @__PURE__ */ jsxDEV5("box", {
16013
16605
  style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
16014
- children: state.focus === "filter" ? /* @__PURE__ */ jsxDEV5(Fragment2, {
16606
+ children: state.focus === "filter" ? /* @__PURE__ */ jsxDEV5(Fragment3, {
16015
16607
  children: [
16016
16608
  /* @__PURE__ */ jsxDEV5("text", {
16017
16609
  fg: fleetTheme.accent,
@@ -16026,76 +16618,106 @@ function FleetApp({
16026
16618
  onSubmit: () => dispatch({ type: "focus", focus: "stacks" })
16027
16619
  }, undefined, false, undefined, this)
16028
16620
  ]
16029
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV5("text", {
16030
- fg: notice ? fleetTheme.warning : fleetTheme.muted,
16031
- children: notice || "click rows \xB7 wheel logs \xB7 j/k navigate \xB7 [/] service \xB7 / filter \xB7 d down \xB7 ? help \xB7 q quit"
16032
- }, undefined, false, undefined, this)
16621
+ }, undefined, true, undefined, this) : statusNotice !== undefined ? /* @__PURE__ */ jsxDEV5(Fragment3, {
16622
+ children: [
16623
+ /* @__PURE__ */ jsxDEV5("text", {
16624
+ fg: connectionNotice !== undefined ? fleetTheme.warning : fleetTheme.text,
16625
+ children: fitFleetText(statusNotice, Math.max(8, terminal.width - 20))
16626
+ }, undefined, false, undefined, this),
16627
+ /* @__PURE__ */ jsxDEV5("box", {
16628
+ style: { flexGrow: 1 }
16629
+ }, undefined, false, undefined, this),
16630
+ /* @__PURE__ */ jsxDEV5("text", {
16631
+ fg: connected ? fleetTheme.muted : fleetTheme.warning,
16632
+ children: fleetCapacitySummary(snapshot.capacity, connected)
16633
+ }, undefined, false, undefined, this)
16634
+ ]
16635
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV5(Fragment3, {
16636
+ children: [
16637
+ wideFooter ? /* @__PURE__ */ jsxDEV5("text", {
16638
+ children: FLEET_KEY_HINTS.map((hint) => /* @__PURE__ */ jsxDEV5("span", {
16639
+ children: [
16640
+ /* @__PURE__ */ jsxDEV5("span", {
16641
+ fg: fleetTheme.bright,
16642
+ bg: fleetTheme.keycapBg,
16643
+ children: ` ${hint.keys} `
16644
+ }, undefined, false, undefined, this),
16645
+ /* @__PURE__ */ jsxDEV5("span", {
16646
+ fg: fleetTheme.muted,
16647
+ children: ` ${hint.label} `
16648
+ }, undefined, false, undefined, this)
16649
+ ]
16650
+ }, hint.keys, true, undefined, this))
16651
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5("text", {
16652
+ fg: fleetTheme.muted,
16653
+ children: "? help \xB7 q quit"
16654
+ }, undefined, false, undefined, this),
16655
+ /* @__PURE__ */ jsxDEV5("box", {
16656
+ style: { flexGrow: 1 }
16657
+ }, undefined, false, undefined, this),
16658
+ state.filter !== "" ? /* @__PURE__ */ jsxDEV5("text", {
16659
+ fg: fleetTheme.accent,
16660
+ children: `filter=${fitFleetText(state.filter, 16)} `
16661
+ }, undefined, false, undefined, this) : null,
16662
+ /* @__PURE__ */ jsxDEV5("text", {
16663
+ fg: connected ? fleetTheme.muted : fleetTheme.warning,
16664
+ children: fleetCapacitySummary(snapshot.capacity, connected)
16665
+ }, undefined, false, undefined, this)
16666
+ ]
16667
+ }, undefined, true, undefined, this)
16033
16668
  }, undefined, false, undefined, this),
16034
16669
  state.helpOpen ? /* @__PURE__ */ jsxDEV5(FleetModal, {
16035
16670
  title: "Fleet help",
16036
16671
  terminalWidth: terminal.width,
16037
16672
  terminalHeight: terminal.height,
16038
- children: [
16039
- /* @__PURE__ */ jsxDEV5("text", {
16040
- fg: fleetTheme.text,
16041
- children: "j/k or arrows navigate focused pane"
16042
- }, undefined, false, undefined, this),
16043
- /* @__PURE__ */ jsxDEV5("text", {
16044
- fg: fleetTheme.text,
16045
- children: "mouse click stacks/services \xB7 wheel scroll logs"
16046
- }, undefined, false, undefined, this),
16047
- /* @__PURE__ */ jsxDEV5("text", {
16048
- fg: fleetTheme.text,
16049
- children: "Tab / Shift-Tab change pane"
16050
- }, undefined, false, undefined, this),
16051
- /* @__PURE__ */ jsxDEV5("text", {
16052
- fg: fleetTheme.text,
16053
- children: "[ / ] previous / next service"
16054
- }, undefined, false, undefined, this),
16055
- /* @__PURE__ */ jsxDEV5("text", {
16056
- fg: fleetTheme.text,
16057
- children: "/ filter \xB7 G follow logs \xB7 0/1/2 layout"
16058
- }, undefined, false, undefined, this),
16059
- /* @__PURE__ */ jsxDEV5("text", {
16060
- fg: fleetTheme.text,
16061
- children: "o open \xB7 y primary \xB7 c direct \xB7 l local \xB7 p public"
16062
- }, undefined, false, undefined, this),
16063
- /* @__PURE__ */ jsxDEV5("text", {
16064
- fg: fleetTheme.text,
16065
- children: "D doctor \xB7 s shared hostnames (claim / allow / deny / release)"
16066
- }, undefined, false, undefined, this),
16067
- /* @__PURE__ */ jsxDEV5("text", {
16068
- fg: fleetTheme.danger,
16069
- children: "d confirmed down (named volumes retained)"
16070
- }, undefined, false, undefined, this),
16071
- /* @__PURE__ */ jsxDEV5("text", {
16072
- fg: fleetTheme.muted,
16073
- children: "Esc closes this dialog"
16074
- }, undefined, false, undefined, this)
16075
- ]
16076
- }, undefined, true, undefined, this) : null,
16673
+ children: HELP_ROWS.map(([keys, action]) => /* @__PURE__ */ jsxDEV5("box", {
16674
+ style: { height: 1, flexDirection: "row" },
16675
+ children: [
16676
+ /* @__PURE__ */ jsxDEV5("text", {
16677
+ fg: fleetTheme.accent,
16678
+ children: padFleetText(keys, 9)
16679
+ }, undefined, false, undefined, this),
16680
+ /* @__PURE__ */ jsxDEV5("text", {
16681
+ fg: fleetTheme.muted,
16682
+ children: action
16683
+ }, undefined, false, undefined, this)
16684
+ ]
16685
+ }, keys, true, undefined, this))
16686
+ }, undefined, false, undefined, this) : null,
16077
16687
  state.doctorOpen ? /* @__PURE__ */ jsxDEV5(FleetModal, {
16078
16688
  title: "hestia doctor",
16079
16689
  terminalWidth: terminal.width,
16080
16690
  terminalHeight: terminal.height,
16081
- children: [
16082
- doctorRunning ? /* @__PURE__ */ jsxDEV5("text", {
16083
- fg: fleetTheme.muted,
16084
- children: "Running diagnostics\u2026"
16085
- }, undefined, false, undefined, this) : doctorRows.slice(0, 10).map((row2) => /* @__PURE__ */ jsxDEV5("text", {
16086
- fg: row2.level === "error" ? fleetTheme.danger : row2.level === "warn" ? fleetTheme.warning : fleetTheme.text,
16087
- children: `${row2.level === "ok" ? "\u2713" : row2.level === "error" ? "\u2717" : "!"} ${sanitizeFleetTerminalText(row2.check)} \u2014 ${sanitizeFleetTerminalText(row2.detail)}`
16088
- }, row2.check, false, undefined, this)),
16089
- !doctorRunning && doctorOmissionSummary(doctorRows) !== undefined ? /* @__PURE__ */ jsxDEV5("text", {
16090
- fg: fleetTheme.warning,
16091
- children: doctorOmissionSummary(doctorRows)
16092
- }, undefined, false, undefined, this) : null,
16093
- /* @__PURE__ */ jsxDEV5("text", {
16094
- fg: fleetTheme.muted,
16095
- children: "Esc closes this report"
16096
- }, undefined, false, undefined, this)
16097
- ]
16098
- }, undefined, true, undefined, this) : null,
16691
+ children: doctorRunning ? /* @__PURE__ */ jsxDEV5("text", {
16692
+ fg: fleetTheme.muted,
16693
+ children: "Running diagnostics\u2026"
16694
+ }, undefined, false, undefined, this) : (() => {
16695
+ const modalHeight = Math.min(22, Math.max(8, terminal.height - 4));
16696
+ const report = budgetDoctorReport(doctorRows, doctorWidth, Math.max(3, modalHeight - 6));
16697
+ const summary = doctorOmissionSummary(doctorRows, report.shown);
16698
+ return [
16699
+ ...report.entries.flatMap(({ row: row2, detailLines }) => {
16700
+ const mark = row2.level === "ok" ? "\u2713" : row2.level === "error" ? "\u2717" : row2.level === "warn" ? "!" : "?";
16701
+ const color = row2.level === "error" ? fleetTheme.danger : row2.level === "warn" ? fleetTheme.warning : row2.level === "ok" ? fleetTheme.healthy : fleetTheme.muted;
16702
+ const head = `${mark} ${sanitizeFleetTerminalText(row2.check)}`;
16703
+ return [
16704
+ /* @__PURE__ */ jsxDEV5("text", {
16705
+ fg: color,
16706
+ children: fitFleetText(head, doctorWidth - 4)
16707
+ }, `${row2.check}:head`, false, undefined, this),
16708
+ ...detailLines.map((part, index) => /* @__PURE__ */ jsxDEV5("text", {
16709
+ fg: fleetTheme.muted,
16710
+ children: ` ${part}`
16711
+ }, `${row2.check}:detail:${index}`, false, undefined, this))
16712
+ ];
16713
+ }),
16714
+ summary === undefined ? null : /* @__PURE__ */ jsxDEV5("text", {
16715
+ fg: fleetTheme.warning,
16716
+ children: summary
16717
+ }, "doctor:omitted", false, undefined, this)
16718
+ ];
16719
+ })()
16720
+ }, undefined, false, undefined, this) : null,
16099
16721
  state.sharedOpen ? /* @__PURE__ */ jsxDEV5(FleetModal, {
16100
16722
  title: "Shared hostnames",
16101
16723
  terminalWidth: terminal.width,
@@ -16112,7 +16734,7 @@ function FleetApp({
16112
16734
  children: [
16113
16735
  /* @__PURE__ */ jsxDEV5("text", {
16114
16736
  fg: active ? fleetTheme.accent : fleetTheme.text,
16115
- children: `${active ? "\u25B8 " : " "}${sanitizeFleetTerminalText(entry.name)} ${sanitizeFleetTerminalText(entry.url)} ${holder}`
16737
+ children: `${active ? "\u258E" : " "} ${sanitizeFleetTerminalText(entry.name)} ${sanitizeFleetTerminalText(entry.url)} ${holder}`
16116
16738
  }, undefined, false, undefined, this),
16117
16739
  entry.queue.map((waiter, position) => /* @__PURE__ */ jsxDEV5("text", {
16118
16740
  fg: fleetTheme.muted,
@@ -16135,32 +16757,56 @@ function FleetApp({
16135
16757
  terminalWidth: terminal.width,
16136
16758
  terminalHeight: terminal.height,
16137
16759
  children: [
16138
- /* @__PURE__ */ jsxDEV5("text", {
16139
- fg: fleetTheme.text,
16760
+ /* @__PURE__ */ jsxDEV5("box", {
16761
+ style: { height: 1, flexDirection: "row" },
16140
16762
  children: [
16141
- "Branch: ",
16142
- sanitizeFleetTerminalText(state.confirmDown.branch)
16763
+ /* @__PURE__ */ jsxDEV5("text", {
16764
+ fg: fleetTheme.faint,
16765
+ children: padFleetText("Branch", 12)
16766
+ }, undefined, false, undefined, this),
16767
+ /* @__PURE__ */ jsxDEV5("text", {
16768
+ fg: fleetTheme.bright,
16769
+ children: sanitizeFleetTerminalText(state.confirmDown.branch)
16770
+ }, undefined, false, undefined, this)
16143
16771
  ]
16144
16772
  }, undefined, true, undefined, this),
16145
- /* @__PURE__ */ jsxDEV5("text", {
16146
- fg: fleetTheme.text,
16773
+ /* @__PURE__ */ jsxDEV5("box", {
16774
+ style: { height: 1, flexDirection: "row" },
16147
16775
  children: [
16148
- "Repository: ",
16149
- sanitizeFleetTerminalText(state.confirmDown.repo)
16776
+ /* @__PURE__ */ jsxDEV5("text", {
16777
+ fg: fleetTheme.faint,
16778
+ children: padFleetText("Repository", 12)
16779
+ }, undefined, false, undefined, this),
16780
+ /* @__PURE__ */ jsxDEV5("text", {
16781
+ fg: fleetTheme.text,
16782
+ children: sanitizeFleetTerminalText(state.confirmDown.repo)
16783
+ }, undefined, false, undefined, this)
16150
16784
  ]
16151
16785
  }, undefined, true, undefined, this),
16152
- /* @__PURE__ */ jsxDEV5("text", {
16153
- fg: fleetTheme.text,
16786
+ /* @__PURE__ */ jsxDEV5("box", {
16787
+ style: { height: 1, flexDirection: "row" },
16154
16788
  children: [
16155
- "Worktree: ",
16156
- sanitizeFleetTerminalText(state.confirmDown.worktree)
16789
+ /* @__PURE__ */ jsxDEV5("text", {
16790
+ fg: fleetTheme.faint,
16791
+ children: padFleetText("Worktree", 12)
16792
+ }, undefined, false, undefined, this),
16793
+ /* @__PURE__ */ jsxDEV5("text", {
16794
+ fg: fleetTheme.text,
16795
+ children: sanitizeFleetTerminalText(state.confirmDown.worktree)
16796
+ }, undefined, false, undefined, this)
16157
16797
  ]
16158
16798
  }, undefined, true, undefined, this),
16159
- /* @__PURE__ */ jsxDEV5("text", {
16160
- fg: fleetTheme.text,
16799
+ /* @__PURE__ */ jsxDEV5("box", {
16800
+ style: { height: 1, flexDirection: "row" },
16161
16801
  children: [
16162
- "Project: ",
16163
- sanitizeFleetTerminalText(state.confirmDown.project)
16802
+ /* @__PURE__ */ jsxDEV5("text", {
16803
+ fg: fleetTheme.faint,
16804
+ children: padFleetText("Project", 12)
16805
+ }, undefined, false, undefined, this),
16806
+ /* @__PURE__ */ jsxDEV5("text", {
16807
+ fg: fleetTheme.text,
16808
+ children: sanitizeFleetTerminalText(state.confirmDown.project)
16809
+ }, undefined, false, undefined, this)
16164
16810
  ]
16165
16811
  }, undefined, true, undefined, this),
16166
16812
  /* @__PURE__ */ jsxDEV5("box", {
@@ -16179,15 +16825,33 @@ function FleetApp({
16179
16825
  ]
16180
16826
  }, undefined, true, undefined, this);
16181
16827
  }
16182
- var LOG_RING_CAPACITY = 2000, EMPTY_CAPACITY;
16828
+ var LOG_RING_CAPACITY = 2000, TOAST_TTL_MS = 3500, EMPTY_CAPACITY, HELP_ROWS;
16183
16829
  var init_FleetApp = __esm(() => {
16830
+ init_fleet_controller();
16184
16831
  init_terminal_text();
16832
+ init_fleet_text();
16185
16833
  init_fleet_theme();
16834
+ init_fleet_log_rows();
16835
+ init_fleet_format();
16836
+ init_fleet_status();
16186
16837
  init_StackSidebar();
16187
16838
  init_ServicePane();
16188
16839
  init_LogPane();
16189
16840
  init_FleetModal();
16190
16841
  EMPTY_CAPACITY = { maxStacks: 0, live: 0, reserved: 0, queued: 0 };
16842
+ HELP_ROWS = [
16843
+ ["j k \u2191\u2193", "move in focused pane \xB7 Tab cycles panes"],
16844
+ [", . [ ]", "previous / next stack \xB7 service"],
16845
+ ["/", "filter stacks (Esc clears, then exits)"],
16846
+ ["f \xB7 g G", "pause/resume follow \xB7 top / bottom of logs"],
16847
+ ["o", "open selected endpoint in browser"],
16848
+ ["y \xB7 Y", "yank endpoint URL \xB7 stack env block"],
16849
+ ["c l p", "yank direct / local / public URL"],
16850
+ ["s", "shared hostnames (claim, allow, deny, release)"],
16851
+ ["D \xB7 d", "doctor report \xB7 down stack (volumes retained)"],
16852
+ ["1 2 0", "split / stacked / auto layout"],
16853
+ ["q", "quit \xB7 mouse click and wheel work everywhere"]
16854
+ ];
16191
16855
  });
16192
16856
 
16193
16857
  // packages/tui/src/job-control.ts
@@ -16338,17 +17002,18 @@ __export(exports_src, {
16338
17002
  var init_src3 = __esm(() => {
16339
17003
  init_runtime();
16340
17004
  init_fleet_source();
17005
+ init_fleet_controller();
16341
17006
  init_terminal_text();
16342
17007
  });
16343
17008
 
16344
17009
  // packages/cli/src/index.ts
16345
17010
  init_src2();
16346
17011
  init_src();
16347
- import { execFileSync as execFileSync4 } from "child_process";
17012
+ import { execFileSync as execFileSync5 } from "child_process";
16348
17013
  import { existsSync as existsSync24 } from "fs";
16349
- import { dirname as dirname8, join as join25 } from "path";
17014
+ import { dirname as dirname8, join as join26 } from "path";
16350
17015
  import { fileURLToPath as fileURLToPath3 } from "url";
16351
- var CLI_VERSION = "1.1.0";
17016
+ var CLI_VERSION = "1.2.0";
16352
17017
  var PACKAGE_NAME = "@tridha643/hestia";
16353
17018
  function compareVersions(a, b) {
16354
17019
  const parse = (value) => {
@@ -16825,7 +17490,7 @@ async function main() {
16825
17490
  `);
16826
17491
  }
16827
17492
  try {
16828
- execFileSync4("bun", installArgs, { stdio: flags.json ? "ignore" : "inherit" });
17493
+ execFileSync5("bun", installArgs, { stdio: flags.json ? "ignore" : "inherit" });
16829
17494
  } catch (error) {
16830
17495
  fail("upgrade-failed", `\`bun ${installArgs.join(" ")}\` failed: ${error.message}`, flags.json);
16831
17496
  }
@@ -16842,8 +17507,8 @@ async function main() {
16842
17507
  fail("usage", "usage: hestia skill path", flags.json);
16843
17508
  const packageRoot = dirname8(dirname8(fileURLToPath3(import.meta.url)));
16844
17509
  const candidates = [
16845
- join25(packageRoot, "skills", "hestia", "SKILL.md"),
16846
- join25(process.cwd(), "skills", "hestia", "SKILL.md")
17510
+ join26(packageRoot, "skills", "hestia", "SKILL.md"),
17511
+ join26(process.cwd(), "skills", "hestia", "SKILL.md")
16847
17512
  ];
16848
17513
  const path = candidates.find(existsSync24);
16849
17514
  if (path === undefined)
@@ -17378,8 +18043,8 @@ ${JSON.stringify(result.config, null, 2)}
17378
18043
  process.stdout.write(JSON.stringify(line) + `
17379
18044
  `);
17380
18045
  } else {
17381
- const text = line.meta ? `[hestia] ${line.text}` : line.text;
17382
- process.stdout.write(`${line.service.padEnd(nameWidth)} \u2502 ${text}
18046
+ const text2 = line.meta ? `[hestia] ${line.text}` : line.text;
18047
+ process.stdout.write(`${line.service.padEnd(nameWidth)} \u2502 ${text2}
17383
18048
  `);
17384
18049
  }
17385
18050
  }
@@ -17444,7 +18109,7 @@ ${JSON.stringify(result.config, null, 2)}
17444
18109
  } else if (sub === "stop") {
17445
18110
  if (launchdManagesThisHome()) {
17446
18111
  const uid = process.getuid?.() ?? 501;
17447
- execFileSync4("launchctl", ["bootout", `gui/${uid}/dev.hestia.daemon`]);
18112
+ execFileSync5("launchctl", ["bootout", `gui/${uid}/dev.hestia.daemon`]);
17448
18113
  process.stderr.write("warning: launchd agent booted out \u2014 `hestia daemon install` re-enables reboot revival\n");
17449
18114
  } else {
17450
18115
  await stopDaemonProcess();
@@ -17516,4 +18181,4 @@ export {
17516
18181
  compareVersions
17517
18182
  };
17518
18183
 
17519
- //# debugId=D1A8C01922C41BA164756E2164756E21
18184
+ //# debugId=E655EED0C9FC8A1364756E2164756E21