@kontextmind/kxm 0.7.177 → 0.7.178

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.
@@ -8880,7 +8880,7 @@ var require_public_api = __commonJS({
8880
8880
  }
8881
8881
  return doc;
8882
8882
  }
8883
- function parse12(src, reviver, options) {
8883
+ function parse13(src, reviver, options) {
8884
8884
  let _reviver = void 0;
8885
8885
  if (typeof reviver === "function") {
8886
8886
  _reviver = reviver;
@@ -8921,7 +8921,7 @@ var require_public_api = __commonJS({
8921
8921
  return value.toString(options);
8922
8922
  return new Document.Document(value, _replacer, options).toString(options);
8923
8923
  }
8924
- exports.parse = parse12;
8924
+ exports.parse = parse13;
8925
8925
  exports.parseAllDocuments = parseAllDocuments;
8926
8926
  exports.parseDocument = parseDocument3;
8927
8927
  exports.stringify = stringify12;
@@ -9184,6 +9184,7 @@ function loadKxmConfig(repoRoot = process.cwd(), options = {}) {
9184
9184
  const userConfigFile = join3(userDir, "config.yaml");
9185
9185
  const repoDir = repoConfigDirectory(repoRoot);
9186
9186
  const repoConfigFile = join3(repoDir, "config.yaml");
9187
+ const includeProject = options.layers !== "user";
9187
9188
  let userRaw = {};
9188
9189
  let userLoadedPath;
9189
9190
  if (existsSync3(userConfigFile)) {
@@ -9197,7 +9198,7 @@ function loadKxmConfig(repoRoot = process.cwd(), options = {}) {
9197
9198
  }
9198
9199
  let repoRaw = {};
9199
9200
  let repoLoadedPath;
9200
- if (existsSync3(repoConfigFile)) {
9201
+ if (includeProject && existsSync3(repoConfigFile)) {
9201
9202
  try {
9202
9203
  const text = readFileSync2(repoConfigFile, "utf8");
9203
9204
  repoRaw = (0, import_yaml.parse)(text) ?? {};
@@ -14043,7 +14044,7 @@ var require_fast_uri = __commonJS({
14043
14044
  normalizeString(uri, options);
14044
14045
  } else if (typeof uri === "object") {
14045
14046
  uri = /** @type {T} */
14046
- parse12(serialize(uri, options), options);
14047
+ parse13(serialize(uri, options), options);
14047
14048
  }
14048
14049
  return uri;
14049
14050
  }
@@ -14083,8 +14084,8 @@ var require_fast_uri = __commonJS({
14083
14084
  function resolveComponent(base, relative8, options, skipNormalization) {
14084
14085
  const target = {};
14085
14086
  if (!skipNormalization) {
14086
- base = parse12(serialize(base, options), options);
14087
- relative8 = parse12(serialize(relative8, options), options);
14087
+ base = parse13(serialize(base, options), options);
14088
+ relative8 = parse13(serialize(relative8, options), options);
14088
14089
  }
14089
14090
  options = options || {};
14090
14091
  if (!options.tolerant && relative8.scheme) {
@@ -14376,7 +14377,7 @@ var require_fast_uri = __commonJS({
14376
14377
  }
14377
14378
  return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
14378
14379
  }
14379
- function parse12(uri, opts) {
14380
+ function parse13(uri, opts) {
14380
14381
  return parseWithStatus(uri, opts).parsed;
14381
14382
  }
14382
14383
  function normalizeString(uri, opts) {
@@ -14413,7 +14414,7 @@ var require_fast_uri = __commonJS({
14413
14414
  resolveComponent,
14414
14415
  equal,
14415
14416
  serialize,
14416
- parse: parse12
14417
+ parse: parse13
14417
14418
  };
14418
14419
  module.exports = fastUri;
14419
14420
  module.exports.default = fastUri;
@@ -19547,7 +19548,7 @@ function resolveDispatchStatus(entry, detected, authenticated, issues) {
19547
19548
  return { status: "no", supported: false, reason: "not_authenticated" };
19548
19549
  }
19549
19550
  if (authenticated === null) {
19550
- const authIssue = issues.find((issue4) => issue4 === "auth_context_required" || issue4 === "auth_unknown" || issue4 === "auth_unparsed" || issue4.startsWith("auth_"));
19551
+ const authIssue = issues.find((issue5) => issue5 === "auth_context_required" || issue5 === "auth_unknown" || issue5 === "auth_unparsed" || issue5.startsWith("auth_"));
19551
19552
  return { status: "no", supported: false, reason: authIssue ?? issues[0] ?? "auth_unknown" };
19552
19553
  }
19553
19554
  return { status: "yes", supported: true };
@@ -21473,7 +21474,7 @@ var init_project_config = __esm({
21473
21474
  issues;
21474
21475
  constructor(issues) {
21475
21476
  const sorted = sortIssues3(issues);
21476
- super(sorted.map((issue4) => `${issue4.file}: ${issue4.code}: ${issue4.message}`).join("\n"));
21477
+ super(sorted.map((issue5) => `${issue5.file}: ${issue5.code}: ${issue5.message}`).join("\n"));
21477
21478
  this.name = "KxmConfigError";
21478
21479
  this.issues = sorted;
21479
21480
  }
@@ -22356,8 +22357,8 @@ import {
22356
22357
  } from "node:fs";
22357
22358
  import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute5, join as join12, relative as relative3, resolve as resolve10, sep as sep3 } from "node:path";
22358
22359
  function databaseError(code, file, message) {
22359
- const issue4 = { phase: "semantic", code, file, message };
22360
- return new KxmConfigError([issue4]);
22360
+ const issue5 = { phase: "semantic", code, file, message };
22361
+ return new KxmConfigError([issue5]);
22361
22362
  }
22362
22363
  function checkedParent(path4, description) {
22363
22364
  const parent = dirname9(path4);
@@ -25451,8 +25452,8 @@ function sortIssues4(issues) {
25451
25452
  return compareCodeUnits5(left.code, right.code);
25452
25453
  });
25453
25454
  }
25454
- function pushIssue(sink, issue4) {
25455
- sink.issues.push({ workflowId: sink.workflowId, ...issue4 });
25455
+ function pushIssue(sink, issue5) {
25456
+ sink.issues.push({ workflowId: sink.workflowId, ...issue5 });
25456
25457
  }
25457
25458
  var KXM_COMPILED_WORKFLOW_SCHEMA, WORKFLOW_SCHEMA, STEP_ID_PATTERN, SUPPORTED_STEP_KINDS, TERMINAL_STATUSES, JOIN_STRATEGIES, EVIDENCE_KINDS, REPOSITORY_ACCESS, DISTINCT_BY, BANNED_RUNTIME_HINT, KxmEngineCompileError;
25458
25459
  var init_engine_compile = __esm({
@@ -25472,7 +25473,7 @@ var init_engine_compile = __esm({
25472
25473
  issues;
25473
25474
  constructor(issues) {
25474
25475
  const sorted = sortIssues4(issues);
25475
- super(sorted.map((issue4) => `${issue4.workflowId}: ${issue4.code}: ${issue4.message}`).join("\n"));
25476
+ super(sorted.map((issue5) => `${issue5.workflowId}: ${issue5.code}: ${issue5.message}`).join("\n"));
25476
25477
  this.name = "KxmEngineCompileError";
25477
25478
  this.issues = sorted;
25478
25479
  }
@@ -31226,7 +31227,7 @@ async function cmdKxmInit(runtime, options, postHooks) {
31226
31227
  return finishInit(0, `init plan: ${initialized.plan.mode}`);
31227
31228
  }
31228
31229
  const next = initialized.plan.mode === "legacy" ? "legacy state is not migrated by this build: initialise a fresh project directory and copy the YAML definitions you want to keep" : initialized.repairPlan?.issues.length ? "managed-template repair is blocked by conflicts or authority changes; local files were preserved" : "partial or provenance-free KXM state requires explicit repair; no files were overwritten";
31229
- return finishInit(1, [next, ...initialized.plan.issues.map((issue4) => `${issue4.file}: ${issue4.code}: ${issue4.message}`)].join("\n"));
31230
+ return finishInit(1, [next, ...initialized.plan.issues.map((issue5) => `${issue5.file}: ${issue5.code}: ${issue5.message}`)].join("\n"));
31230
31231
  } catch (error) {
31231
31232
  if (error instanceof KxmConfigError) {
31232
31233
  print(runtime.io, runtime.json, {
@@ -40186,7 +40187,7 @@ async function cmdContextWikiLint(runtime, project) {
40186
40187
  const compiled = response.body;
40187
40188
  const issues = compiled.lint;
40188
40189
  print(runtime.io, runtime.json, { ok: issues.length === 0, command: "context wiki-lint", project, issues, audit: compiled.audit }, issues.length === 0 ? "wiki lint clean" : `wiki lint found ${issues.length} issue(s)`);
40189
- return issues.every((issue4) => issue4.severity !== "error") ? 0 : 1;
40190
+ return issues.every((issue5) => issue5.severity !== "error") ? 0 : 1;
40190
40191
  }
40191
40192
  function skillStateFromFlag(value) {
40192
40193
  if (value === "candidate" || value === "promoted" || value === "quarantined" || value === "rejected") return value;
@@ -44327,6 +44328,66 @@ var LEGACY_CTRL_SEQUENCES = {
44327
44328
  home: ["\x1B[7^"],
44328
44329
  end: ["\x1B[8^"]
44329
44330
  };
44331
+ var LEGACY_SEQUENCE_KEY_IDS = {
44332
+ "\x1BOA": "up",
44333
+ "\x1BOB": "down",
44334
+ "\x1BOC": "right",
44335
+ "\x1BOD": "left",
44336
+ "\x1BOH": "home",
44337
+ "\x1BOF": "end",
44338
+ "\x1B[E": "clear",
44339
+ "\x1BOE": "clear",
44340
+ "\x1BOe": "ctrl+clear",
44341
+ "\x1B[e": "shift+clear",
44342
+ "\x1B[2~": "insert",
44343
+ "\x1B[2$": "shift+insert",
44344
+ "\x1B[2^": "ctrl+insert",
44345
+ "\x1B[3$": "shift+delete",
44346
+ "\x1B[3^": "ctrl+delete",
44347
+ "\x1B[[5~": "pageUp",
44348
+ "\x1B[[6~": "pageDown",
44349
+ "\x1B[a": "shift+up",
44350
+ "\x1B[b": "shift+down",
44351
+ "\x1B[c": "shift+right",
44352
+ "\x1B[d": "shift+left",
44353
+ "\x1BOa": "ctrl+up",
44354
+ "\x1BOb": "ctrl+down",
44355
+ "\x1BOc": "ctrl+right",
44356
+ "\x1BOd": "ctrl+left",
44357
+ "\x1B[5$": "shift+pageUp",
44358
+ "\x1B[6$": "shift+pageDown",
44359
+ "\x1B[7$": "shift+home",
44360
+ "\x1B[8$": "shift+end",
44361
+ "\x1B[5^": "ctrl+pageUp",
44362
+ "\x1B[6^": "ctrl+pageDown",
44363
+ "\x1B[7^": "ctrl+home",
44364
+ "\x1B[8^": "ctrl+end",
44365
+ "\x1BOP": "f1",
44366
+ "\x1BOQ": "f2",
44367
+ "\x1BOR": "f3",
44368
+ "\x1BOS": "f4",
44369
+ "\x1B[11~": "f1",
44370
+ "\x1B[12~": "f2",
44371
+ "\x1B[13~": "f3",
44372
+ "\x1B[14~": "f4",
44373
+ "\x1B[[A": "f1",
44374
+ "\x1B[[B": "f2",
44375
+ "\x1B[[C": "f3",
44376
+ "\x1B[[D": "f4",
44377
+ "\x1B[[E": "f5",
44378
+ "\x1B[15~": "f5",
44379
+ "\x1B[17~": "f6",
44380
+ "\x1B[18~": "f7",
44381
+ "\x1B[19~": "f8",
44382
+ "\x1B[20~": "f9",
44383
+ "\x1B[21~": "f10",
44384
+ "\x1B[23~": "f11",
44385
+ "\x1B[24~": "f12",
44386
+ "\x1Bb": "alt+left",
44387
+ "\x1Bf": "alt+right",
44388
+ "\x1Bp": "alt+up",
44389
+ "\x1Bn": "alt+down"
44390
+ };
44330
44391
  var matchesLegacySequence = (data, sequences) => sequences.includes(data);
44331
44392
  var matchesLegacyModifierSequence = (data, key, modifier) => {
44332
44393
  if (modifier === MODIFIERS.shift) {
@@ -44472,6 +44533,22 @@ function matchesPrintableModifyOtherKeys(data, expectedKeycode, expectedModifier
44472
44533
  return false;
44473
44534
  return normalizeShiftedLetterIdentityCodepoint(parsed.codepoint, parsed.modifier) === normalizeShiftedLetterIdentityCodepoint(expectedKeycode, expectedModifier);
44474
44535
  }
44536
+ function formatKeyNameWithModifiers(keyName, modifier) {
44537
+ const mods = [];
44538
+ const effectiveMod = modifier & ~LOCK_MASK;
44539
+ const supportedModifierMask = MODIFIERS.shift | MODIFIERS.ctrl | MODIFIERS.alt | MODIFIERS.super;
44540
+ if ((effectiveMod & ~supportedModifierMask) !== 0)
44541
+ return void 0;
44542
+ if (effectiveMod & MODIFIERS.shift)
44543
+ mods.push("shift");
44544
+ if (effectiveMod & MODIFIERS.ctrl)
44545
+ mods.push("ctrl");
44546
+ if (effectiveMod & MODIFIERS.alt)
44547
+ mods.push("alt");
44548
+ if (effectiveMod & MODIFIERS.super)
44549
+ mods.push("super");
44550
+ return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName;
44551
+ }
44475
44552
  function parseKeyId(keyId) {
44476
44553
  const parts = keyId.toLowerCase().split("+");
44477
44554
  const key = parts[parts.length - 1];
@@ -44728,6 +44805,149 @@ function matchesKey(data, keyId) {
44728
44805
  }
44729
44806
  return false;
44730
44807
  }
44808
+ function formatParsedKey(codepoint, modifier, baseLayoutKey) {
44809
+ const normalizedCodepoint = normalizeKittyFunctionalCodepoint(codepoint);
44810
+ const identityCodepoint = normalizeShiftedLetterIdentityCodepoint(normalizedCodepoint, modifier);
44811
+ const isLatinLetter = identityCodepoint >= 97 && identityCodepoint <= 122;
44812
+ const isDigit = identityCodepoint >= 48 && identityCodepoint <= 57;
44813
+ const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(identityCodepoint));
44814
+ const effectiveCodepoint = isLatinLetter || isDigit || isKnownSymbol ? identityCodepoint : baseLayoutKey ?? identityCodepoint;
44815
+ let keyName;
44816
+ if (effectiveCodepoint === CODEPOINTS.escape)
44817
+ keyName = "escape";
44818
+ else if (effectiveCodepoint === CODEPOINTS.tab)
44819
+ keyName = "tab";
44820
+ else if (effectiveCodepoint === CODEPOINTS.enter || effectiveCodepoint === CODEPOINTS.kpEnter)
44821
+ keyName = "enter";
44822
+ else if (effectiveCodepoint === CODEPOINTS.space)
44823
+ keyName = "space";
44824
+ else if (effectiveCodepoint === CODEPOINTS.backspace)
44825
+ keyName = "backspace";
44826
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.delete)
44827
+ keyName = "delete";
44828
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.insert)
44829
+ keyName = "insert";
44830
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.home)
44831
+ keyName = "home";
44832
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.end)
44833
+ keyName = "end";
44834
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageUp)
44835
+ keyName = "pageUp";
44836
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageDown)
44837
+ keyName = "pageDown";
44838
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.up)
44839
+ keyName = "up";
44840
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.down)
44841
+ keyName = "down";
44842
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.left)
44843
+ keyName = "left";
44844
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.right)
44845
+ keyName = "right";
44846
+ else if (effectiveCodepoint >= 48 && effectiveCodepoint <= 57)
44847
+ keyName = String.fromCharCode(effectiveCodepoint);
44848
+ else if (effectiveCodepoint >= 97 && effectiveCodepoint <= 122)
44849
+ keyName = String.fromCharCode(effectiveCodepoint);
44850
+ else if (SYMBOL_KEYS.has(String.fromCharCode(effectiveCodepoint)))
44851
+ keyName = String.fromCharCode(effectiveCodepoint);
44852
+ if (!keyName)
44853
+ return void 0;
44854
+ return formatKeyNameWithModifiers(keyName, modifier);
44855
+ }
44856
+ function parseKey(data) {
44857
+ const kitty = parseKittySequence(data);
44858
+ if (kitty) {
44859
+ return formatParsedKey(kitty.codepoint, kitty.modifier, kitty.baseLayoutKey);
44860
+ }
44861
+ const modifyOtherKeys = parseModifyOtherKeysSequence(data);
44862
+ if (modifyOtherKeys) {
44863
+ return formatParsedKey(modifyOtherKeys.codepoint, modifyOtherKeys.modifier);
44864
+ }
44865
+ if (_kittyProtocolActive) {
44866
+ if (data === "\x1B\r" || data === "\n")
44867
+ return "shift+enter";
44868
+ }
44869
+ const legacySequenceKeyId = LEGACY_SEQUENCE_KEY_IDS[data];
44870
+ if (legacySequenceKeyId)
44871
+ return legacySequenceKeyId;
44872
+ if (data === "\x1B")
44873
+ return "escape";
44874
+ if (data === "")
44875
+ return "ctrl+\\";
44876
+ if (data === "")
44877
+ return "ctrl+]";
44878
+ if (data === "")
44879
+ return "ctrl+-";
44880
+ if (data === "\x1B\x1B")
44881
+ return "ctrl+alt+[";
44882
+ if (data === "\x1B")
44883
+ return "ctrl+alt+\\";
44884
+ if (data === "\x1B")
44885
+ return "ctrl+alt+]";
44886
+ if (data === "\x1B")
44887
+ return "ctrl+alt+-";
44888
+ if (data === " ")
44889
+ return "tab";
44890
+ if (data === "\r" || !_kittyProtocolActive && data === "\n" || data === "\x1BOM")
44891
+ return "enter";
44892
+ if (data === "\0")
44893
+ return "ctrl+space";
44894
+ if (data === " ")
44895
+ return "space";
44896
+ if (data === "\x7F")
44897
+ return "backspace";
44898
+ if (data === "\b")
44899
+ return isWindowsTerminalSession() ? "ctrl+backspace" : "backspace";
44900
+ if (data === "\x1B[Z")
44901
+ return "shift+tab";
44902
+ if (!_kittyProtocolActive && data === "\x1B\r")
44903
+ return "alt+enter";
44904
+ if (!_kittyProtocolActive && data === "\x1B ")
44905
+ return "alt+space";
44906
+ if (data === "\x1B\x7F" || data === "\x1B\b")
44907
+ return "alt+backspace";
44908
+ if (!_kittyProtocolActive && data === "\x1BB")
44909
+ return "alt+left";
44910
+ if (!_kittyProtocolActive && data === "\x1BF")
44911
+ return "alt+right";
44912
+ if (!_kittyProtocolActive && data.length === 2 && data[0] === "\x1B") {
44913
+ const code = data.charCodeAt(1);
44914
+ if (code >= 1 && code <= 26) {
44915
+ return `ctrl+alt+${String.fromCharCode(code + 96)}`;
44916
+ }
44917
+ const key = String.fromCharCode(code);
44918
+ if (code >= 97 && code <= 122 || code >= 48 && code <= 57 || SYMBOL_KEYS.has(key)) {
44919
+ return `alt+${key}`;
44920
+ }
44921
+ }
44922
+ if (data === "\x1B[A")
44923
+ return "up";
44924
+ if (data === "\x1B[B")
44925
+ return "down";
44926
+ if (data === "\x1B[C")
44927
+ return "right";
44928
+ if (data === "\x1B[D")
44929
+ return "left";
44930
+ if (data === "\x1B[H" || data === "\x1BOH")
44931
+ return "home";
44932
+ if (data === "\x1B[F" || data === "\x1BOF")
44933
+ return "end";
44934
+ if (data === "\x1B[3~")
44935
+ return "delete";
44936
+ if (data === "\x1B[5~")
44937
+ return "pageUp";
44938
+ if (data === "\x1B[6~")
44939
+ return "pageDown";
44940
+ if (data.length === 1) {
44941
+ const code = data.charCodeAt(0);
44942
+ if (code >= 1 && code <= 26) {
44943
+ return `ctrl+${String.fromCharCode(code + 96)}`;
44944
+ }
44945
+ if (code >= 32 && code <= 126) {
44946
+ return data;
44947
+ }
44948
+ }
44949
+ return void 0;
44950
+ }
44731
44951
  var KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/;
44732
44952
  var KITTY_PRINTABLE_ALLOWED_MODIFIERS = MODIFIERS.shift | LOCK_MASK;
44733
44953
  function decodeKittyPrintable(data) {
@@ -50259,6 +50479,13 @@ import { join as join46, resolve as resolve27, dirname as dirname20 } from "node
50259
50479
  import { spawnSync as spawnSync12 } from "node:child_process";
50260
50480
 
50261
50481
  // packages/core/tui/src/types/surface.ts
50482
+ var KXM_TUI_FIELD_KINDS = ["text", "enum", "choice", "info"];
50483
+ var KXM_TUI_STATUSES = ["ready", "available", "blocked"];
50484
+ var KXM_TUI_STEP_STATES = ["pending", "satisfied", "running", "done", "failed"];
50485
+ var KXM_TUI_NOTICE_LEVELS = ["info", "error"];
50486
+ var KXM_TUI_SET_ACTION = "set";
50487
+ var KXM_TUI_CLEAR_ACTION = "clear";
50488
+ var KXM_TUI_ABORT_ACTION = "abort";
50262
50489
  var KXM_TUI_LIMITS = Object.freeze({
50263
50490
  sourceMax: 128,
50264
50491
  idMax: 128,
@@ -50280,8 +50507,231 @@ var KXM_TUI_LIMITS = Object.freeze({
50280
50507
  sectionsMax: 32,
50281
50508
  orderMax: 1e3
50282
50509
  });
50510
+ var ID_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
50511
+ var SOURCE_PATTERN = /^[A-Za-z0-9@][A-Za-z0-9@/._:-]*$/;
50512
+ var KEY_PATH_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.[\]-]*$/;
50513
+ var ACTION_KEY_PATTERN = /^[A-Za-z0-9]$/;
50514
+ function issue4(path4, code, message) {
50515
+ return { path: path4, code, message };
50516
+ }
50517
+ function isPlainObject(value) {
50518
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50519
+ }
50520
+ function boundedText(issues, path4, value, max, required2) {
50521
+ if (value === void 0) {
50522
+ if (required2) issues.push(issue4(path4, "required", `${path4} is required`));
50523
+ return;
50524
+ }
50525
+ if (typeof value !== "string" || value.length === 0) {
50526
+ issues.push(issue4(path4, "invalid_string", `${path4} must be a non-empty string`));
50527
+ return;
50528
+ }
50529
+ if (value.length > max) {
50530
+ issues.push(issue4(path4, "too_long", `${path4} exceeds ${max} characters`));
50531
+ return;
50532
+ }
50533
+ if (/[\u0000-\u0008\u000a-\u001f\u007f]/u.test(value)) {
50534
+ issues.push(issue4(path4, "control_character", `${path4} must not contain control characters`));
50535
+ }
50536
+ }
50537
+ function optionalEnum(issues, path4, value, allowed) {
50538
+ if (value === void 0) return;
50539
+ if (typeof value !== "string" || !allowed.includes(value)) {
50540
+ issues.push(issue4(path4, "invalid_choice", `${path4} must be one of ${allowed.join(", ")}`));
50541
+ }
50542
+ }
50543
+ function ratioField(issues, path4, value) {
50544
+ if (value === void 0) return;
50545
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
50546
+ issues.push(issue4(path4, "invalid_ratio", `${path4} must be a finite number between 0 and 1`));
50547
+ }
50548
+ }
50549
+ function validateChoice(issues, path4, raw) {
50550
+ if (!isPlainObject(raw)) {
50551
+ issues.push(issue4(path4, "invalid_object", `${path4} must be an object`));
50552
+ return;
50553
+ }
50554
+ boundedText(issues, `${path4}.id`, raw.id, KXM_TUI_LIMITS.choiceIdMax, true);
50555
+ if (typeof raw.id === "string" && /[\s]/u.test(raw.id)) {
50556
+ issues.push(issue4(`${path4}.id`, "invalid_choice_id", `${path4}.id must not contain whitespace`));
50557
+ }
50558
+ boundedText(issues, `${path4}.label`, raw.label, KXM_TUI_LIMITS.choiceLabelMax, true);
50559
+ boundedText(issues, `${path4}.detail`, raw.detail, KXM_TUI_LIMITS.shortDetailMax, false);
50560
+ boundedText(issues, `${path4}.group`, raw.group, KXM_TUI_LIMITS.shortDetailMax, false);
50561
+ boundedText(issues, `${path4}.statusText`, raw.statusText, KXM_TUI_LIMITS.shortDetailMax, false);
50562
+ boundedText(issues, `${path4}.action`, raw.action, KXM_TUI_LIMITS.idMax, false);
50563
+ optionalEnum(issues, `${path4}.status`, raw.status, KXM_TUI_STATUSES);
50564
+ }
50565
+ function validateAction(issues, path4, raw) {
50566
+ if (!isPlainObject(raw)) {
50567
+ issues.push(issue4(path4, "invalid_object", `${path4} must be an object`));
50568
+ return;
50569
+ }
50570
+ if (typeof raw.key !== "string" || !ACTION_KEY_PATTERN.test(raw.key)) {
50571
+ issues.push(issue4(`${path4}.key`, "invalid_action_key", `${path4}.key must be a single alphanumeric key`));
50572
+ }
50573
+ boundedText(issues, `${path4}.label`, raw.label, KXM_TUI_LIMITS.shortDetailMax, true);
50574
+ boundedText(issues, `${path4}.action`, raw.action, KXM_TUI_LIMITS.idMax, true);
50575
+ }
50576
+ function validateStep(issues, path4, raw) {
50577
+ if (!isPlainObject(raw)) {
50578
+ issues.push(issue4(path4, "invalid_object", `${path4} must be an object`));
50579
+ return;
50580
+ }
50581
+ boundedText(issues, `${path4}.label`, raw.label, KXM_TUI_LIMITS.shortDetailMax, true);
50582
+ boundedText(issues, `${path4}.detail`, raw.detail, KXM_TUI_LIMITS.stepDetailMax, false);
50583
+ optionalEnum(issues, `${path4}.state`, raw.state, KXM_TUI_STEP_STATES);
50584
+ if (raw.state === void 0) issues.push(issue4(`${path4}.state`, "required", `${path4}.state is required`));
50585
+ }
50586
+ function validateField(issues, path4, raw) {
50587
+ if (!isPlainObject(raw)) {
50588
+ issues.push(issue4(path4, "invalid_object", `${path4} must be an object`));
50589
+ return;
50590
+ }
50591
+ boundedText(issues, `${path4}.id`, raw.id, KXM_TUI_LIMITS.idMax, true);
50592
+ if (typeof raw.id === "string" && !ID_PATTERN.test(raw.id)) {
50593
+ issues.push(issue4(`${path4}.id`, "invalid_id", `${path4}.id must match ${ID_PATTERN}`));
50594
+ }
50595
+ boundedText(issues, `${path4}.label`, raw.label, KXM_TUI_LIMITS.labelMax, true);
50596
+ if (raw.kind === void 0) {
50597
+ issues.push(issue4(`${path4}.kind`, "required", `${path4}.kind is required`));
50598
+ } else {
50599
+ optionalEnum(issues, `${path4}.kind`, raw.kind, KXM_TUI_FIELD_KINDS);
50600
+ }
50601
+ boundedText(issues, `${path4}.value`, raw.value, KXM_TUI_LIMITS.valueMax, false);
50602
+ boundedText(issues, `${path4}.placeholder`, raw.placeholder, KXM_TUI_LIMITS.shortDetailMax, false);
50603
+ boundedText(issues, `${path4}.detail`, raw.detail, KXM_TUI_LIMITS.detailMax, false);
50604
+ boundedText(issues, `${path4}.statusText`, raw.statusText, KXM_TUI_LIMITS.statusTextMax, false);
50605
+ if (raw.updatedAt !== void 0) {
50606
+ boundedText(issues, `${path4}.updatedAt`, raw.updatedAt, 64, false);
50607
+ if (typeof raw.updatedAt === "string" && !Number.isFinite(Date.parse(raw.updatedAt))) {
50608
+ issues.push(issue4(`${path4}.updatedAt`, "invalid_timestamp", `${path4}.updatedAt must be an ISO timestamp`));
50609
+ }
50610
+ }
50611
+ optionalEnum(issues, `${path4}.status`, raw.status, KXM_TUI_STATUSES);
50612
+ if (raw.keyPath !== void 0) {
50613
+ boundedText(issues, `${path4}.keyPath`, raw.keyPath, KXM_TUI_LIMITS.keyPathMax, false);
50614
+ if (typeof raw.keyPath === "string" && !KEY_PATH_PATTERN.test(raw.keyPath)) {
50615
+ issues.push(issue4(`${path4}.keyPath`, "invalid_key_path", `${path4}.keyPath must be a dotted config key`));
50616
+ }
50617
+ }
50618
+ for (const listKey of ["busy", "awaitingInput"]) {
50619
+ if (raw[listKey] !== void 0 && typeof raw[listKey] !== "boolean") {
50620
+ issues.push(issue4(`${path4}.${listKey}`, "invalid_boolean", `${path4}.${listKey} must be a boolean`));
50621
+ }
50622
+ }
50623
+ if (raw.progress !== void 0) {
50624
+ if (!isPlainObject(raw.progress)) {
50625
+ issues.push(issue4(`${path4}.progress`, "invalid_object", `${path4}.progress must be an object`));
50626
+ } else {
50627
+ boundedText(issues, `${path4}.progress.label`, raw.progress.label, KXM_TUI_LIMITS.shortDetailMax, true);
50628
+ ratioField(issues, `${path4}.progress.ratio`, raw.progress.ratio);
50629
+ }
50630
+ }
50631
+ for (const [listKey, max, check] of [
50632
+ ["choices", KXM_TUI_LIMITS.choicesMax, validateChoice],
50633
+ ["actions", KXM_TUI_LIMITS.actionsMax, validateAction],
50634
+ ["steps", KXM_TUI_LIMITS.stepsMax, validateStep]
50635
+ ]) {
50636
+ const list = raw[listKey];
50637
+ if (list === void 0) continue;
50638
+ if (!Array.isArray(list) || list.length > max) {
50639
+ issues.push(issue4(`${path4}.${listKey}`, "invalid_list", `${path4}.${listKey} must be an array of at most ${max}`));
50640
+ continue;
50641
+ }
50642
+ list.forEach((entry, index) => check(issues, `${path4}.${listKey}[${index}]`, entry));
50643
+ }
50644
+ const output = raw.output;
50645
+ if (output !== void 0) {
50646
+ if (!Array.isArray(output) || output.length > KXM_TUI_LIMITS.outputMaxLines) {
50647
+ issues.push(issue4(`${path4}.output`, "invalid_list", `${path4}.output must be an array of at most ${KXM_TUI_LIMITS.outputMaxLines} lines`));
50648
+ } else {
50649
+ output.forEach((line, index) => {
50650
+ if (typeof line !== "string") issues.push(issue4(`${path4}.output[${index}]`, "invalid_string", "output lines must be strings"));
50651
+ else if (line.length > KXM_TUI_LIMITS.outputLineMax) issues.push(issue4(`${path4}.output[${index}]`, "too_long", "output line is too long"));
50652
+ });
50653
+ }
50654
+ }
50655
+ }
50656
+ function validateSection(issues, path4, raw) {
50657
+ if (!isPlainObject(raw)) {
50658
+ issues.push(issue4(path4, "invalid_object", `${path4} must be an object`));
50659
+ return;
50660
+ }
50661
+ boundedText(issues, `${path4}.id`, raw.id, KXM_TUI_LIMITS.idMax, true);
50662
+ if (typeof raw.id === "string" && !ID_PATTERN.test(raw.id)) {
50663
+ issues.push(issue4(`${path4}.id`, "invalid_id", `${path4}.id must match ${ID_PATTERN}`));
50664
+ }
50665
+ boundedText(issues, `${path4}.title`, raw.title, KXM_TUI_LIMITS.labelMax, true);
50666
+ boundedText(issues, `${path4}.detail`, raw.detail, KXM_TUI_LIMITS.shortDetailMax, false);
50667
+ boundedText(issues, `${path4}.notice`, raw.notice, KXM_TUI_LIMITS.detailMax, false);
50668
+ optionalEnum(issues, `${path4}.noticeLevel`, raw.noticeLevel, KXM_TUI_NOTICE_LEVELS);
50669
+ if (typeof raw.order !== "number" || !Number.isInteger(raw.order) || raw.order < 0 || raw.order > KXM_TUI_LIMITS.orderMax) {
50670
+ issues.push(issue4(`${path4}.order`, "invalid_order", `${path4}.order must be an integer between 0 and ${KXM_TUI_LIMITS.orderMax}`));
50671
+ }
50672
+ if (!Array.isArray(raw.fields) || raw.fields.length > KXM_TUI_LIMITS.fieldsMax) {
50673
+ issues.push(issue4(`${path4}.fields`, "invalid_list", `${path4}.fields must be an array of at most ${KXM_TUI_LIMITS.fieldsMax}`));
50674
+ return;
50675
+ }
50676
+ const seen = /* @__PURE__ */ new Set();
50677
+ raw.fields.forEach((field, index) => {
50678
+ const fieldPath = `${path4}.fields[${index}]`;
50679
+ validateField(issues, fieldPath, field);
50680
+ if (isPlainObject(field) && typeof field.id === "string") {
50681
+ if (seen.has(field.id)) issues.push(issue4(`${fieldPath}.id`, "duplicate_id", `${field.id} is published twice in ${path4}`));
50682
+ seen.add(field.id);
50683
+ }
50684
+ });
50685
+ }
50686
+ function validateKxmTuiSurface(sections) {
50687
+ if (!Array.isArray(sections)) return [issue4("sections", "invalid_list", "sections must be an array")];
50688
+ if (sections.length > KXM_TUI_LIMITS.sectionsMax) {
50689
+ return [issue4("sections", "too_many", `sections must number at most ${KXM_TUI_LIMITS.sectionsMax}`)];
50690
+ }
50691
+ const issues = [];
50692
+ const seenSources = /* @__PURE__ */ new Map();
50693
+ sections.forEach((section, index) => {
50694
+ const path4 = `sections[${index}]`;
50695
+ validateSection(issues, path4, section);
50696
+ if (!isPlainObject(section)) return;
50697
+ boundedText(issues, `${path4}.source`, section.source, KXM_TUI_LIMITS.sourceMax, true);
50698
+ if (typeof section.source === "string" && !SOURCE_PATTERN.test(section.source)) {
50699
+ issues.push(issue4(`${path4}.source`, "invalid_source", `${path4}.source must be an addressable owner id`));
50700
+ }
50701
+ if (typeof section.source === "string" && typeof section.id === "string") {
50702
+ const ids = seenSources.get(section.source) ?? /* @__PURE__ */ new Set();
50703
+ seenSources.set(section.source, ids);
50704
+ if (ids.has(section.id)) issues.push(issue4(`${path4}.id`, "duplicate_section", `${section.source}/${section.id} is published twice`));
50705
+ ids.add(section.id);
50706
+ }
50707
+ });
50708
+ return issues;
50709
+ }
50710
+ function isSelectableField(field) {
50711
+ return field !== void 0 && field.kind !== "info";
50712
+ }
50713
+ function groupedKxmTuiChoices(field) {
50714
+ const choices = field.choices ?? [];
50715
+ if (choices.length === 0) return [];
50716
+ if (choices.every((choice) => choice.group === void 0)) return [{ group: "", choices }];
50717
+ const groups = [];
50718
+ const index = /* @__PURE__ */ new Map();
50719
+ for (const choice of choices) {
50720
+ const group = choice.group ?? "";
50721
+ let bucket = index.get(group);
50722
+ if (!bucket) {
50723
+ bucket = [];
50724
+ index.set(group, bucket);
50725
+ groups.push({ group, choices: bucket });
50726
+ }
50727
+ bucket.push(choice);
50728
+ }
50729
+ return groups;
50730
+ }
50283
50731
 
50284
50732
  // packages/core/tui/src/tui/keys.ts
50733
+ var KXM_TUI_INPUT_MAX_LENGTH = 4096;
50734
+ var CONTROL_BYTES = /[\u0000-\u0008\u000a-\u001f\u007f\u0080-\u009f]/u;
50285
50735
  var NAMED_KEYS = Object.freeze({
50286
50736
  up: "up",
50287
50737
  down: "down",
@@ -50299,8 +50749,109 @@ var NAMED_KEYS = Object.freeze({
50299
50749
  backspace: "backspace",
50300
50750
  delete: "delete"
50301
50751
  });
50752
+ var CONTROL_BYTES_GLOBAL = /[\u0000-\u0008\u000a-\u001f\u007f\u0080-\u009f]/gu;
50753
+ function normalizeInputText(value) {
50754
+ return value.replace(CONTROL_BYTES_GLOBAL, " ").replace(/ {2,}/gu, " ").trim().slice(0, KXM_TUI_INPUT_MAX_LENGTH);
50755
+ }
50756
+ function decodeKxmTuiInput(data) {
50757
+ if (typeof data !== "string" || data.length === 0) return { kind: "ignored" };
50758
+ const named = parseKey(data);
50759
+ if (named === "ctrl+c") return { kind: "ctrlC" };
50760
+ if (named) {
50761
+ const kind = NAMED_KEYS[named.toLowerCase()];
50762
+ if (kind) return { kind };
50763
+ if (named === "space") return { kind: "text", text: " " };
50764
+ if (Array.from(named).length === 1 && !CONTROL_BYTES.test(named)) {
50765
+ return { kind: "text", text: named };
50766
+ }
50767
+ return { kind: "ignored" };
50768
+ }
50769
+ const kitty = decodeKittyPrintable(data);
50770
+ if (kitty) {
50771
+ const text = normalizeInputText(kitty);
50772
+ return text ? { kind: "text", text } : { kind: "ignored" };
50773
+ }
50774
+ if (data.startsWith("\x1B")) return { kind: "ignored" };
50775
+ const pasted = normalizeInputText(data);
50776
+ return pasted ? { kind: "text", text: pasted } : { kind: "ignored" };
50777
+ }
50778
+ function deleteBackward(text) {
50779
+ const units = Array.from(text);
50780
+ units.pop();
50781
+ return units.join("");
50782
+ }
50783
+ function insertAt(text, caret, insert, maxLength = KXM_TUI_INPUT_MAX_LENGTH) {
50784
+ const units = Array.from(text);
50785
+ const at2 = Math.max(0, Math.min(caret, units.length));
50786
+ const added = Array.from(insert);
50787
+ const merged = [...units.slice(0, at2), ...added, ...units.slice(at2)].join("");
50788
+ const next = merged.length > maxLength ? merged.slice(0, maxLength) : merged;
50789
+ return { text: next, caret: Math.min(next.length, at2 + added.length) };
50790
+ }
50302
50791
 
50303
50792
  // packages/core/tui/src/tui/layout.ts
50793
+ var KXM_TUI_ELLIPSIS = "\u2026";
50794
+ function fitText(text, width) {
50795
+ if (width <= 0) return "";
50796
+ if (visibleWidth(text) <= width) return text;
50797
+ if (!text.includes("\x1B")) {
50798
+ let out = "";
50799
+ for (const unit of Array.from(text)) {
50800
+ if (visibleWidth(out) + visibleWidth(unit) > width - KXM_TUI_ELLIPSIS.length) break;
50801
+ out += unit;
50802
+ }
50803
+ return out + KXM_TUI_ELLIPSIS;
50804
+ }
50805
+ return truncateToWidth(text, width, KXM_TUI_ELLIPSIS);
50806
+ }
50807
+ function alignRight(left, right, width) {
50808
+ if (width <= 0) return "";
50809
+ const detail = fitText(right, Math.max(0, width - 2));
50810
+ const detailWidth = visibleWidth(detail);
50811
+ const leftWidth = Math.max(1, width - detailWidth - 1);
50812
+ const fitted = fitText(left, leftWidth);
50813
+ const filler = Math.max(1, width - visibleWidth(fitted) - detailWidth);
50814
+ return `${fitted}${" ".repeat(filler)}${detail}`;
50815
+ }
50816
+ function padRight(text, width) {
50817
+ const missing = width - visibleWidth(text);
50818
+ return missing > 0 ? text + " ".repeat(missing) : text;
50819
+ }
50820
+ function wrapPlain(text, width) {
50821
+ if (width <= 0) return [];
50822
+ const lines = [];
50823
+ let current = "";
50824
+ for (const word of text.split(/\s+/u).filter(Boolean)) {
50825
+ if (!current.length) current = word;
50826
+ else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) current = `${current} ${word}`;
50827
+ else {
50828
+ lines.push(current);
50829
+ current = word;
50830
+ }
50831
+ }
50832
+ if (current.length) lines.push(current);
50833
+ return lines.length ? lines : [];
50834
+ }
50835
+ function formatAge(iso, nowMs) {
50836
+ if (!iso) return "\u2014";
50837
+ const at2 = Date.parse(iso);
50838
+ if (!Number.isFinite(at2)) return "\u2014";
50839
+ const seconds = Math.floor((nowMs - at2) / 1e3);
50840
+ if (seconds < 0) return "\u2014";
50841
+ if (seconds < 60) return `${seconds}s`;
50842
+ const minutes = Math.floor(seconds / 60);
50843
+ if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
50844
+ const hours = Math.floor(minutes / 60);
50845
+ if (hours < 24) return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
50846
+ const days = Math.floor(hours / 24);
50847
+ return `${days}d${String(hours % 24).padStart(2, "0")}h`;
50848
+ }
50849
+ function progressBar(ratio, width = 10) {
50850
+ const cells = Math.max(1, width);
50851
+ if (ratio === void 0 || !Number.isFinite(ratio)) return "\u2591".repeat(cells);
50852
+ const filled = Math.round(Math.max(0, Math.min(1, ratio)) * cells);
50853
+ return "\u2588".repeat(filled) + "\u2591".repeat(Math.max(0, cells - filled));
50854
+ }
50304
50855
  var KXM_TUI_MARKS = Object.freeze({
50305
50856
  pending: "\u25CB",
50306
50857
  active: "\u25B6",
@@ -50365,9 +50916,368 @@ function createKxmTuiAnsiTheme(color) {
50365
50916
  panelBg: (text) => `\x1B[48;5;236m${text}\x1B[0m`
50366
50917
  };
50367
50918
  }
50919
+ function createDefaultKxmTuiTheme(env = process.env) {
50920
+ return createKxmTuiAnsiTheme(!(env.NO_COLOR || env.KXM_TUI_NO_COLOR));
50921
+ }
50922
+
50923
+ // packages/core/tui/src/tui/panel.ts
50924
+ var PANE_KEYS = /* @__PURE__ */ new Set(["j", "k", "h", "l"]);
50925
+ function initialKxmTuiPanelState(sections) {
50926
+ return {
50927
+ pane: "sections",
50928
+ mode: "browse",
50929
+ sectionIndex: 0,
50930
+ fieldIndex: firstSelectableFieldIndex(sections[0]?.fields ?? []),
50931
+ choiceIndex: 0,
50932
+ draft: "",
50933
+ caret: 0,
50934
+ filter: "",
50935
+ filtering: false,
50936
+ help: false,
50937
+ pending: []
50938
+ };
50939
+ }
50940
+ function pendingKey(invocation) {
50941
+ return `${invocation.source}\0${invocation.sectionId}\0${invocation.fieldId}`;
50942
+ }
50943
+ function firstSelectableFieldIndex(fields) {
50944
+ const index = fields.findIndex(isSelectableField);
50945
+ return index < 0 ? 0 : index;
50946
+ }
50947
+ function visibleChoices(field, filter) {
50948
+ const choices = field?.choices ?? [];
50949
+ const needle = filter.trim().toLowerCase();
50950
+ if (!needle) return [...choices];
50951
+ return choices.filter(
50952
+ (choice) => `${choice.group ?? ""} ${choice.label} ${choice.id} ${choice.detail ?? ""}`.toLowerCase().includes(needle)
50953
+ );
50954
+ }
50955
+ function currentSection(sections, state) {
50956
+ return sections[state.sectionIndex];
50957
+ }
50958
+ function currentField(sections, state) {
50959
+ return currentSection(sections, state)?.fields[state.fieldIndex];
50960
+ }
50961
+ function reconcileKxmTuiPanelState(state, sections) {
50962
+ const sectionIndex = Math.min(state.sectionIndex, Math.max(0, sections.length - 1));
50963
+ const fields = sections[sectionIndex]?.fields ?? [];
50964
+ let fieldIndex = Math.min(state.fieldIndex, Math.max(0, fields.length - 1));
50965
+ if (!isSelectableField(fields[fieldIndex])) fieldIndex = firstSelectableFieldIndex(fields);
50966
+ const field = fields[fieldIndex];
50967
+ const keepsEdit = state.mode === "edit" && (field?.statusText !== void 0 || field?.awaitingInput === true);
50968
+ return {
50969
+ ...state,
50970
+ sectionIndex,
50971
+ fieldIndex,
50972
+ mode: keepsEdit ? "edit" : "browse",
50973
+ choiceIndex: 0,
50974
+ pending: []
50975
+ };
50976
+ }
50977
+ function reduceKxmTuiInput(state, sections, input) {
50978
+ return reduceDecoded(state, sections, decodeKxmTuiInput(input));
50979
+ }
50980
+ function invokeEffect(state, section, field, action, value) {
50981
+ const invocation = {
50982
+ source: section.source,
50983
+ sectionId: section.id,
50984
+ fieldId: field.id,
50985
+ action,
50986
+ ...value === void 0 ? {} : { value }
50987
+ };
50988
+ const key = pendingKey(invocation);
50989
+ return {
50990
+ state: { ...state, pending: state.pending.includes(key) ? state.pending : [...state.pending, key] },
50991
+ effects: [{ type: "invoke", invocation }]
50992
+ };
50993
+ }
50994
+ function moveSection(state, sections, delta) {
50995
+ if (sections.length === 0) return state;
50996
+ const next = Math.max(0, Math.min(state.sectionIndex + delta, sections.length - 1));
50997
+ if (next === state.sectionIndex) return state;
50998
+ return {
50999
+ ...state,
51000
+ sectionIndex: next,
51001
+ fieldIndex: firstSelectableFieldIndex(sections[next]?.fields ?? []),
51002
+ mode: "browse",
51003
+ choiceIndex: 0,
51004
+ draft: "",
51005
+ caret: 0,
51006
+ filter: "",
51007
+ filtering: false
51008
+ };
51009
+ }
51010
+ function moveField(state, fields, delta) {
51011
+ const step = delta < 0 ? -1 : 1;
51012
+ let next = state.fieldIndex + step;
51013
+ while (next >= 0 && next < fields.length && !isSelectableField(fields[next])) next += step;
51014
+ if (next < 0 || next >= fields.length) return state;
51015
+ return {
51016
+ ...state,
51017
+ fieldIndex: next,
51018
+ mode: "browse",
51019
+ choiceIndex: 0,
51020
+ draft: "",
51021
+ caret: 0,
51022
+ filter: "",
51023
+ filtering: false
51024
+ };
51025
+ }
51026
+ function moveChoice(state, count, delta) {
51027
+ if (count === 0) return state;
51028
+ return { ...state, choiceIndex: Math.max(0, Math.min(state.choiceIndex + delta, count - 1)) };
51029
+ }
51030
+ function choiceAt(field, state) {
51031
+ const choices = visibleChoices(field, state.filter);
51032
+ return choices[Math.max(0, Math.min(state.choiceIndex, choices.length - 1))];
51033
+ }
51034
+ function enterField(state, section, field) {
51035
+ if (!field) return { state, effects: [] };
51036
+ if (field.kind === "choice") {
51037
+ if ((field.choices?.length ?? 0) === 0) {
51038
+ return { state, effects: [{ type: "notice", message: `${field.label} has nothing to offer yet` }] };
51039
+ }
51040
+ return { state: { ...state, mode: "choices", choiceIndex: 0 }, effects: [] };
51041
+ }
51042
+ if (field.kind === "enum") {
51043
+ const choices = field.choices ?? [];
51044
+ if (choices.length === 0) {
51045
+ return { state, effects: [{ type: "notice", message: `${field.label} has no values published` }] };
51046
+ }
51047
+ const current = choices.findIndex((choice) => choice.id === field.value);
51048
+ const next = choices[(current + 1) % choices.length];
51049
+ if (next.status === "blocked") {
51050
+ return { state, effects: [{ type: "notice", message: next.statusText ?? `${next.label} is unavailable` }] };
51051
+ }
51052
+ return invokeEffect(state, section, field, next.action ?? KXM_TUI_SET_ACTION, next.id);
51053
+ }
51054
+ if (field.kind !== "text") return { state, effects: [] };
51055
+ return { state: { ...state, mode: "edit", draft: field.value ?? "", caret: (field.value ?? "").length }, effects: [] };
51056
+ }
51057
+ function reduceDecoded(state, sections, decoded) {
51058
+ const section = sections[state.sectionIndex];
51059
+ const field = currentField(sections, state);
51060
+ if (decoded.kind === "ctrlC") {
51061
+ if (field?.busy && section) {
51062
+ return invokeEffect(state, section, field, KXM_TUI_ABORT_ACTION);
51063
+ }
51064
+ return { state, effects: [{ type: "quit" }] };
51065
+ }
51066
+ if (!section) return { state, effects: [{ type: "quit" }] };
51067
+ if (state.mode === "edit") return reduceEdit(state, section, field, decoded);
51068
+ if (state.mode === "choices") return reduceChoices(state, section, field, decoded);
51069
+ return reduceBrowse(state, sections, section, field, decoded);
51070
+ }
51071
+ function caretSource(text, caret) {
51072
+ return Array.from(text).slice(0, caret).join("").length;
51073
+ }
51074
+ function reduceEdit(state, section, field, decoded) {
51075
+ if (!field) return { state: { ...state, mode: "browse" }, effects: [] };
51076
+ switch (decoded.kind) {
51077
+ case "escape":
51078
+ return { state: { ...state, mode: "browse", draft: "", caret: 0 }, effects: [] };
51079
+ case "enter": {
51080
+ const value = normalizeInputText(state.draft).trim();
51081
+ if (!value) return { state: { ...state, mode: "browse", draft: "", caret: 0 }, effects: [] };
51082
+ const step = invokeEffect(state, section, field, KXM_TUI_SET_ACTION, value);
51083
+ return { state: { ...step.state, mode: "browse", draft: "", caret: 0 }, effects: step.effects };
51084
+ }
51085
+ case "backspace": {
51086
+ if (state.caret === 0) return { state, effects: [] };
51087
+ const cut = caretSource(state.draft, state.caret);
51088
+ const before = deleteBackward(state.draft.slice(0, cut));
51089
+ const draft = `${before}${state.draft.slice(cut)}`;
51090
+ return { state: { ...state, draft, caret: Array.from(before).length }, effects: [] };
51091
+ }
51092
+ case "delete": {
51093
+ const units = Array.from(state.draft);
51094
+ const text = [...units.slice(0, state.caret), ...units.slice(state.caret + 1)].join("");
51095
+ return { state: { ...state, draft: text }, effects: [] };
51096
+ }
51097
+ case "left":
51098
+ return { state: { ...state, caret: Math.max(0, state.caret - 1) }, effects: [] };
51099
+ case "right":
51100
+ return { state: { ...state, caret: Math.min(Array.from(state.draft).length, state.caret + 1) }, effects: [] };
51101
+ case "home":
51102
+ return { state: { ...state, caret: 0 }, effects: [] };
51103
+ case "end":
51104
+ return { state: { ...state, caret: Array.from(state.draft).length }, effects: [] };
51105
+ case "text": {
51106
+ const text = decoded.text ?? "";
51107
+ if (!text) return { state, effects: [] };
51108
+ const inserted = insertAt(state.draft, state.caret, text);
51109
+ return { state: { ...state, draft: inserted.text, caret: inserted.caret }, effects: [] };
51110
+ }
51111
+ default:
51112
+ return { state, effects: [] };
51113
+ }
51114
+ }
51115
+ function reduceChoices(state, section, field, decoded) {
51116
+ if (!field) return { state: { ...state, mode: "browse" }, effects: [] };
51117
+ const choices = visibleChoices(field, state.filter);
51118
+ switch (decoded.kind) {
51119
+ case "escape":
51120
+ return { state: { ...state, mode: "browse", filter: "", filtering: false, choiceIndex: 0 }, effects: [] };
51121
+ case "up":
51122
+ return { state: moveChoice(state, choices.length, -1), effects: [] };
51123
+ case "down":
51124
+ return { state: moveChoice(state, choices.length, 1), effects: [] };
51125
+ case "pageUp":
51126
+ return { state: moveChoice(state, choices.length, -8), effects: [] };
51127
+ case "pageDown":
51128
+ return { state: moveChoice(state, choices.length, 8), effects: [] };
51129
+ case "enter": {
51130
+ const choice = choiceAt(field, state);
51131
+ if (!choice) return { state, effects: [] };
51132
+ if (choice.status === "blocked") {
51133
+ return { state, effects: [{ type: "notice", message: choice.statusText ?? `${choice.label} is unavailable` }] };
51134
+ }
51135
+ const clearing = (choice.action ?? KXM_TUI_SET_ACTION) === KXM_TUI_CLEAR_ACTION;
51136
+ const step = invokeEffect(
51137
+ state,
51138
+ section,
51139
+ field,
51140
+ choice.action ?? KXM_TUI_SET_ACTION,
51141
+ clearing ? void 0 : choice.id
51142
+ );
51143
+ return { state: { ...step.state, mode: "browse", filter: "", filtering: false }, effects: step.effects };
51144
+ }
51145
+ case "backspace": {
51146
+ if (state.filter.length === 0) return { state: { ...state, filtering: false }, effects: [] };
51147
+ return { state: { ...state, filter: deleteBackward(state.filter), choiceIndex: 0 }, effects: [] };
51148
+ }
51149
+ case "text": {
51150
+ const text = normalizeInputText(decoded.text ?? "");
51151
+ if (!text) return { state, effects: [] };
51152
+ return { state: { ...state, filtering: true, filter: `${state.filter}${text}`, choiceIndex: 0 }, effects: [] };
51153
+ }
51154
+ default:
51155
+ return { state, effects: [] };
51156
+ }
51157
+ }
51158
+ function reduceBrowse(state, sections, section, field, decoded) {
51159
+ if (state.help && (decoded.kind === "escape" || decoded.kind === "enter")) {
51160
+ return { state: { ...state, help: false }, effects: [] };
51161
+ }
51162
+ if (state.help) return { state, effects: [] };
51163
+ if (decoded.kind === "text") {
51164
+ const key = decoded.text ?? "";
51165
+ if (key === "q") return { state, effects: [{ type: "quit" }] };
51166
+ if (key === "h" || key === "?") return { state: { ...state, help: true }, effects: [] };
51167
+ const action = field?.actions?.find((candidate) => candidate.key === key);
51168
+ if (action && field) return invokeEffect(state, section, field, action.action);
51169
+ if ((key === "x" || key === "d") && field && field.value !== void 0 && field.kind !== "info") {
51170
+ return invokeEffect(state, section, field, KXM_TUI_CLEAR_ACTION);
51171
+ }
51172
+ if (PANE_KEYS.has(key)) {
51173
+ if (key === "h") return { state: { ...state, pane: "sections" }, effects: [] };
51174
+ if (key === "l") return { state: { ...state, pane: "fields" }, effects: [] };
51175
+ const delta = key === "k" ? -1 : 1;
51176
+ return {
51177
+ state: state.pane === "fields" ? moveField(state, section.fields, delta) : moveSection(state, sections, delta),
51178
+ effects: []
51179
+ };
51180
+ }
51181
+ return { state, effects: [] };
51182
+ }
51183
+ switch (decoded.kind) {
51184
+ case "escape":
51185
+ return state.pane === "fields" ? { state: { ...state, pane: "sections", mode: "browse" }, effects: [] } : { state, effects: [{ type: "quit" }] };
51186
+ case "tab":
51187
+ return { state: { ...state, pane: state.pane === "fields" ? "sections" : "fields" }, effects: [] };
51188
+ case "right":
51189
+ return { state: { ...state, pane: "fields" }, effects: [] };
51190
+ case "left":
51191
+ return { state: { ...state, pane: "sections" }, effects: [] };
51192
+ case "up":
51193
+ return { state: state.pane === "fields" ? moveField(state, section.fields, -1) : moveSection(state, sections, -1), effects: [] };
51194
+ case "down":
51195
+ return { state: state.pane === "fields" ? moveField(state, section.fields, 1) : moveSection(state, sections, 1), effects: [] };
51196
+ case "pageUp":
51197
+ return { state: moveSection(state, sections, -1), effects: [] };
51198
+ case "pageDown":
51199
+ return { state: moveSection(state, sections, 1), effects: [] };
51200
+ case "enter":
51201
+ return state.pane === "fields" ? enterField(state, section, field) : { state: { ...state, pane: "fields" }, effects: [] };
51202
+ default:
51203
+ return { state, effects: [] };
51204
+ }
51205
+ }
50368
51206
 
50369
51207
  // packages/core/tui/src/tui/render.ts
50370
51208
  var SECTION_PANE_RATIO = 1 / 3;
51209
+ var MIN_PANE_WIDTH = 16;
51210
+ var PROGRESS_WIDTH = 20;
51211
+ var OUTPUT_TAIL = 6;
51212
+ var SPLIT_MIN_WIDTH = 76;
51213
+ function statusMark(status, theme, fallback) {
51214
+ switch (status) {
51215
+ case "ready":
51216
+ return theme.fg("success", KXM_TUI_MARKS.ready);
51217
+ case "available":
51218
+ return theme.fg("accent", KXM_TUI_MARKS.available);
51219
+ case "blocked":
51220
+ return theme.fg("error", KXM_TUI_MARKS.blocked);
51221
+ default:
51222
+ return theme.dim(fallback);
51223
+ }
51224
+ }
51225
+ function cursorMark(active, theme) {
51226
+ return active ? theme.cursor(KXM_TUI_MARKS.cursor) : KXM_TUI_MARKS.blankCursor;
51227
+ }
51228
+ function paneSplit(width) {
51229
+ const third = Math.max(MIN_PANE_WIDTH, Math.floor(width * SECTION_PANE_RATIO));
51230
+ const sections = Math.min(third, Math.max(MIN_PANE_WIDTH, Math.floor(width / 2)));
51231
+ return { sections, fields: Math.max(MIN_PANE_WIDTH, width - sections - 1) };
51232
+ }
51233
+ function fieldIsPending(state, section, field) {
51234
+ return state.pending.includes(pendingKey({ source: section.source, sectionId: section.id, fieldId: field.id }));
51235
+ }
51236
+ function sectionRow(section, focused, pane, theme, width) {
51237
+ const cursor2 = cursorMark(focused && pane === "sections", theme);
51238
+ const flag2 = section.notice ? theme.fg(section.noticeLevel === "error" ? "error" : "warning", "!") : " ";
51239
+ const count = section.fields.length;
51240
+ const unset = section.fields.filter((field) => field.kind !== "info" && field.value === void 0).length;
51241
+ const right = unset > 0 ? `${unset} unset / ${count}` : `${count} field${count === 1 ? "" : "s"}`;
51242
+ return fitText(alignRight(`${cursor2} ${flag2} ${section.title}`, right, width), width);
51243
+ }
51244
+ function choiceSuffix(choice) {
51245
+ const parts = [];
51246
+ if (choice.detail) parts.push(choice.detail);
51247
+ if (choice.statusText) parts.push(choice.statusText);
51248
+ return parts.join(" \xB7 ");
51249
+ }
51250
+ function choiceRows(field, state, theme, width) {
51251
+ const shown = visibleChoices(field, state.filter);
51252
+ const lines = [];
51253
+ if (state.filtering) {
51254
+ lines.push(fitText(` ${theme.dim(`filter: ${state.filter || "(type to filter \xB7 esc clears)"}`)}`, width));
51255
+ }
51256
+ if (shown.length === 0) {
51257
+ lines.push(fitText(` ${theme.dim("no match")}`, width));
51258
+ return lines;
51259
+ }
51260
+ let position = -1;
51261
+ for (const group of groupedKxmTuiChoices(field)) {
51262
+ const filtered = shown.filter((choice) => (choice.group ?? "") === group.group);
51263
+ if (filtered.length === 0) continue;
51264
+ if (group.group) lines.push(fitText(` ${theme.key(group.group.toUpperCase())}`, width));
51265
+ for (const choice of filtered) {
51266
+ position += 1;
51267
+ lines.push(
51268
+ fitText(
51269
+ alignRight(
51270
+ `${cursorMark(position === state.choiceIndex, theme)} ${statusMark(choice.status, theme, KXM_TUI_MARKS.pending)} ${choice.label}`,
51271
+ choiceSuffix(choice),
51272
+ width
51273
+ ),
51274
+ width
51275
+ )
51276
+ );
51277
+ }
51278
+ }
51279
+ return lines;
51280
+ }
50371
51281
  var STEP_MARKS = {
50372
51282
  pending: "\xB7",
50373
51283
  satisfied: KXM_TUI_MARKS.passed,
@@ -50375,12 +51285,494 @@ var STEP_MARKS = {
50375
51285
  done: KXM_TUI_MARKS.passed,
50376
51286
  failed: KXM_TUI_MARKS.failed
50377
51287
  };
51288
+ function stepColor(state, theme) {
51289
+ switch (state) {
51290
+ case "running":
51291
+ return (text) => theme.fg("accent", text);
51292
+ case "done":
51293
+ return (text) => theme.fg("success", text);
51294
+ case "failed":
51295
+ return (text) => theme.fg("error", text);
51296
+ default:
51297
+ return theme.dim;
51298
+ }
51299
+ }
51300
+ function stepRows(steps, theme, width) {
51301
+ return steps.map((step) => {
51302
+ const paint = stepColor(step.state, theme);
51303
+ return fitText(alignRight(` ${paint(STEP_MARKS[step.state])} ${step.label}`, step.detail ?? "", width), width);
51304
+ });
51305
+ }
51306
+ function progressRow(field, theme, width) {
51307
+ const progress = field.progress;
51308
+ if (!progress) return "";
51309
+ const ratio = progress.ratio;
51310
+ const right = ratio === void 0 ? "working" : `${Math.round(ratio * 100)}%`;
51311
+ return fitText(alignRight(` ${progress.label}`, `${progressBar(ratio, PROGRESS_WIDTH)} ${right}`, width), width);
51312
+ }
51313
+ function focusedFieldDetail(field, state, theme, width) {
51314
+ const lines = [];
51315
+ if (field.keyPath) lines.push(fitText(` ${theme.dim(`key: ${field.keyPath}`)}`, width));
51316
+ for (const line of wrapPlain(field.detail ?? "", width - 3)) {
51317
+ if (line) lines.push(fitText(` ${theme.dim(line)}`, width));
51318
+ }
51319
+ for (const line of wrapPlain(field.statusText ?? "", width - 3)) {
51320
+ if (line) lines.push(fitText(` ${theme.fg("error", line)}`, width));
51321
+ }
51322
+ const progress = progressRow(field, theme, width);
51323
+ if (progress) lines.push(progress);
51324
+ if (field.steps?.length) lines.push(...stepRows(field.steps, theme, width));
51325
+ if (field.output?.length) {
51326
+ lines.push(...field.output.slice(-OUTPUT_TAIL).map((line) => fitText(` ${theme.dim(line)}`, width)));
51327
+ }
51328
+ if (field.actions?.length) {
51329
+ lines.push(fitText(` ${theme.key(field.actions.map((action) => `${action.key} ${action.label}`).join(" \xB7 "))}`, width));
51330
+ }
51331
+ if (state.mode === "choices") lines.push(...choiceRows(field, state, theme, width));
51332
+ if (state.mode === "edit" || field.awaitingInput) {
51333
+ const draft = state.mode === "edit" ? `${state.draft.slice(0, state.caret)}|${state.draft.slice(state.caret)}` : "|";
51334
+ lines.push(fitText(` ${theme.dim("\u203A")} ${draft}`, width));
51335
+ }
51336
+ return lines;
51337
+ }
51338
+ function fieldRows(section, state, theme, width, nowMs) {
51339
+ const lines = [];
51340
+ section.fields.forEach((field, index) => {
51341
+ const focused = state.pane === "fields" && state.fieldIndex === index;
51342
+ if (field.kind === "info") {
51343
+ lines.push(fitText(` ${theme.dim(field.label)} ${theme.dim(field.value ?? field.placeholder ?? "")}`, width));
51344
+ return;
51345
+ }
51346
+ const pending = fieldIsPending(state, section, field);
51347
+ const mark = statusMark(field.status, theme, pending ? "\u25CC" : KXM_TUI_MARKS.pending);
51348
+ const value = pending && field.value === void 0 ? theme.dim("saving\u2026") : field.value;
51349
+ const shown = value ?? theme.dim(field.placeholder ?? "not set");
51350
+ const age2 = field.updatedAt ? theme.dim(` (${formatAge(field.updatedAt, nowMs)})`) : "";
51351
+ lines.push(fitText(`${cursorMark(focused, theme)} ${mark} ${alignRight(field.label, `${shown}${age2}`, width - 4)}`, width));
51352
+ if (focused) lines.push(...focusedFieldDetail(field, state, theme, width));
51353
+ });
51354
+ return lines;
51355
+ }
51356
+ function noticeRows(section, theme, width) {
51357
+ if (!section?.notice) return [];
51358
+ const token = section.noticeLevel === "error" ? "error" : "warning";
51359
+ return wrapPlain(section.notice, width - 2).map((line) => fitText(theme.fg(token, line), width));
51360
+ }
51361
+ function singlePaneLines(sections, state, theme, width, nowMs) {
51362
+ const section = sections[state.sectionIndex];
51363
+ if (state.pane === "fields" && section) {
51364
+ return [
51365
+ fitText(theme.headerBg(alignRight(` ${section.title}`, section.source, width)), width),
51366
+ ...noticeRows(section, theme, width),
51367
+ ...fieldRows(section, state, theme, width, nowMs)
51368
+ ];
51369
+ }
51370
+ return [
51371
+ fitText(theme.headerBg(alignRight(" SECTIONS", `${sections.length}`, width)), width),
51372
+ ...sections.map((candidate, index) => sectionRow(candidate, index === state.sectionIndex, state.pane, theme, width))
51373
+ ];
51374
+ }
51375
+ function twoPaneLines(sections, state, theme, width, nowMs) {
51376
+ const split = paneSplit(width);
51377
+ const left = [fitText(theme.key(" SECTIONS"), split.sections)];
51378
+ sections.forEach(
51379
+ (candidate, index) => left.push(sectionRow(candidate, index === state.sectionIndex, state.pane, theme, split.sections))
51380
+ );
51381
+ const right = [];
51382
+ const section = sections[state.sectionIndex];
51383
+ if (section) {
51384
+ right.push(fitText(alignRight(` ${section.title}`, section.source, split.fields), split.fields));
51385
+ if (section.detail) right.push(fitText(` ${theme.dim(section.detail)}`, split.fields));
51386
+ right.push(...noticeRows(section, theme, split.fields));
51387
+ right.push(...fieldRows(section, state, theme, split.fields, nowMs));
51388
+ } else {
51389
+ right.push(fitText(theme.dim("no surface is published"), split.fields));
51390
+ }
51391
+ const rows = [];
51392
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
51393
+ rows.push(fitText(`${padRight(left[index] ?? "", split.sections)} ${right[index] ?? ""}`, width));
51394
+ }
51395
+ return rows;
51396
+ }
51397
+ function renderKxmTuiPanel(sections, state, theme, options, width = 120) {
51398
+ const nowMs = options.nowMs ?? Date.now();
51399
+ const title = `${theme.cursor(options.title)}${options.breadcrumb ? ` ${theme.dim(options.breadcrumb)}` : ""}`;
51400
+ const header = [fitText(title, width)];
51401
+ if (state.help) {
51402
+ const help = (options.helpLines ?? ["no help published"]).flatMap((line) => wrapPlain(line, Math.max(1, width - 2)));
51403
+ return [...header, ...help.map((line) => fitText(line, width)), fitText(theme.dim("esc close help"), width)];
51404
+ }
51405
+ const body = width < SPLIT_MIN_WIDTH ? singlePaneLines(sections, state, theme, width, nowMs) : twoPaneLines(sections, state, theme, width, nowMs);
51406
+ const footer = options.footer ? [fitText(theme.dim(options.footer), width)] : [];
51407
+ return [...header, ...body, ...footer];
51408
+ }
50378
51409
 
50379
51410
  // packages/core/tui/src/tui/panelComponent.ts
50380
51411
  var KXM_TUI_PANEL_HELP = Object.freeze([
50381
51412
  "\u2191\u2193 move \xB7 \u2190\u2192 or Tab panes \xB7 PgUp/PgDn section \xB7 enter edit or open list",
50382
51413
  "type to filter a list \xB7 x clears a value \xB7 esc backs out \xB7 q quit \xB7 h help"
50383
51414
  ]);
51415
+ function rowsComponent() {
51416
+ const stack = new VStack([], { gap: 0 });
51417
+ const scroll = new ScrollView(stack, { primary: true, overscroll: "contain", scrollbar: "auto" });
51418
+ const fill = (rows) => {
51419
+ stack.clear();
51420
+ for (const row of rows) stack.addChild(new TruncatedText(row, 0, 0));
51421
+ };
51422
+ return { stack, scroll, fill };
51423
+ }
51424
+ var KxmTuiPanelComponent = class {
51425
+ root = new VStack([], { gap: 0 });
51426
+ options;
51427
+ body = rowsComponent();
51428
+ sections;
51429
+ state;
51430
+ notice;
51431
+ unsubscribe;
51432
+ constructor(options) {
51433
+ this.options = options;
51434
+ this.sections = options.registry.getSections();
51435
+ this.state = initialKxmTuiPanelState(this.sections);
51436
+ this.unsubscribe = options.registry.subscribe(() => this.refresh());
51437
+ this.rebuild();
51438
+ }
51439
+ getState() {
51440
+ return this.state;
51441
+ }
51442
+ getSections() {
51443
+ return this.sections;
51444
+ }
51445
+ /** Point the cursor at a field, e.g. after opening a named section. */
51446
+ focus(sectionId, fieldId) {
51447
+ const sectionIndex = this.sections.findIndex((section) => section.id === sectionId);
51448
+ if (sectionIndex < 0) return;
51449
+ const fields = this.sections[sectionIndex]?.fields ?? [];
51450
+ const fieldIndex = fieldId ? Math.max(0, fields.findIndex((field) => field.id === fieldId)) : this.state.fieldIndex;
51451
+ this.state = { ...this.state, pane: "fields", sectionIndex, fieldIndex };
51452
+ this.rebuild();
51453
+ this.options.requestRender();
51454
+ }
51455
+ setStatus(message) {
51456
+ this.notice = message;
51457
+ this.rebuild();
51458
+ this.options.requestRender();
51459
+ }
51460
+ refresh() {
51461
+ const next = this.options.registry.getSections();
51462
+ this.sections = next;
51463
+ this.state = reconcileKxmTuiPanelState(this.state, next);
51464
+ this.rebuild();
51465
+ this.options.requestRender();
51466
+ }
51467
+ handleInput(data) {
51468
+ if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("u"))) {
51469
+ this.body.scroll.scrollBy(-(this.viewport() - 2));
51470
+ this.options.requestRender();
51471
+ return;
51472
+ }
51473
+ if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("d"))) {
51474
+ this.body.scroll.scrollBy(this.viewport() - 2);
51475
+ this.options.requestRender();
51476
+ return;
51477
+ }
51478
+ const step = reduceKxmTuiInput(this.state, this.sections, data);
51479
+ this.state = step.state;
51480
+ for (const effect of step.effects) this.run(effect);
51481
+ this.rebuild();
51482
+ this.options.requestRender();
51483
+ }
51484
+ viewport() {
51485
+ const height = this.body.scroll.viewportHeight;
51486
+ return height > 3 ? height : 10;
51487
+ }
51488
+ run(effect) {
51489
+ if (effect.type === "quit") {
51490
+ this.options.onQuit();
51491
+ return;
51492
+ }
51493
+ if (effect.type === "notice") {
51494
+ this.notice = effect.message;
51495
+ return;
51496
+ }
51497
+ this.notice = void 0;
51498
+ void this.options.registry.invoke(effect.invocation).then((result) => {
51499
+ if (!result.ok) this.setStatus(result.message ?? "the owner refused that change");
51500
+ else if (this.notice === void 0) this.setStatus(void 0);
51501
+ });
51502
+ }
51503
+ footer() {
51504
+ const status = this.notice ?? this.options.statusText?.();
51505
+ const base = "\u2191\u2193 \xB7 \u2190\u2192 panes \xB7 enter edit \xB7 x clear \xB7 esc back \xB7 h help \xB7 q quit";
51506
+ return status ? `${base} ${status}` : base;
51507
+ }
51508
+ rebuild() {
51509
+ const theme = this.options.theme;
51510
+ const width = this.options.getWidth?.() ?? 120;
51511
+ const title = new Box(1, 0, theme.headerBg);
51512
+ title.addChild(new HStack([
51513
+ { component: new Text(theme.cursor(this.options.title), 0, 0), basis: 12, shrink: 1, minSize: 6 },
51514
+ {
51515
+ component: new Text(
51516
+ this.options.breadcrumb ? theme.dim(fitText(this.options.breadcrumb, Math.max(4, width - 16))) : "",
51517
+ 0,
51518
+ 0
51519
+ ),
51520
+ grow: 1,
51521
+ shrink: 1,
51522
+ minSize: 8
51523
+ }
51524
+ ], { gap: 2 }));
51525
+ this.body.fill(
51526
+ renderKxmTuiPanel(this.sections, this.state, theme, {
51527
+ title: this.options.title,
51528
+ ...this.options.breadcrumb ? { breadcrumb: this.options.breadcrumb } : {},
51529
+ helpLines: this.options.helpLines ?? KXM_TUI_PANEL_HELP,
51530
+ footer: this.footer()
51531
+ }, width)
51532
+ );
51533
+ const panel = new Box(0, 0, theme.panelBg);
51534
+ panel.addChild(this.body.scroll);
51535
+ this.root.clear();
51536
+ this.root.addChild(title, { basis: "auto", shrink: 0 });
51537
+ this.root.addChild(panel, { basis: "auto", grow: 1, shrink: 1, minSize: 1 });
51538
+ }
51539
+ handleMouse(event) {
51540
+ return this.root.handleMouse?.(event);
51541
+ }
51542
+ invalidate() {
51543
+ this.root.invalidate();
51544
+ }
51545
+ render(width) {
51546
+ return this.root.render(width);
51547
+ }
51548
+ dispose() {
51549
+ this.unsubscribe();
51550
+ this.unsubscribe = () => void 0;
51551
+ }
51552
+ };
51553
+
51554
+ // packages/core/tui/src/services/registry.ts
51555
+ function describe(error) {
51556
+ if (error instanceof Error && error.message) return error.message.split(/\r?\n/u)[0].slice(0, KXM_TUI_LIMITS.detailMax);
51557
+ return String(error).split(/\r?\n/u)[0].slice(0, KXM_TUI_LIMITS.detailMax);
51558
+ }
51559
+ function normalizeSource(source) {
51560
+ if (!/^[A-Za-z0-9@][A-Za-z0-9@/._:-]*$/u.test(source) || source.length > KXM_TUI_LIMITS.sourceMax) {
51561
+ throw new Error(`invalid kxm tui surface source: ${source.slice(0, 64)}`);
51562
+ }
51563
+ return source;
51564
+ }
51565
+ function createKxmTuiRegistry(options = {}) {
51566
+ const contributions = /* @__PURE__ */ new Map();
51567
+ const notices = /* @__PURE__ */ new Map();
51568
+ const listeners = /* @__PURE__ */ new Set();
51569
+ const published = /* @__PURE__ */ new Map();
51570
+ const issues = /* @__PURE__ */ new Map();
51571
+ let generation = 0;
51572
+ let disposed = false;
51573
+ let epoch = 0;
51574
+ const report2 = (error, context) => {
51575
+ if (options.onInternalError) options.onInternalError(error, context);
51576
+ };
51577
+ const emit = (event) => {
51578
+ for (const listener of Array.from(listeners)) {
51579
+ try {
51580
+ listener(event);
51581
+ } catch (error) {
51582
+ report2(error, "listener");
51583
+ }
51584
+ }
51585
+ };
51586
+ function readOwner(source, force) {
51587
+ if (!force) {
51588
+ const cached = published.get(source);
51589
+ if (cached) return cached;
51590
+ }
51591
+ const contribution = contributions.get(source);
51592
+ if (!contribution) {
51593
+ published.set(source, []);
51594
+ return [];
51595
+ }
51596
+ let sections;
51597
+ try {
51598
+ const raw = contribution.listSections();
51599
+ const ownerIssues = validateKxmTuiSurface(raw);
51600
+ if (ownerIssues.length > 0) {
51601
+ issues.set(source, ownerIssues);
51602
+ sections = raw.filter((section) => Boolean(section) && typeof section === "object").map((section) => ({
51603
+ ...section,
51604
+ notice: section.notice ?? `published surface was refused: ${ownerIssues[0].code} at ${ownerIssues[0].path}`,
51605
+ noticeLevel: "error"
51606
+ }));
51607
+ } else {
51608
+ issues.delete(source);
51609
+ sections = raw;
51610
+ }
51611
+ } catch (error) {
51612
+ issues.set(source, [{ path: source, code: "list_sections_failed", message: describe(error) }]);
51613
+ report2(error, `listSections:${source}`);
51614
+ sections = [{ source, id: "error", title: source, order: 900, notice: describe(error), noticeLevel: "error", fields: [] }];
51615
+ }
51616
+ const pending = notices.get(source);
51617
+ const withNotice = pending !== void 0 && sections.length > 0 ? sections.map((section, index) => index === 0 ? { ...section, notice: pending, noticeLevel: "error" } : section) : sections;
51618
+ published.set(source, withNotice);
51619
+ return withNotice;
51620
+ }
51621
+ function merged() {
51622
+ const all = [];
51623
+ for (const source of contributions.keys()) all.push(...readOwner(source, false));
51624
+ return all.slice().sort((a, b2) => a.order - b2.order || a.source.localeCompare(b2.source) || a.title.localeCompare(b2.title));
51625
+ }
51626
+ return {
51627
+ register(contribution) {
51628
+ if (disposed) throw new Error("kxm tui registry is disposed");
51629
+ const source = normalizeSource(contribution.source);
51630
+ notices.delete(source);
51631
+ published.delete(source);
51632
+ issues.delete(source);
51633
+ contributions.set(source, { ...contribution, source });
51634
+ emit({ reason: "publish", source });
51635
+ },
51636
+ unregister(source) {
51637
+ contributions.delete(source);
51638
+ published.delete(source);
51639
+ issues.delete(source);
51640
+ notices.delete(source);
51641
+ emit({ reason: "publish", source });
51642
+ },
51643
+ getSections() {
51644
+ return merged();
51645
+ },
51646
+ getIssues() {
51647
+ return [...issues.values()].flat();
51648
+ },
51649
+ async invoke(invocation) {
51650
+ if (disposed) return { ok: false, message: "kxm tui registry is disposed" };
51651
+ const contribution = contributions.get(invocation.source);
51652
+ if (!contribution) {
51653
+ emit({ reason: "refused", source: invocation.source, message: "unknown owner" });
51654
+ return { ok: false, message: `${invocation.source} does not own a surface here` };
51655
+ }
51656
+ const handler = contribution.handlers[invocation.action];
51657
+ if (!handler) {
51658
+ const message = `${invocation.source} does not accept "${invocation.action}"`;
51659
+ emit({ reason: "refused", source: invocation.source, message });
51660
+ return { ok: false, message };
51661
+ }
51662
+ const currentEpoch = epoch;
51663
+ try {
51664
+ await handler({
51665
+ sectionId: invocation.sectionId,
51666
+ fieldId: invocation.fieldId,
51667
+ action: invocation.action,
51668
+ ...invocation.value === void 0 ? {} : { value: invocation.value }
51669
+ });
51670
+ } catch (error) {
51671
+ const failure = contribution.describeError?.(error, invocation.action) ?? describe(error);
51672
+ notices.set(invocation.source, failure);
51673
+ emit({ reason: "failed", source: invocation.source, message: failure });
51674
+ readOwner(invocation.source, true);
51675
+ generation += 1;
51676
+ return { ok: false, message: failure };
51677
+ }
51678
+ if (currentEpoch !== epoch) {
51679
+ readOwner(invocation.source, true);
51680
+ return { ok: false, message: "stale write result was discarded" };
51681
+ }
51682
+ notices.delete(invocation.source);
51683
+ readOwner(invocation.source, true);
51684
+ generation += 1;
51685
+ emit({ reason: "publish", source: invocation.source });
51686
+ return { ok: true };
51687
+ },
51688
+ subscribe(listener) {
51689
+ listeners.add(listener);
51690
+ return () => listeners.delete(listener);
51691
+ },
51692
+ getGeneration() {
51693
+ return generation;
51694
+ },
51695
+ hasSource(source) {
51696
+ return contributions.has(source);
51697
+ },
51698
+ listSources() {
51699
+ return [...contributions.keys()];
51700
+ },
51701
+ dispose() {
51702
+ if (disposed) return;
51703
+ disposed = true;
51704
+ epoch += 1;
51705
+ listeners.clear();
51706
+ contributions.clear();
51707
+ published.clear();
51708
+ issues.clear();
51709
+ notices.clear();
51710
+ }
51711
+ };
51712
+ }
51713
+
51714
+ // packages/core/tui/src/adapters/terminal.ts
51715
+ function renderKxmTuiPanelFrame(input) {
51716
+ const theme = input.theme ?? createPlainKxmTuiTheme();
51717
+ const lines = renderKxmTuiPanel(
51718
+ input.registry.getSections(),
51719
+ initialKxmTuiPanelState(input.registry.getSections()),
51720
+ theme,
51721
+ { title: input.title, footer: input.footer ?? "non-interactive frame", nowMs: input.nowMs ?? Date.now() },
51722
+ input.width ?? 100
51723
+ );
51724
+ return `${lines.map((line) => stripTerminalSequences(line)).join("\n")}
51725
+ `;
51726
+ }
51727
+ async function runKxmTuiPanel(input) {
51728
+ const theme = input.theme ?? createDefaultKxmTuiTheme();
51729
+ const tty = input.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
51730
+ if (!tty) {
51731
+ input.stdout(renderKxmTuiPanelFrame({
51732
+ registry: input.registry,
51733
+ title: input.title,
51734
+ theme,
51735
+ ...input.now === void 0 ? {} : { nowMs: input.now() },
51736
+ ...input.breadcrumb ? { footer: input.breadcrumb } : {}
51737
+ }));
51738
+ return 0;
51739
+ }
51740
+ const terminal = input.terminal ?? new ProcessTerminal();
51741
+ const tui = new TuiAltScreen(terminal, false, void 0, { mouse: true });
51742
+ let timer;
51743
+ let stopped = false;
51744
+ let finished = () => void 0;
51745
+ const done = new Promise((resolve35) => {
51746
+ finished = resolve35;
51747
+ });
51748
+ const stop = () => {
51749
+ if (stopped) return;
51750
+ stopped = true;
51751
+ if (timer) clearInterval(timer);
51752
+ input.abort?.removeEventListener("abort", stop);
51753
+ panel.dispose();
51754
+ tui.stop();
51755
+ finished();
51756
+ };
51757
+ const panel = new KxmTuiPanelComponent({
51758
+ registry: input.registry,
51759
+ theme,
51760
+ title: input.title,
51761
+ ...input.breadcrumb ? { breadcrumb: input.breadcrumb } : {},
51762
+ ...input.helpLines ? { helpLines: input.helpLines } : {},
51763
+ requestRender: () => tui.requestRender(),
51764
+ onQuit: stop,
51765
+ getWidth: () => terminal.columns
51766
+ });
51767
+ input.abort?.addEventListener("abort", stop, { once: true });
51768
+ tui.setLayoutRoot(panel.root);
51769
+ tui.setFocus(panel);
51770
+ tui.start();
51771
+ timer = setInterval(() => panel.refresh(), input.pollMs ?? 2e3);
51772
+ timer.unref?.();
51773
+ await done;
51774
+ return stopped && input.abort?.aborted ? 130 : 0;
51775
+ }
50384
51776
 
50385
51777
  // plugins/kxm/src/external-effects.ts
50386
51778
  init_database();
@@ -53056,7 +54448,7 @@ init_routing();
53056
54448
  init_prices();
53057
54449
  import { spawnSync as spawnSync15 } from "node:child_process";
53058
54450
  import { createHash as createHash19 } from "node:crypto";
53059
- import { existsSync as existsSync48, mkdtempSync as mkdtempSync3, readFileSync as readFileSync42, rmSync as rmSync13 } from "node:fs";
54451
+ import { existsSync as existsSync49, mkdtempSync as mkdtempSync3, readFileSync as readFileSync43, rmSync as rmSync13 } from "node:fs";
53060
54452
  import { tmpdir as tmpdir3 } from "node:os";
53061
54453
  import { basename as basename11, extname as extname3, join as join57, resolve as resolve33 } from "node:path";
53062
54454
  import { createInterface as createInterface3 } from "node:readline";
@@ -54301,6 +55693,304 @@ function executeSshRun(params) {
54301
55693
  // plugins/kxm/src/cli/system.ts
54302
55694
  init_config();
54303
55695
 
55696
+ // plugins/kxm/src/config-editor.ts
55697
+ var import_yaml21 = __toESM(require_dist(), 1);
55698
+ import { existsSync as existsSync46, readFileSync as readFileSync41 } from "node:fs";
55699
+ init_config();
55700
+ var KXM_CONFIG_EDITOR_SOURCE = "kxm/config";
55701
+ var INTEGER_TEXT = /^(0|[1-9][0-9]*)$/;
55702
+ var DECIMAL_TEXT = /^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$/;
55703
+ var TEXT_VALUE = /^[A-Za-z0-9@][A-Za-z0-9@._:/-]{0,127}$/;
55704
+ var SCREEN_CHOICES = ["agents", "tasks", "workflows", "plans", "inbox", "procs", "spend"];
55705
+ var SECTIONS = [
55706
+ {
55707
+ id: "user",
55708
+ title: "User",
55709
+ order: 10,
55710
+ fields: [
55711
+ { id: "user-theme", label: "Theme", keyPath: "user.theme", spec: { kind: "enum", choices: ["dark", "light", "minimal"] } },
55712
+ { id: "user-token-budget", label: "Token budget", keyPath: "user.tokenBudget", spec: { kind: "number", integer: true, min: 1, max: 2e6 } }
55713
+ ]
55714
+ },
55715
+ {
55716
+ id: "defaults",
55717
+ title: "Defaults",
55718
+ order: 20,
55719
+ fields: [
55720
+ { id: "defaults-workflow", label: "Workflow", keyPath: "defaults.workflow", spec: { kind: "text" } },
55721
+ { id: "defaults-harness", label: "Harness", keyPath: "defaults.harness", spec: { kind: "text" } }
55722
+ ]
55723
+ },
55724
+ {
55725
+ id: "dash",
55726
+ title: "Dashboard",
55727
+ order: 30,
55728
+ fields: [
55729
+ { id: "dash-screen", label: "Default screen", keyPath: "dash.defaultScreen", spec: { kind: "enum", choices: SCREEN_CHOICES } },
55730
+ { id: "dash-refresh", label: "Refresh interval", keyPath: "dash.refreshIntervalMs", spec: { kind: "number", integer: true, min: 250, max: 36e5 } },
55731
+ { id: "dash-auto-open", label: "Auto-open", keyPath: "dash.autoOpen", spec: { kind: "enum", choices: ["true", "false"] } }
55732
+ ]
55733
+ },
55734
+ {
55735
+ id: "sync",
55736
+ title: "Sync",
55737
+ order: 40,
55738
+ fields: [
55739
+ { id: "sync-tracker", label: "Default tracker", keyPath: "sync.defaultTracker", spec: { kind: "enum", choices: ["github", "jira", "none"] } }
55740
+ ]
55741
+ },
55742
+ {
55743
+ id: "hub",
55744
+ title: "Hub",
55745
+ order: 50,
55746
+ fields: [
55747
+ {
55748
+ id: "hub-auto-start",
55749
+ label: "Auto-start",
55750
+ keyPath: "hub.autoStart",
55751
+ spec: { kind: "enum", choices: ["background", "off"] },
55752
+ note: "Hub URLs and key references stay out of this panel."
55753
+ }
55754
+ ]
55755
+ },
55756
+ {
55757
+ id: "improvement",
55758
+ title: "Improvement",
55759
+ order: 60,
55760
+ fields: [
55761
+ { id: "improvement-policy", label: "Promotion policy", keyPath: "improvement.promotionPolicy", spec: { kind: "enum", choices: ["manual_pr", "critic_quorum", "auto_threshold"] } },
55762
+ { id: "improvement-half-life", label: "Telemetry half-life", keyPath: "improvement.telemetryHalfLifeDays", spec: { kind: "number", integer: false, min: 1e-6, max: 3650 } },
55763
+ { id: "improvement-min-runs", label: "Minimum runs", keyPath: "improvement.autoThreshold.minRuns", spec: { kind: "number", integer: true, min: 1, max: 1e6 } },
55764
+ { id: "improvement-min-pass-rate", label: "Minimum pass rate", keyPath: "improvement.autoThreshold.minPassRate", spec: { kind: "number", integer: false, min: 0, max: 1 } },
55765
+ { id: "improvement-min-cost-savings", label: "Minimum cost savings", keyPath: "improvement.autoThreshold.minCostSavings", spec: { kind: "number", integer: false, min: 0, max: 1e6 } }
55766
+ ]
55767
+ },
55768
+ {
55769
+ id: "routing",
55770
+ title: "Routing",
55771
+ order: 70,
55772
+ fields: [
55773
+ {
55774
+ id: "routing-shadow",
55775
+ label: "Shadow execution",
55776
+ keyPath: "routing.shadowExecution.enabled",
55777
+ spec: { kind: "enum", choices: ["true", "false"] },
55778
+ note: "Strategy, fallback, and context limits stay in role and route files."
55779
+ },
55780
+ { id: "routing-sample-rate", label: "Sample rate", keyPath: "routing.shadowExecution.sampleRate", spec: { kind: "number", integer: false, min: 0, max: 1 } },
55781
+ { id: "routing-breaker-mode", label: "Breaker mode", keyPath: "routing.circuitBreaker.mode", spec: { kind: "enum", choices: ["soft_demotion", "quarantine"] } },
55782
+ { id: "routing-failure-threshold", label: "Failure threshold", keyPath: "routing.circuitBreaker.failureThreshold", spec: { kind: "number", integer: true, min: 1, max: 1e4 } },
55783
+ { id: "routing-window-seconds", label: "Window seconds", keyPath: "routing.circuitBreaker.windowSeconds", spec: { kind: "number", integer: true, min: 1, max: 31536e3 } },
55784
+ { id: "routing-cooldown-seconds", label: "Cooldown seconds", keyPath: "routing.circuitBreaker.cooldownSeconds", spec: { kind: "number", integer: true, min: 0, max: 31536e3 } },
55785
+ { id: "routing-penalty", label: "Penalty multiplier", keyPath: "routing.circuitBreaker.penaltyMultiplier", spec: { kind: "number", integer: false, min: 1e-6, max: 1e3 } }
55786
+ ]
55787
+ },
55788
+ {
55789
+ id: "telemetry",
55790
+ title: "Telemetry",
55791
+ order: 80,
55792
+ fields: [
55793
+ { id: "telemetry-federated", label: "Federated", keyPath: "telemetry.federated", spec: { kind: "enum", choices: ["true", "false"] } },
55794
+ { id: "telemetry-anonymize", label: "Anonymize", keyPath: "telemetry.anonymize", spec: { kind: "enum", choices: ["true", "false"] } }
55795
+ ]
55796
+ }
55797
+ ];
55798
+ var KxmConfigEditorError = class extends Error {
55799
+ code = "config_edit_refused";
55800
+ constructor(message) {
55801
+ super(message);
55802
+ this.name = "KxmConfigEditorError";
55803
+ }
55804
+ };
55805
+ function createKxmConfigEditorContribution(target) {
55806
+ return {
55807
+ source: KXM_CONFIG_EDITOR_SOURCE,
55808
+ republishAfterWrite: true,
55809
+ listSections: () => listConfigEditorSections(target),
55810
+ handlers: {
55811
+ [KXM_TUI_SET_ACTION]: (input) => {
55812
+ writeConfigEditorValue(target, input.sectionId, input.fieldId, input.value);
55813
+ },
55814
+ [KXM_TUI_CLEAR_ACTION]: (input) => {
55815
+ clearConfigEditorValue(target, input.sectionId, input.fieldId);
55816
+ }
55817
+ },
55818
+ describeError: (error) => error instanceof Error && error.message ? error.message : "config edit failed"
55819
+ };
55820
+ }
55821
+ function scopeFile(target) {
55822
+ return kxmConfigFileForScope(target.cwd, target.scope, target.userConfigDir);
55823
+ }
55824
+ function scopePhrase(scope) {
55825
+ return scope === "project" ? "the project file .kxm/config.yaml" : "the user file config.yaml";
55826
+ }
55827
+ function listConfigEditorSections(target) {
55828
+ const config = loadKxmConfig(target.cwd, {
55829
+ ...target.userConfigDir === void 0 ? {} : { userConfigDir: target.userConfigDir },
55830
+ layers: target.scope === "user" ? "user" : "all"
55831
+ });
55832
+ const owned = readScopeDocument(scopeFile(target));
55833
+ const where = scopePhrase(target.scope);
55834
+ return SECTIONS.map((section) => {
55835
+ const fields = section.fields.map((field) => publishField(field, config, owned, where));
55836
+ const diagnosis = fields.find((field) => field.statusText)?.statusText;
55837
+ return {
55838
+ source: KXM_CONFIG_EDITOR_SOURCE,
55839
+ id: section.id,
55840
+ title: section.title,
55841
+ order: section.order,
55842
+ detail: `kxm.config.v1 \xB7 ${target.scope} scope. Re-read from disk after each write.`,
55843
+ ...diagnosis === void 0 ? {} : { notice: diagnosis, noticeLevel: "error" },
55844
+ fields
55845
+ };
55846
+ });
55847
+ }
55848
+ function publishField(field, config, owned, where) {
55849
+ const effective = getKxmConfigValue(config, field.keyPath);
55850
+ const stored = ownValue(owned, field.keyPath);
55851
+ const shown = displayScalar(effective);
55852
+ const inherited = !stored.present;
55853
+ const diagnosis = storedDiagnosis(field, stored, effective);
55854
+ const detail = [
55855
+ `Writes ${where}.`,
55856
+ inherited ? "Inherited." : "Set in this scope.",
55857
+ "Clear removes only this override.",
55858
+ field.note ?? "",
55859
+ diagnosis ?? ""
55860
+ ].filter((part) => part.length > 0).join(" ");
55861
+ const choices = field.spec.kind === "enum" ? field.spec.choices.map((choice) => ({ id: choice, label: choice })) : void 0;
55862
+ return {
55863
+ id: field.id,
55864
+ label: field.label,
55865
+ kind: field.spec.kind === "enum" ? "enum" : "text",
55866
+ keyPath: field.keyPath,
55867
+ detail,
55868
+ status: "ready",
55869
+ ...shown === void 0 ? {} : { value: shown },
55870
+ ...choices === void 0 ? {} : { choices },
55871
+ ...diagnosis === void 0 ? {} : { statusText: diagnosis }
55872
+ };
55873
+ }
55874
+ function displayScalar(value) {
55875
+ if (typeof value === "string" && value.length > 0) return value;
55876
+ if (typeof value === "boolean") return value ? "true" : "false";
55877
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
55878
+ return void 0;
55879
+ }
55880
+ function isBooleanEnum(spec) {
55881
+ return spec.kind === "enum" && spec.choices.length === 2 && spec.choices.every((choice) => choice === "true" || choice === "false");
55882
+ }
55883
+ function storedTypeMatches(field, value) {
55884
+ if (field.spec.kind === "number") {
55885
+ return typeof value === "number" && Number.isFinite(value) && (!field.spec.integer || Number.isInteger(value));
55886
+ }
55887
+ if (isBooleanEnum(field.spec)) return typeof value === "boolean";
55888
+ return typeof value === "string";
55889
+ }
55890
+ function storedDiagnosis(field, stored, effective) {
55891
+ if (!stored.present) return void 0;
55892
+ if (!storedTypeMatches(field, stored.value)) return "stored type does not match this field";
55893
+ if (field.spec.kind === "enum" && !isBooleanEnum(field.spec) && !field.spec.choices.includes(stored.value)) {
55894
+ return "stored value is not a supported scalar";
55895
+ }
55896
+ if (field.spec.kind === "number") {
55897
+ const value = stored.value;
55898
+ if (value < field.spec.min || value > field.spec.max) return "stored value is not a supported scalar";
55899
+ }
55900
+ if (stored.value !== effective) return "stored value is not a supported scalar";
55901
+ return void 0;
55902
+ }
55903
+ function writeConfigEditorValue(target, sectionId, fieldId, raw) {
55904
+ const field = requireField(sectionId, fieldId);
55905
+ const value = parseEditorValue(field, raw);
55906
+ setKxmConfigValue(target.cwd, field.keyPath, value, {
55907
+ scope: target.scope,
55908
+ ...target.userConfigDir === void 0 ? {} : { userConfigDir: target.userConfigDir }
55909
+ });
55910
+ }
55911
+ function clearConfigEditorValue(target, sectionId, fieldId) {
55912
+ const field = requireField(sectionId, fieldId);
55913
+ deleteKxmConfigValue(target.cwd, field.keyPath, {
55914
+ scope: target.scope,
55915
+ ...target.userConfigDir === void 0 ? {} : { userConfigDir: target.userConfigDir }
55916
+ });
55917
+ }
55918
+ function requireField(sectionId, fieldId) {
55919
+ const section = SECTIONS.find((item) => item.id === sectionId);
55920
+ const field = section?.fields.find((item) => item.id === fieldId);
55921
+ if (!field) throw new KxmConfigEditorError("unknown configuration field");
55922
+ return field;
55923
+ }
55924
+ function parseEditorValue(field, raw) {
55925
+ if (raw === void 0) throw new KxmConfigEditorError("a value is required");
55926
+ if (raw.length === 0 || raw.length > 128 || /[\u0000-\u001f\u007f]/u.test(raw)) {
55927
+ throw new KxmConfigEditorError("value must be a single bounded line");
55928
+ }
55929
+ if (field.spec.kind === "enum") {
55930
+ if (!field.spec.choices.includes(raw)) throw new KxmConfigEditorError("value is not a supported choice");
55931
+ if (isBooleanEnum(field.spec)) return raw === "true";
55932
+ return raw;
55933
+ }
55934
+ if (field.spec.kind === "text") {
55935
+ if (!TEXT_VALUE.test(raw)) throw new KxmConfigEditorError("value must be a single bounded line");
55936
+ return raw;
55937
+ }
55938
+ if (!(field.spec.integer ? INTEGER_TEXT : DECIMAL_TEXT).test(raw)) {
55939
+ throw new KxmConfigEditorError("value must be a finite number in range");
55940
+ }
55941
+ const parsed = Number(raw);
55942
+ if (!Number.isFinite(parsed) || field.spec.integer && !Number.isInteger(parsed) || parsed < field.spec.min || parsed > field.spec.max) {
55943
+ throw new KxmConfigEditorError("value must be a finite number in range");
55944
+ }
55945
+ return parsed;
55946
+ }
55947
+ function readScopeDocument(file) {
55948
+ if (!existsSync46(file)) return void 0;
55949
+ const parsed = (0, import_yaml21.parse)(readFileSync41(file, "utf8"));
55950
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
55951
+ throw new KxmConfigEditorError(`config file ${file} must be a mapping`);
55952
+ }
55953
+ return parsed;
55954
+ }
55955
+ function ownValue(root, keyPath) {
55956
+ if (!root) return { present: false, value: void 0 };
55957
+ let cursor2 = root;
55958
+ for (const part of keyPath.split(".")) {
55959
+ if (!cursor2 || typeof cursor2 !== "object" || Array.isArray(cursor2)) return { present: false, value: void 0 };
55960
+ const record = cursor2;
55961
+ if (!Object.prototype.hasOwnProperty.call(record, part)) return { present: false, value: void 0 };
55962
+ cursor2 = record[part];
55963
+ }
55964
+ return { present: true, value: cursor2 };
55965
+ }
55966
+ var EDITOR_HELP = [
55967
+ "\u2191\u2193 move \xB7 enter edits \xB7 x clears this scope's override \xB7 q quits",
55968
+ "Writes are checked, saved, then re-read. A refused edit leaves the file unchanged."
55969
+ ];
55970
+ var FRAME_NOW_MS = Date.UTC(2026, 9, 11);
55971
+ async function runKxmConfigEditor(input) {
55972
+ const registry = createKxmTuiRegistry();
55973
+ try {
55974
+ registry.register(createKxmConfigEditorContribution(input.target));
55975
+ registry.getSections();
55976
+ const failed2 = registry.getIssues()[0];
55977
+ if (failed2) throw new KxmConfigEditorError(failed2.message);
55978
+ const interactive = input.isTty && !input.dryRun;
55979
+ return await runKxmTuiPanel({
55980
+ registry,
55981
+ title: "KXM configuration",
55982
+ breadcrumb: `kxm.config.v1 \xB7 ${input.target.scope} scope`,
55983
+ helpLines: EDITOR_HELP,
55984
+ stdout: input.stdout,
55985
+ isTty: interactive,
55986
+ ...interactive ? {} : { theme: createPlainKxmTuiTheme() },
55987
+ now: () => FRAME_NOW_MS
55988
+ });
55989
+ } finally {
55990
+ registry.dispose();
55991
+ }
55992
+ }
55993
+
54304
55994
  // plugins/kxm/src/autocomplete.ts
54305
55995
  var TOP_LEVEL_COMMANDS = [
54306
55996
  "init",
@@ -54354,7 +56044,7 @@ var SUBCOMMANDS = {
54354
56044
  memory: ["brief", "note", "sync"],
54355
56045
  routing: ["report", "benchmark"],
54356
56046
  hub: ["view", "start", "stop", "bind", "unbind"],
54357
- config: ["get", "set", "list"],
56047
+ config: ["get", "set", "edit", "list"],
54358
56048
  goal: ["create", "list", "get"],
54359
56049
  task: ["create", "list", "get", "run", "sync"],
54360
56050
  studio: ["layout", "serve"],
@@ -54476,7 +56166,7 @@ _kxm() {
54476
56166
  _arguments '1:shell:(bash zsh fish)'
54477
56167
  ;;
54478
56168
  config)
54479
- _arguments '1:action:(get set list)'
56169
+ _arguments '1:action:(get set edit list)'
54480
56170
  ;;
54481
56171
  runs)
54482
56172
  _arguments '1:subcommand:(status cancel list)'
@@ -54520,7 +56210,7 @@ complete -c kxm -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
54520
56210
  complete -c kxm -n "__fish_seen_subcommand_from dash" -l screen -a "agents tasks workflows plans inbox procs spend"
54521
56211
 
54522
56212
  # Config subcommand
54523
- complete -c kxm -n "__fish_seen_subcommand_from config" -a "get set list"
56213
+ complete -c kxm -n "__fish_seen_subcommand_from config" -a "get set edit list"
54524
56214
 
54525
56215
  # Runs subcommand
54526
56216
  complete -c kxm -n "__fish_seen_subcommand_from runs" -a "status cancel list"
@@ -54540,7 +56230,7 @@ complete -c kxm -n "__fish_seen_subcommand_from goal" -a "create list get"
54540
56230
  }
54541
56231
 
54542
56232
  // plugins/kxm/src/completion-install.ts
54543
- import { existsSync as existsSync46, mkdirSync as mkdirSync33, readFileSync as readFileSync41, writeFileSync as writeFileSync28 } from "node:fs";
56233
+ import { existsSync as existsSync47, mkdirSync as mkdirSync33, readFileSync as readFileSync42, writeFileSync as writeFileSync28 } from "node:fs";
54544
56234
  import { homedir as homedir9 } from "node:os";
54545
56235
  import { basename as basename10, delimiter, dirname as dirname24, isAbsolute as isAbsolute11, join as join55, resolve as resolve32 } from "node:path";
54546
56236
  var COMPLETION_MARKER = "# kxm completion";
@@ -54569,7 +56259,7 @@ function completionScriptPath(shell, options = {}) {
54569
56259
  function bashRcCandidate(options) {
54570
56260
  const home = effectiveHome(options);
54571
56261
  const candidates = [join55(home, ".bashrc"), join55(home, ".bash_profile")];
54572
- const existing = candidates.find((candidate) => existsSync46(candidate));
56262
+ const existing = candidates.find((candidate) => existsSync47(candidate));
54573
56263
  return existing ?? candidates[0];
54574
56264
  }
54575
56265
  function zshRcCandidate(options) {
@@ -54614,13 +56304,13 @@ function installShellCompletion(shellInput, options = {}) {
54614
56304
  const scriptPath = completionScriptPath(shell, options);
54615
56305
  const { rcFile } = completionRcTarget(shell, options);
54616
56306
  const script = generateShellCompletion(shell);
54617
- const existingScript = existsSync46(scriptPath);
54618
- const existingScriptMatches = existingScript && readFileSync41(scriptPath, "utf8") === script;
56307
+ const existingScript = existsSync47(scriptPath);
56308
+ const existingScriptMatches = existingScript && readFileSync42(scriptPath, "utf8") === script;
54619
56309
  let rcModified = false;
54620
56310
  let alreadyInstalled = false;
54621
56311
  if (shell === "fish") {
54622
56312
  const fishTarget = fishCompletionTarget(shell, options);
54623
- const fishInstalled = existsSync46(fishTarget) && readFileSync41(fishTarget, "utf8") === script;
56313
+ const fishInstalled = existsSync47(fishTarget) && readFileSync42(fishTarget, "utf8") === script;
54624
56314
  alreadyInstalled = fishInstalled;
54625
56315
  if (!options.dryRun && (!fishInstalled || options.overwrite)) {
54626
56316
  mkdirSync33(resolve32(fishTarget, ".."), { recursive: true });
@@ -54637,7 +56327,7 @@ function installShellCompletion(shellInput, options = {}) {
54637
56327
  }
54638
56328
  if (rcFile) {
54639
56329
  const line = sourceLine(shell, scriptPath);
54640
- const rcContent = existsSync46(rcFile) ? readFileSync41(rcFile, "utf8") : "";
56330
+ const rcContent = existsSync47(rcFile) ? readFileSync42(rcFile, "utf8") : "";
54641
56331
  alreadyInstalled = rcContent.includes(line) || rcContent.includes(`${COMPLETION_MARKER}`);
54642
56332
  }
54643
56333
  if (!options.dryRun) {
@@ -54653,7 +56343,7 @@ ${marker}
54653
56343
  ${line}
54654
56344
  `;
54655
56345
  mkdirSync33(resolve32(rcFile, ".."), { recursive: true });
54656
- writeFileSync28(rcFile, (existsSync46(rcFile) ? readFileSync41(rcFile, "utf8") : "") + stanza, { encoding: "utf8" });
56346
+ writeFileSync28(rcFile, (existsSync47(rcFile) ? readFileSync42(rcFile, "utf8") : "") + stanza, { encoding: "utf8" });
54657
56347
  rcModified = true;
54658
56348
  }
54659
56349
  }
@@ -54670,13 +56360,13 @@ function kxmBinDir(env = process.env) {
54670
56360
  const argv1 = env.KXM_ENTRY ?? process.argv[1];
54671
56361
  if (argv1 && isAbsolute11(argv1)) {
54672
56362
  const dir = dirname24(argv1);
54673
- if (existsSync46(join55(dir, process.platform === "win32" ? "kxm.cmd" : "kxm"))) return dir;
56363
+ if (existsSync47(join55(dir, process.platform === "win32" ? "kxm.cmd" : "kxm"))) return dir;
54674
56364
  }
54675
56365
  const pathEnv = env.PATH ?? "";
54676
56366
  for (const part of pathEnv.split(delimiter)) {
54677
56367
  if (!part) continue;
54678
56368
  const candidate = resolve32(part, process.platform === "win32" ? "kxm.cmd" : "kxm");
54679
- if (existsSync46(candidate)) return resolve32(part);
56369
+ if (existsSync47(candidate)) return resolve32(part);
54680
56370
  }
54681
56371
  return void 0;
54682
56372
  }
@@ -54694,7 +56384,7 @@ function installPathEntry(shellInput, options = {}) {
54694
56384
  }
54695
56385
  const { rcFile } = completionRcTarget(shell, options);
54696
56386
  const line = pathLine(binDir);
54697
- const rcContent = existsSync46(rcFile ?? "") ? readFileSync41(rcFile, "utf8") : "";
56387
+ const rcContent = existsSync47(rcFile ?? "") ? readFileSync42(rcFile, "utf8") : "";
54698
56388
  const alreadyInstalled = rcContent.includes(binDir) || (options.env?.PATH ?? process.env.PATH ?? "").split(delimiter).includes(binDir);
54699
56389
  if (!options.dryRun && rcFile && !alreadyInstalled) {
54700
56390
  mkdirSync33(dirname24(rcFile), { recursive: true });
@@ -54707,9 +56397,9 @@ ${line}
54707
56397
  }
54708
56398
 
54709
56399
  // plugins/kxm/src/init-guide-setup.ts
54710
- var import_yaml21 = __toESM(require_dist(), 1);
56400
+ var import_yaml22 = __toESM(require_dist(), 1);
54711
56401
  init_routes();
54712
- import { existsSync as existsSync47, mkdirSync as mkdirSync34, writeFileSync as writeFileSync29 } from "node:fs";
56402
+ import { existsSync as existsSync48, mkdirSync as mkdirSync34, writeFileSync as writeFileSync29 } from "node:fs";
54713
56403
  import { dirname as dirname25, join as join56 } from "node:path";
54714
56404
  var ADMITTED_GUIDE_BINDINGS = Object.freeze({
54715
56405
  "anthropic/claude-fable-5.1": { harness: "claude", provider: "anthropic", model: "fable" },
@@ -55142,7 +56832,7 @@ function renderGuideSetupFiles(projectRoot, plan) {
55142
56832
  if (binding.harness === "pi" && !piMatch) {
55143
56833
  files.push({
55144
56834
  path: join56(projectRoot, ".kxm", "agents", `${role}.yaml`),
55145
- content: (0, import_yaml21.stringify)(agentDocument(role, stage))
56835
+ content: (0, import_yaml22.stringify)(agentDocument(role, stage))
55146
56836
  });
55147
56837
  continue;
55148
56838
  }
@@ -55167,15 +56857,15 @@ function renderGuideSetupFiles(projectRoot, plan) {
55167
56857
  }
55168
56858
  files.push({
55169
56859
  path: join56(projectRoot, ".kxm", "agents", `${role}.yaml`),
55170
- content: (0, import_yaml21.stringify)(agentDocument(role, stage))
56860
+ content: (0, import_yaml22.stringify)(agentDocument(role, stage))
55171
56861
  });
55172
56862
  files.push({
55173
56863
  path: join56(projectRoot, ".kxm", "models", `${routeId2}.yaml`),
55174
- content: (0, import_yaml21.stringify)(modelDocument)
56864
+ content: (0, import_yaml22.stringify)(modelDocument)
55175
56865
  });
55176
56866
  files.push({
55177
56867
  path: join56(projectRoot, ".kxm", "roles", `${role}.yaml`),
55178
- content: (0, import_yaml21.stringify)({
56868
+ content: (0, import_yaml22.stringify)({
55179
56869
  schema: "kxm.role.v2",
55180
56870
  id: role,
55181
56871
  purpose: writer ? "writer" : "experiment",
@@ -55188,7 +56878,7 @@ function renderGuideSetupFiles(projectRoot, plan) {
55188
56878
  for (const workflow of plan.workflows) {
55189
56879
  files.push({
55190
56880
  path: join56(projectRoot, ".kxm", "workflows", `${workflow.slug}.yaml`),
55191
- content: (0, import_yaml21.stringify)(workflowDocument(workflow))
56881
+ content: (0, import_yaml22.stringify)(workflowDocument(workflow))
55192
56882
  });
55193
56883
  }
55194
56884
  return files;
@@ -55206,14 +56896,14 @@ function mergeGuideRouteAdmission(projectRoot, plan) {
55206
56896
  policy.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
55207
56897
  const path4 = join56(projectRoot, ".kxm", "routes.yaml");
55208
56898
  mkdirSync34(dirname25(path4), { recursive: true });
55209
- writeFileSync29(path4, (0, import_yaml21.stringify)(policy), "utf8");
56899
+ writeFileSync29(path4, (0, import_yaml22.stringify)(policy), "utf8");
55210
56900
  return added;
55211
56901
  }
55212
56902
  function writeGuideSetupFiles(files) {
55213
56903
  const written = [];
55214
56904
  const existed = [];
55215
56905
  for (const file of files) {
55216
- if (existsSync47(file.path)) {
56906
+ if (existsSync48(file.path)) {
55217
56907
  existed.push(file.path);
55218
56908
  continue;
55219
56909
  }
@@ -55294,7 +56984,7 @@ function applyKxmPackageUpdate(runtime, notice) {
55294
56984
  for (const step of planned) {
55295
56985
  if (step.kind === "verify") {
55296
56986
  if (!verifyReleaseAssetDigest(step.path, step.sha256)) {
55297
- const actual = existsSync48(step.path) ? createHash19("sha256").update(readFileSync42(step.path)).digest("hex") : "missing";
56987
+ const actual = existsSync49(step.path) ? createHash19("sha256").update(readFileSync43(step.path)).digest("hex") : "missing";
55298
56988
  return {
55299
56989
  ok: false,
55300
56990
  error: "release_digest_mismatch",
@@ -55622,12 +57312,12 @@ async function cmdValidate(runtime, fileFlag) {
55622
57312
  }
55623
57313
  const selectedFile = explicitFile || configuredFile;
55624
57314
  const file = selectedFile ? resolve33(runtime.cwd, selectedFile) : void 0;
55625
- if (file && !existsSync48(file)) {
57315
+ if (file && !existsSync49(file)) {
55626
57316
  printWorker(runtime, worker, { ok: false, command: "validate", error: "file_not_found", file }, `workflow file not found: ${file}`);
55627
57317
  return 1;
55628
57318
  }
55629
57319
  try {
55630
- const raw = file ? readFileSync42(file, "utf8") : inline;
57320
+ const raw = file ? readFileSync43(file, "utf8") : inline;
55631
57321
  if (explicitFile && file && !raw.trimStart().startsWith("[")) {
55632
57322
  const value = parseRestrictedYaml2(raw, file);
55633
57323
  const issues = new KxmSchemaRegistry().validate("workflow", value, file);
@@ -55802,6 +57492,37 @@ async function cmdConfigSet(runtime, key, value, options) {
55802
57492
  return 1;
55803
57493
  }
55804
57494
  }
57495
+ async function cmdConfigEdit(runtime, options, isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
57496
+ const scope = options.scope;
57497
+ if (scope !== "project" && scope !== "user") {
57498
+ const detail = "config edit requires --scope project or --scope user";
57499
+ print(runtime.io, runtime.json, { ok: false, command: "config edit", error: "config_edit_unsupported", detail }, detail);
57500
+ return 2;
57501
+ }
57502
+ if (runtime.json) {
57503
+ const detail = "config edit does not print machine-readable output";
57504
+ print(runtime.io, runtime.json, { ok: false, command: "config edit", error: "config_edit_unsupported", detail }, detail);
57505
+ return 2;
57506
+ }
57507
+ try {
57508
+ const userConfigDir = userConfigDirForEnv(runtime.env);
57509
+ return await runKxmConfigEditor({
57510
+ target: {
57511
+ cwd: runtime.cwd,
57512
+ scope,
57513
+ ...userConfigDir === void 0 ? {} : { userConfigDir }
57514
+ },
57515
+ stdout: (text) => runtime.io.stdout(text),
57516
+ isTty,
57517
+ dryRun: runtime.dryRun
57518
+ });
57519
+ } catch (error) {
57520
+ const message = error instanceof Error ? error.message : String(error);
57521
+ runtime.io.stderr(`config edit failed: ${message}
57522
+ `);
57523
+ return 1;
57524
+ }
57525
+ }
55805
57526
  async function cmdConfigList(runtime) {
55806
57527
  try {
55807
57528
  const config = loadKxmConfig(runtime.cwd);
@@ -55876,9 +57597,9 @@ async function maybeOfferCompletionInstall(runtime) {
55876
57597
  if (!pathNeeded) {
55877
57598
  const scriptPath = completionScriptPath(shell, { env: runtime.env, configDir: runtime.env.KXM_USER_CONFIG_DIR });
55878
57599
  const { rcFile } = completionRcTarget(shell, { env: runtime.env });
55879
- if (rcFile && existsSync48(rcFile)) {
57600
+ if (rcFile && existsSync49(rcFile)) {
55880
57601
  try {
55881
- if (readFileSync42(rcFile, "utf8").includes(scriptPath)) return;
57602
+ if (readFileSync43(rcFile, "utf8").includes(scriptPath)) return;
55882
57603
  } catch {
55883
57604
  }
55884
57605
  }
@@ -56253,6 +57974,7 @@ var DRY_RUN_COMMANDS = /* @__PURE__ */ new Set([
56253
57974
  "hub unbind",
56254
57975
  "config get",
56255
57976
  "config set",
57977
+ "config edit",
56256
57978
  "config list",
56257
57979
  "goal create",
56258
57980
  "goal list",
@@ -56825,7 +58547,7 @@ function createProgram(ctx, result, argv) {
56825
58547
  });
56826
58548
  const gate = addGlobalOptions(program2.command("gate").description("Validate definitions and operate evidence gates"));
56827
58549
  gate.helpCommand("help", "Show gate help");
56828
- addGlobalOptions(gate.command("validate").description("Parse workflow definitions without printing secrets")).option("--file <path>", "Workflow definition file").action(async function validateAction(options) {
58550
+ addGlobalOptions(gate.command("validate").description("Parse workflow definitions without printing secrets")).option("--file <path>", "Workflow definition file").action(async function validateAction2(options) {
56829
58551
  result.code = await cmdValidate(runtimeFrom(ctx, this), options.file);
56830
58552
  });
56831
58553
  addGlobalOptions(gate.command("artifacts-exist").description("Verify a non-empty file under workspace assets")).requiredOption("--path <file>", "Artifact file under workspace assets").action(async function artifactsExistAction(options) {
@@ -56953,6 +58675,9 @@ function createProgram(ctx, result, argv) {
56953
58675
  addGlobalOptions(configCmd.command("set <key> <value>").description("Set a configuration value")).option("--scope <scope>", "Configuration scope: user or project (default: project)", "project").action(async function configSetAction(key, value, options) {
56954
58676
  result.code = await cmdConfigSet(runtimeFrom(ctx, this), key, value, options);
56955
58677
  });
58678
+ addGlobalOptions(configCmd.command("edit").description("Edit supported non-secret settings in the terminal panel")).option("--scope <scope>", "Write scope: project or user").action(async function configEditAction(options) {
58679
+ result.code = await cmdConfigEdit(runtimeFrom(ctx, this), options);
58680
+ });
56956
58681
  addGlobalOptions(configCmd.command("list", { isDefault: true }).description("List resolved configuration values")).action(async function configListAction() {
56957
58682
  result.code = await cmdConfigList(runtimeFrom(ctx, this));
56958
58683
  });