@treeseed/cli 0.4.12 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
- import { Box, render, Text, useApp, useInput, useWindowSize } from "ink";
1
+ import { spawnSync } from "node:child_process";
2
+ import { Box, render, Text, useApp, useInput, usePaste, useWindowSize } from "ink";
2
3
  import React from "react";
3
4
  import {
4
5
  AppFrame,
@@ -14,25 +15,71 @@ import {
14
15
  SecondaryButton,
15
16
  StatusBar,
16
17
  TextInputField,
17
- TopTabs,
18
+ formatSecretMaskedValue,
18
19
  truncateLine,
19
20
  wrapText
20
21
  } from "../ui/framework.js";
21
22
  import { useTerminalMouse } from "../ui/mouse.js";
22
- const CONFIG_FILTERS = ["all", "local", "staging", "prod"];
23
- const CONFIG_VIEW_MODES = ["startup", "full"];
23
+ const FULL_CONFIG_FILTERS = ["local", "staging", "prod"];
24
24
  function maskValue(value) {
25
25
  if (!value) {
26
26
  return "(unset)";
27
27
  }
28
- if (value.length <= 8) {
29
- return "********";
30
- }
31
- return `${value.slice(0, 3)}...${value.slice(-3)}`;
28
+ return formatSecretMaskedValue(value);
32
29
  }
33
30
  function scopeOrder(scope) {
34
31
  return ["local", "staging", "prod"].indexOf(scope);
35
32
  }
33
+ function providerWorkflowKey(entry) {
34
+ const id = entry.id.toUpperCase();
35
+ if (id.startsWith("GH_") || id.includes("GITHUB")) {
36
+ return "github";
37
+ }
38
+ if (id.startsWith("CLOUDFLARE_") || id.includes("TURNSTILE") || entry.group === "cloudflare") {
39
+ return "cloudflare";
40
+ }
41
+ if (id.startsWith("RAILWAY_") || entry.group === "railway") {
42
+ return "railway";
43
+ }
44
+ if (entry.group === "local-development") {
45
+ return "local-development";
46
+ }
47
+ if (entry.group === "forms") {
48
+ return "forms";
49
+ }
50
+ if (entry.group === "smtp") {
51
+ return "smtp";
52
+ }
53
+ if (entry.group === "auth") {
54
+ return "auth-core";
55
+ }
56
+ return entry.group;
57
+ }
58
+ function providerWorkflowRank(entry) {
59
+ const order = ["auth-core", "github", "cloudflare", "railway", "local-development", "forms", "smtp"];
60
+ const index = order.indexOf(providerWorkflowKey(entry));
61
+ return index === -1 ? order.length : index;
62
+ }
63
+ function normalizedClusterKey(entry) {
64
+ const provider = providerWorkflowKey(entry);
65
+ const cluster = entry.cluster.trim().toLowerCase();
66
+ if (provider === "cloudflare") {
67
+ if (entry.id.includes("API_TOKEN") || entry.id.includes("ACCOUNT_ID")) {
68
+ return "cloudflare-account";
69
+ }
70
+ if (entry.id.includes("TURNSTILE") || cluster.includes("turnstile")) {
71
+ return "cloudflare-turnstile";
72
+ }
73
+ return `cloudflare-${cluster}`;
74
+ }
75
+ if (provider === "railway") {
76
+ if (entry.id.includes("API_TOKEN")) {
77
+ return "railway-access";
78
+ }
79
+ return `railway-${cluster}`;
80
+ }
81
+ return `${provider}-${cluster}`;
82
+ }
36
83
  function resolveFirstNonEmptyValue(scopes, entriesByScope, entryId, field) {
37
84
  for (const scope of scopes) {
38
85
  const entry = entriesByScope[scope].find((candidate) => candidate.id === entryId);
@@ -43,15 +90,78 @@ function resolveFirstNonEmptyValue(scopes, entriesByScope, entryId, field) {
43
90
  }
44
91
  return "";
45
92
  }
93
+ function resolveSharedEntryValue(relevantScopes, requiredScopes, entriesByScope, entryId, field, options = {}) {
94
+ const preferredScopes = requiredScopes.length > 0 ? requiredScopes : relevantScopes;
95
+ const preferredValue = resolveFirstNonEmptyValue(preferredScopes, entriesByScope, entryId, field);
96
+ if (preferredValue.length > 0) {
97
+ return preferredValue;
98
+ }
99
+ if (options.fallbackToRelevant === false) {
100
+ return "";
101
+ }
102
+ return resolveFirstNonEmptyValue(relevantScopes, entriesByScope, entryId, field);
103
+ }
104
+ function resolveCurrentConfigValue(context, overrides, entryId, scope = "local") {
105
+ const sharedOverrideKey = `shared:${entryId}`;
106
+ if (sharedOverrideKey in overrides) {
107
+ return overrides[sharedOverrideKey] ?? "";
108
+ }
109
+ const scopedOverrideKey = `${scope}:${entryId}`;
110
+ if (scopedOverrideKey in overrides) {
111
+ return overrides[scopedOverrideKey] ?? "";
112
+ }
113
+ for (const candidateScope of [scope, ...context.scopes.filter((candidate) => candidate !== scope)]) {
114
+ const entry = context.entriesByScope[candidateScope].find((candidate) => candidate.id === entryId);
115
+ if (entry?.storage === "shared") {
116
+ const overrideKey = `shared:${entryId}`;
117
+ if (overrideKey in overrides) {
118
+ return overrides[overrideKey] ?? "";
119
+ }
120
+ }
121
+ if (typeof entry?.currentValue === "string" && entry.currentValue.length > 0) {
122
+ return entry.currentValue;
123
+ }
124
+ }
125
+ return "";
126
+ }
46
127
  function hasUsableValue(value) {
47
128
  return typeof value === "string" && value.trim().length > 0;
48
129
  }
130
+ function isConfigValueValid(entry, value) {
131
+ if (!hasUsableValue(value)) {
132
+ return false;
133
+ }
134
+ if (!entry.validation) {
135
+ return true;
136
+ }
137
+ switch (entry.validation.kind) {
138
+ case "string":
139
+ case "nonempty":
140
+ return value.trim().length > 0 && (typeof entry.validation.minLength !== "number" || value.trim().length >= entry.validation.minLength);
141
+ case "boolean":
142
+ return /^(true|false|1|0)$/i.test(value);
143
+ case "number":
144
+ return Number.isFinite(Number(value));
145
+ case "url":
146
+ try {
147
+ new URL(value);
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ case "email":
153
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
154
+ case "enum":
155
+ return entry.validation.values.includes(value);
156
+ default:
157
+ return true;
158
+ }
159
+ }
49
160
  function isWizardRequiredMissing(page) {
50
- if (!page.required) {
161
+ if (page.requiredScopes.length === 0) {
51
162
  return false;
52
163
  }
53
- const resolvedValue = page.currentValue || page.suggestedValue || page.finalValue || page.entry.effectiveValue || "";
54
- return !hasUsableValue(resolvedValue);
164
+ return !isConfigValueValid(page.entry, page.finalValue);
55
165
  }
56
166
  function startupPriority(page) {
57
167
  if (page.required) {
@@ -68,6 +178,21 @@ function formatDisplayValue(page, value, emptyLabel) {
68
178
  }
69
179
  return page.entry.sensitivity === "secret" ? maskValue(value) : value;
70
180
  }
181
+ function filterCliConfigPages(pages, query) {
182
+ const normalizedQuery = query.trim().toLowerCase();
183
+ if (!normalizedQuery) {
184
+ return pages;
185
+ }
186
+ return pages.filter(
187
+ (page) => [
188
+ page.entry.id,
189
+ page.entry.label,
190
+ page.entry.group,
191
+ page.entry.cluster,
192
+ page.scope
193
+ ].some((field) => field.toLowerCase().includes(normalizedQuery))
194
+ );
195
+ }
71
196
  function tabRects(prefix, items, selectedIndex, y, startX) {
72
197
  let x = startX + prefix.length;
73
198
  return items.map((item, index) => {
@@ -90,7 +215,7 @@ function buttonRects(labels, y, startX) {
90
215
  });
91
216
  }
92
217
  function computeConfigViewportLayout(rows, columns) {
93
- const layout = computeViewportLayout(rows, columns, { topBarHeight: 2, footerHeight: 2 });
218
+ const layout = computeViewportLayout(rows, columns, { topBarHeight: 3, footerHeight: 2 });
94
219
  const sidebarWidth = Math.max(22, Math.min(34, Math.floor(layout.columns * 0.28)));
95
220
  const contentWidth = Math.max(34, layout.columns - sidebarWidth - 1);
96
221
  const actionRowHeight = 1;
@@ -107,7 +232,7 @@ function computeConfigViewportLayout(rows, columns) {
107
232
  };
108
233
  }
109
234
  function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode = "startup") {
110
- const selectedScopes = selectedFilter === "all" ? context.scopes : context.scopes.filter((scope) => scope === selectedFilter);
235
+ const selectedScopes = viewMode === "startup" ? context.scopes : context.scopes.filter((scope) => scope === selectedFilter);
111
236
  const sharedEntries = /* @__PURE__ */ new Set();
112
237
  const pages = [];
113
238
  for (const scope of selectedScopes) {
@@ -119,17 +244,30 @@ function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode =
119
244
  const relevantScopes = selectedScopes.filter((candidateScope) => context.entriesByScope[candidateScope].some((candidate) => candidate.id === entry.id));
120
245
  const key2 = `shared:${entry.id}`;
121
246
  sharedEntries.add(entry.id);
122
- const currentValue = resolveFirstNonEmptyValue(relevantScopes, context.entriesByScope, entry.id, "currentValue");
123
- const suggestedValue = resolveFirstNonEmptyValue(relevantScopes, context.entriesByScope, entry.id, "suggestedValue");
247
+ const requiredScopes = relevantScopes.filter((candidateScope) => context.entriesByScope[candidateScope].some((candidate) => candidate.id === entry.id && candidate.required));
248
+ const currentValue = resolveSharedEntryValue(relevantScopes, requiredScopes, context.entriesByScope, entry.id, "currentValue");
249
+ const suggestedValue = resolveSharedEntryValue(relevantScopes, requiredScopes, context.entriesByScope, entry.id, "suggestedValue", {
250
+ fallbackToRelevant: requiredScopes.length === 0
251
+ });
252
+ const effectiveValue = resolveSharedEntryValue(relevantScopes, requiredScopes, context.entriesByScope, entry.id, "effectiveValue", {
253
+ fallbackToRelevant: requiredScopes.length === 0
254
+ });
124
255
  const candidatePage2 = {
256
+ kind: "entry",
125
257
  key: key2,
126
258
  entry,
127
259
  scope,
128
260
  scopes: relevantScopes,
129
- required: relevantScopes.some((candidateScope) => context.entriesByScope[candidateScope].some((candidate) => candidate.id === entry.id && candidate.required)),
261
+ requiredScopes,
262
+ required: requiredScopes.length > 0,
130
263
  currentValue,
131
264
  suggestedValue,
132
- finalValue: key2 in overrides ? overrides[key2] : currentValue || suggestedValue || entry.effectiveValue || ""
265
+ finalValue: resolveEntryPageFinalValue(key2, {
266
+ ...entry,
267
+ currentValue,
268
+ suggestedValue,
269
+ effectiveValue
270
+ }, overrides)
133
271
  };
134
272
  pages.push({
135
273
  ...candidatePage2,
@@ -139,14 +277,16 @@ function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode =
139
277
  }
140
278
  const key = `${scope}:${entry.id}`;
141
279
  const candidatePage = {
280
+ kind: "entry",
142
281
  key,
143
282
  entry,
144
283
  scope,
145
284
  scopes: [scope],
285
+ requiredScopes: entry.required ? [scope] : [],
146
286
  required: entry.required,
147
287
  currentValue: entry.currentValue,
148
288
  suggestedValue: entry.suggestedValue,
149
- finalValue: key in overrides ? overrides[key] : entry.currentValue || entry.suggestedValue || entry.effectiveValue || ""
289
+ finalValue: resolveEntryPageFinalValue(key, entry, overrides)
150
290
  };
151
291
  pages.push({
152
292
  ...candidatePage,
@@ -158,18 +298,25 @@ function buildCliConfigPages(context, selectedFilter, overrides = {}, viewMode =
158
298
  if (startupPriority(left) !== startupPriority(right)) {
159
299
  return startupPriority(left) - startupPriority(right);
160
300
  }
161
- if (left.entry.storage !== right.entry.storage) {
162
- return left.entry.storage === "shared" ? -1 : 1;
301
+ if (left.entry.startupProfile !== right.entry.startupProfile) {
302
+ const order = { core: 0, optional: 1, advanced: 2 };
303
+ return order[left.entry.startupProfile] - order[right.entry.startupProfile];
163
304
  }
164
- if (left.entry.purposes.length !== right.entry.purposes.length) {
165
- return right.entry.purposes.length - left.entry.purposes.length;
305
+ if (providerWorkflowRank(left.entry) !== providerWorkflowRank(right.entry)) {
306
+ return providerWorkflowRank(left.entry) - providerWorkflowRank(right.entry);
307
+ }
308
+ if (normalizedClusterKey(left.entry) !== normalizedClusterKey(right.entry)) {
309
+ return normalizedClusterKey(left.entry).localeCompare(normalizedClusterKey(right.entry));
166
310
  }
167
- if (left.entry.group !== right.entry.group) {
168
- return left.entry.group.localeCompare(right.entry.group);
311
+ if (left.entry.storage !== right.entry.storage) {
312
+ return left.entry.storage === "shared" ? -1 : 1;
169
313
  }
170
314
  if (left.scope !== right.scope) {
171
315
  return scopeOrder(left.scope) - scopeOrder(right.scope);
172
316
  }
317
+ if (left.entry.purposes.length !== right.entry.purposes.length) {
318
+ return right.entry.purposes.length - left.entry.purposes.length;
319
+ }
173
320
  return left.entry.label.localeCompare(right.entry.label);
174
321
  });
175
322
  return viewMode === "startup" ? orderedPages.filter((page) => page.wizardRequiredMissing) : orderedPages;
@@ -179,17 +326,17 @@ function buildStartupDetailLines(step, draftValue) {
179
326
  return ["No startup configuration is required for the selected environment set."];
180
327
  }
181
328
  return [
182
- `Step ${step.index + 1} of ${step.total}`,
183
- step.entry.label,
184
- step.entry.id,
185
- `${step.required ? "Required" : "Optional"} ${step.entry.storage === "shared" ? "shared" : "environment-specific"} value for ${step.scopes.join(", ")}`,
186
- "",
187
- step.entry.description || "Treeseed needs this value to complete setup.",
329
+ `${step.entry.label} (${step.entry.id})`,
330
+ `Applies to: ${step.scopes.join(", ")}`,
331
+ `Required in: ${step.requiredScopes.join(", ")}`,
332
+ `Storage: ${step.entry.storage}`,
188
333
  "",
189
- `How to get it: ${step.entry.howToGet || "Use the suggested/default value if it matches your setup."}`,
190
334
  `Current value: ${formatDisplayValue(step, step.currentValue, "(unset)")}`,
191
335
  `Suggested value: ${formatDisplayValue(step, step.suggestedValue, "(none)")}`,
192
- `Pending value: ${formatDisplayValue(step, draftValue, "(unset)")}`
336
+ `Pending value: ${formatDisplayValue(step, draftValue, "(unset)")}`,
337
+ "",
338
+ step.entry.description || "Treeseed needs this value to complete setup.",
339
+ `How to get it: ${step.entry.howToGet || "Use the suggested/default value if it matches your setup."}`
193
340
  ];
194
341
  }
195
342
  function buildFullDetailLines(page, draftValue) {
@@ -197,23 +344,17 @@ function buildFullDetailLines(page, draftValue) {
197
344
  return ["No configuration entries match the selected environment filter."];
198
345
  }
199
346
  return [
200
- page.entry.label,
201
- page.entry.id,
347
+ `${page.entry.label} (${page.entry.id})`,
202
348
  `Scope: ${page.scopes.join(", ")}`,
203
349
  `Storage: ${page.entry.storage} | ${page.required ? "required" : "optional"}`,
204
350
  `Group: ${page.entry.group}`,
205
- `Used for: ${page.entry.purposes.join(", ") || "(none)"}`,
206
- `Targets: ${page.entry.targets.join(", ") || "(none)"}`,
207
- "",
208
- "About",
209
- page.entry.description || "(no description)",
210
- "",
211
- "How to get it",
212
- page.entry.howToGet || "(no extra setup guidance)",
213
351
  "",
214
352
  `Current: ${formatDisplayValue(page, page.currentValue, "(unset)")}`,
215
353
  `Suggested: ${formatDisplayValue(page, page.suggestedValue, "(none)")}`,
216
- `Pending: ${formatDisplayValue(page, draftValue, "(unset)")}`
354
+ `Pending: ${formatDisplayValue(page, draftValue, "(unset)")}`,
355
+ "",
356
+ page.entry.description || "(no description)",
357
+ `Get it: ${page.entry.howToGet || "(no extra setup guidance)"}`
217
358
  ];
218
359
  }
219
360
  function detailViewportLines(lines, width, height, offset) {
@@ -233,6 +374,12 @@ function nextDraftValue(page, drafts) {
233
374
  }
234
375
  return page.key in drafts ? drafts[page.key] : page.finalValue;
235
376
  }
377
+ function resolveEntryPageFinalValue(pageKey, entry, overrides) {
378
+ if (pageKey in overrides) {
379
+ return overrides[pageKey];
380
+ }
381
+ return entry.effectiveValue || entry.suggestedValue || entry.currentValue || "";
382
+ }
236
383
  function insertAt(value, insert, cursor) {
237
384
  return `${value.slice(0, cursor)}${insert}${value.slice(cursor)}`;
238
385
  }
@@ -255,10 +402,47 @@ function deleteForward(value, cursor) {
255
402
  };
256
403
  }
257
404
  function cycleFocus(current, viewMode) {
258
- const areas = viewMode === "startup" ? ["environment", "mode", "content", "actions"] : ["environment", "mode", "sidebar", "content", "actions"];
405
+ const areas = viewMode === "startup" ? ["content", "actions"] : ["environment", "filter", "sidebar", "content", "actions"];
259
406
  const index = areas.indexOf(current);
260
407
  return areas[(index + 1) % areas.length] ?? "content";
261
408
  }
409
+ function normalizeConfigInputChunk(input) {
410
+ if (!input) {
411
+ return "";
412
+ }
413
+ return input.replace(/\u001b\[200~/gu, "").replace(/\u001b\[201~/gu, "").replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "").replace(/\n+$/gu, "");
414
+ }
415
+ function applyConfigInputInsertion(state, input) {
416
+ const normalized = normalizeConfigInputChunk(input);
417
+ if (!normalized) {
418
+ return state;
419
+ }
420
+ return {
421
+ value: insertAt(state.value, normalized, state.cursor),
422
+ cursor: state.cursor + normalized.length
423
+ };
424
+ }
425
+ function runClipboardCommand(command, args) {
426
+ const result = spawnSync(command, args, {
427
+ stdio: "pipe",
428
+ encoding: "utf8",
429
+ timeout: 1500
430
+ });
431
+ if (result.status !== 0) {
432
+ return null;
433
+ }
434
+ const text = String(result.stdout ?? "").replace(/\r\n/gu, "\n");
435
+ return text.length > 0 ? text : null;
436
+ }
437
+ function readLinuxClipboardText() {
438
+ if (process.platform !== "linux") {
439
+ return null;
440
+ }
441
+ return runClipboardCommand("wl-paste", ["--no-newline"]) ?? runClipboardCommand("xclip", ["-selection", "clipboard", "-o"]) ?? runClipboardCommand("xsel", ["--clipboard", "--output"]);
442
+ }
443
+ function isCtrlVPaste(input, key) {
444
+ return key.ctrl && input === "v" || input === "";
445
+ }
262
446
  async function runCliConfigEditor(context, options = {}) {
263
447
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
264
448
  return null;
@@ -275,6 +459,8 @@ async function runCliConfigEditor(context, options = {}) {
275
459
  resolveSession(result);
276
460
  };
277
461
  function App() {
462
+ const sidebarFilterHeight = 4;
463
+ const [currentContext, setCurrentContext] = React.useState(context);
278
464
  const [filterIndex, setFilterIndex] = React.useState(0);
279
465
  const [viewMode, setViewMode] = React.useState(options.initialViewMode ?? "startup");
280
466
  const [pageIndex, setPageIndex] = React.useState(0);
@@ -283,32 +469,59 @@ async function runCliConfigEditor(context, options = {}) {
283
469
  const [drafts, setDrafts] = React.useState({});
284
470
  const [cursorPositions, setCursorPositions] = React.useState({});
285
471
  const [overrides, setOverrides] = React.useState({});
286
- const [focusArea, setFocusArea] = React.useState(options.initialViewMode === "full" ? "sidebar" : "content");
472
+ const [filterQuery, setFilterQuery] = React.useState("");
473
+ const [filterCursor, setFilterCursor] = React.useState(0);
474
+ const [focusArea, setFocusArea] = React.useState(options.initialViewMode === "full" ? "filter" : "content");
287
475
  const [actionIndex, setActionIndex] = React.useState(0);
288
- const [statusMessage, setStatusMessage] = React.useState("Startup wizard ready. Click or Tab through controls, then update values one step at a time.");
476
+ const [saving, setSaving] = React.useState(false);
477
+ const [statusMessage, setStatusMessage] = React.useState(
478
+ options.initialStatusMessage ?? (options.initialViewMode === "full" ? "Full editor ready. Filter the variable list or edit the selected value." : "Startup wizard ready. Update each required value in order.")
479
+ );
289
480
  const { exit } = useApp();
290
481
  const windowSize = useWindowSize();
291
482
  const layout = computeConfigViewportLayout(windowSize?.rows ?? 24, windowSize?.columns ?? 100);
292
- const selectedFilter = CONFIG_FILTERS[filterIndex] ?? "all";
293
- const pages = buildCliConfigPages(context, selectedFilter, overrides, viewMode);
483
+ const selectedFilter = FULL_CONFIG_FILTERS[filterIndex] ?? "local";
484
+ const allPages = buildCliConfigPages(currentContext, selectedFilter, overrides, viewMode);
485
+ const pages = viewMode === "full" ? filterCliConfigPages(allPages, filterQuery) : allPages;
294
486
  const safePageIndex = pages.length === 0 ? 0 : Math.min(pageIndex, pages.length - 1);
295
487
  const selectedPage = pages[safePageIndex] ?? null;
296
488
  const draftValue = nextDraftValue(selectedPage, drafts);
297
489
  const cursorPosition = selectedPage ? Math.max(0, Math.min(cursorPositions[selectedPage.key] ?? draftValue.length, draftValue.length)) : 0;
490
+ const focusAreaRef = React.useRef(focusArea);
491
+ const viewModeRef = React.useRef(viewMode);
492
+ const selectedPageRef = React.useRef(selectedPage);
493
+ const draftValueRef = React.useRef(draftValue);
494
+ const cursorPositionRef = React.useRef(cursorPosition);
495
+ const filterQueryRef = React.useRef(filterQuery);
496
+ const filterCursorRef = React.useRef(filterCursor);
497
+ focusAreaRef.current = focusArea;
498
+ viewModeRef.current = viewMode;
499
+ selectedPageRef.current = selectedPage;
500
+ draftValueRef.current = draftValue;
501
+ cursorPositionRef.current = cursorPosition;
502
+ filterQueryRef.current = filterQuery;
503
+ filterCursorRef.current = filterCursor;
298
504
  const startupStep = selectedPage ? { ...selectedPage, index: safePageIndex, total: pages.length } : null;
299
505
  const detailSourceLines = viewMode === "startup" ? buildStartupDetailLines(startupStep, draftValue) : buildFullDetailLines(selectedPage, draftValue);
300
- const detailPanel = detailViewportLines(detailSourceLines, layout.contentWidth, layout.detailHeight, detailOffset);
301
- const authStatus = context.authStatusByScope.local ?? context.authStatusByScope[context.scopes[0]];
302
- const sidebarViewportSize = Math.max(1, layout.bodyHeight - 4);
506
+ const detailWidth = viewMode === "full" ? layout.contentWidth : layout.columns;
507
+ const detailPanel = detailViewportLines(detailSourceLines, detailWidth, layout.detailHeight, detailOffset);
508
+ const configReadiness = {
509
+ github: { configured: hasUsableValue(resolveCurrentConfigValue(currentContext, overrides, "GH_TOKEN", "local")) },
510
+ cloudflare: { configured: hasUsableValue(resolveCurrentConfigValue(currentContext, overrides, "CLOUDFLARE_API_TOKEN", "local")) },
511
+ railway: { configured: hasUsableValue(resolveCurrentConfigValue(currentContext, overrides, "RAILWAY_API_TOKEN", "local")) },
512
+ localDevelopment: currentContext.configReadinessByScope.local?.localDevelopment ?? { configured: true }
513
+ };
514
+ const sidebarHeight = Math.max(4, layout.bodyHeight - sidebarFilterHeight);
515
+ const sidebarViewportSize = Math.max(1, sidebarHeight - 4);
303
516
  const safeSidebarOffset = clampOffset(ensureVisible(safePageIndex, sidebarOffset, sidebarViewportSize), pages.length, sidebarViewportSize);
304
517
  const visibleSidebar = pages.slice(safeSidebarOffset, safeSidebarOffset + sidebarViewportSize);
305
- const startupActions = selectedPage ? ["Back", ...hasUsableValue(selectedPage?.suggestedValue ?? "") ? ["Use Suggested + Next"] : [], "Update + Next", "Full Editor"] : ["Open Full Editor", "Save and Continue"];
306
- const fullActions = ["Update + Next", "Clear", "Startup Wizard", "Finish"];
518
+ const startupActions = selectedPage ? ["Back", ...hasUsableValue(selectedPage?.suggestedValue ?? "") ? ["Use Suggested + Next"] : [], "Update + Next"] : [];
519
+ const fullActions = ["Save Value", "Clear", "Finish"];
307
520
  const actions = viewMode === "startup" ? startupActions : fullActions;
308
- const envRects = tabRects("Env ", CONFIG_FILTERS, filterIndex, 2, 1);
309
- const modeRects = tabRects("View ", CONFIG_VIEW_MODES, CONFIG_VIEW_MODES.indexOf(viewMode), 2, Math.floor(layout.columns / 2));
521
+ const envRects = viewMode === "full" ? tabRects("Env ", FULL_CONFIG_FILTERS, filterIndex, 2, 1) : [];
310
522
  const clickRegions = [];
311
523
  const sidebarRect = { x: 0, y: layout.topBarHeight, width: layout.sidebarWidth, height: layout.bodyHeight };
524
+ const filterRect = { x: 0, y: layout.topBarHeight, width: layout.sidebarWidth, height: sidebarFilterHeight };
312
525
  const detailRect = {
313
526
  x: viewMode === "full" ? layout.sidebarWidth + 1 : 0,
314
527
  y: layout.topBarHeight,
@@ -332,32 +545,120 @@ async function runCliConfigEditor(context, options = {}) {
332
545
  }, [detailPanel.offset, detailOffset]);
333
546
  React.useEffect(() => {
334
547
  setActionIndex(0);
335
- }, [viewMode]);
548
+ }, [viewMode, selectedPage?.key]);
336
549
  React.useEffect(() => {
337
550
  setDetailOffset(0);
338
551
  }, [selectedPage?.key]);
552
+ React.useEffect(() => {
553
+ if (viewMode === "startup" && selectedPage) {
554
+ setFocusArea("content");
555
+ }
556
+ }, [selectedPage?.key, viewMode, selectedPage]);
557
+ React.useEffect(() => {
558
+ setPageIndex(0);
559
+ setSidebarOffset(0);
560
+ }, [filterQuery, selectedFilter]);
339
561
  React.useEffect(() => {
340
562
  if (selectedPage && !(selectedPage.key in cursorPositions)) {
341
563
  setCursorPositions((current) => ({ ...current, [selectedPage.key]: draftValue.length }));
342
564
  }
343
565
  }, [cursorPositions, draftValue.length, selectedPage]);
344
566
  React.useEffect(() => {
345
- setFocusArea(viewMode === "full" ? "sidebar" : "content");
567
+ setFocusArea(viewMode === "full" ? "filter" : "content");
346
568
  }, [viewMode]);
347
- const saveCurrentDraft = React.useCallback((advance) => {
348
- if (!selectedPage) {
569
+ React.useEffect(() => {
570
+ if (viewMode === "startup" && pages.length === 0) {
571
+ setViewMode("full");
572
+ setFocusArea("filter");
573
+ setPageIndex(0);
574
+ setFilterIndex(0);
575
+ setFilterQuery("");
576
+ setFilterCursor(0);
577
+ setStatusMessage("Startup configuration is complete. Switched to the full editor.");
578
+ }
579
+ }, [pages.length, viewMode]);
580
+ const finishWithOverrides = React.useCallback((nextOverrides) => {
581
+ finish({
582
+ overrides: nextOverrides,
583
+ viewMode
584
+ });
585
+ }, [viewMode]);
586
+ const advanceStartupFlow = React.useCallback((nextOverrides, currentPageKey) => {
587
+ const nextPages = buildCliConfigPages(currentContext, selectedFilter, nextOverrides, "startup");
588
+ const currentIndex = nextPages.findIndex((page) => page.key === currentPageKey);
589
+ const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
590
+ if (nextIndex >= nextPages.length) {
591
+ setOverrides(nextOverrides);
592
+ setViewMode("full");
593
+ setFocusArea("filter");
594
+ setPageIndex(0);
595
+ setFilterIndex(0);
596
+ setFilterQuery("");
597
+ setFilterCursor(0);
598
+ setDetailOffset(0);
599
+ setStatusMessage("Startup configuration is complete. Switched to the full editor.");
600
+ return;
601
+ }
602
+ setFocusArea("content");
603
+ setPageIndex(nextIndex);
604
+ setDetailOffset(0);
605
+ }, [currentContext, selectedFilter]);
606
+ const commitCurrentDraft = React.useCallback(async (value) => {
607
+ if (!selectedPage || !options.onCommit) {
608
+ return true;
609
+ }
610
+ setSaving(true);
611
+ setStatusMessage(`Saving ${selectedPage.entry.id}...`);
612
+ try {
613
+ const refreshedContext = await options.onCommit({
614
+ scope: selectedPage.scope,
615
+ entryId: selectedPage.entry.id,
616
+ value
617
+ });
618
+ setCurrentContext(refreshedContext);
619
+ setDrafts((current) => ({ ...current, [selectedPage.key]: value }));
620
+ setCursorPositions((current) => ({ ...current, [selectedPage.key]: value.length }));
621
+ setOverrides((current) => {
622
+ const next = { ...current };
623
+ delete next[selectedPage.key];
624
+ return next;
625
+ });
626
+ return true;
627
+ } catch (error) {
628
+ setStatusMessage(error instanceof Error ? error.message : `Unable to save ${selectedPage.entry.id}.`);
629
+ return false;
630
+ } finally {
631
+ setSaving(false);
632
+ }
633
+ }, [options, selectedPage]);
634
+ const saveCurrentDraft = React.useCallback(async (advance) => {
635
+ if (!selectedPage || saving) {
349
636
  return;
350
637
  }
351
638
  const value = nextDraftValue(selectedPage, drafts);
352
- setOverrides((current) => ({ ...current, [selectedPage.key]: value }));
639
+ const committed = await commitCurrentDraft(value);
640
+ if (!committed) {
641
+ return;
642
+ }
643
+ const nextOverrides = { ...overrides, [selectedPage.key]: value };
644
+ setOverrides(nextOverrides);
645
+ setFocusArea("content");
353
646
  setStatusMessage(`Updated ${selectedPage.entry.id}.`);
354
647
  if (advance) {
648
+ if (viewMode === "startup") {
649
+ advanceStartupFlow(nextOverrides, selectedPage.key);
650
+ return;
651
+ }
355
652
  setPageIndex((current) => Math.min(Math.max(0, pages.length - 1), current + 1));
356
653
  setDetailOffset(0);
357
654
  }
358
- }, [drafts, pages.length, selectedPage]);
359
- const activateAction = React.useCallback((label) => {
655
+ }, [advanceStartupFlow, commitCurrentDraft, drafts, overrides, pages.length, saving, selectedPage, viewMode]);
656
+ const activateAction = React.useCallback(async (label) => {
657
+ if (saving) {
658
+ return;
659
+ }
360
660
  if (label === "Back") {
661
+ setFocusArea("content");
361
662
  setPageIndex((current) => Math.max(0, current - 1));
362
663
  setDetailOffset(0);
363
664
  return;
@@ -366,14 +667,28 @@ async function runCliConfigEditor(context, options = {}) {
366
667
  const suggested = selectedPage.suggestedValue || selectedPage.finalValue;
367
668
  setDrafts((current) => ({ ...current, [selectedPage.key]: suggested }));
368
669
  setCursorPositions((current) => ({ ...current, [selectedPage.key]: suggested.length }));
369
- setOverrides((current) => ({ ...current, [selectedPage.key]: suggested }));
370
- setPageIndex((current) => Math.min(Math.max(0, pages.length - 1), current + 1));
670
+ const committed = await commitCurrentDraft(suggested);
671
+ if (!committed) {
672
+ return;
673
+ }
674
+ const nextOverrides = { ...overrides, [selectedPage.key]: suggested };
675
+ setOverrides(nextOverrides);
676
+ setFocusArea("content");
371
677
  setStatusMessage(`Accepted suggested value for ${selectedPage.entry.id}.`);
678
+ if (viewMode === "startup") {
679
+ advanceStartupFlow(nextOverrides, selectedPage.key);
680
+ return;
681
+ }
682
+ setPageIndex((current) => Math.min(Math.max(0, pages.length - 1), current + 1));
372
683
  setDetailOffset(0);
373
684
  return;
374
685
  }
375
686
  if (label === "Update + Next") {
376
- saveCurrentDraft(true);
687
+ void saveCurrentDraft(true);
688
+ return;
689
+ }
690
+ if (label === "Save Value") {
691
+ void saveCurrentDraft(false);
377
692
  return;
378
693
  }
379
694
  if (label === "Clear" && selectedPage) {
@@ -383,35 +698,19 @@ async function runCliConfigEditor(context, options = {}) {
383
698
  setStatusMessage(`Cleared ${selectedPage.entry.id}.`);
384
699
  return;
385
700
  }
386
- if (label === "Full Editor") {
387
- setViewMode("full");
388
- setFocusArea("sidebar");
389
- setStatusMessage("Switched to the advanced editor.");
390
- return;
701
+ if (label === "Finish") {
702
+ finishWithOverrides(overrides);
391
703
  }
392
- if (label === "Startup Wizard") {
393
- setViewMode("startup");
394
- setFocusArea("content");
395
- setStatusMessage("Switched back to the startup wizard.");
396
- return;
397
- }
398
- if (label === "Open Full Editor") {
399
- setViewMode("full");
400
- setFocusArea("sidebar");
401
- setStatusMessage("Opened the advanced editor.");
402
- return;
403
- }
404
- if (label === "Save and Continue" || label === "Finish") {
405
- finish({
406
- overrides,
407
- viewMode
408
- });
409
- }
410
- }, [overrides, pages.length, saveCurrentDraft, selectedPage, viewMode]);
704
+ }, [advanceStartupFlow, commitCurrentDraft, finishWithOverrides, overrides, saveCurrentDraft, saving, selectedPage, viewMode]);
411
705
  const scrollRegions = [
412
706
  ...viewMode === "full" ? [{
413
707
  id: "config-sidebar",
414
- rect: sidebarRect,
708
+ rect: {
709
+ x: sidebarRect.x,
710
+ y: sidebarRect.y + sidebarFilterHeight,
711
+ width: sidebarRect.width,
712
+ height: sidebarHeight
713
+ },
415
714
  state: {
416
715
  offset: safeSidebarOffset,
417
716
  viewportSize: sidebarViewportSize,
@@ -445,7 +744,7 @@ async function runCliConfigEditor(context, options = {}) {
445
744
  return;
446
745
  }
447
746
  findClickableRegion(clickRegions, event.x, event.y)?.onClick();
448
- });
747
+ }, { enabled: options.mouseEnabled === true });
449
748
  useInput((input, key) => {
450
749
  if (key.ctrl && input === "c") {
451
750
  exit();
@@ -456,36 +755,74 @@ async function runCliConfigEditor(context, options = {}) {
456
755
  setFocusArea((current) => cycleFocus(current, viewMode));
457
756
  return;
458
757
  }
459
- if (input === "t") {
460
- setViewMode((current) => current === "startup" ? "full" : "startup");
461
- return;
462
- }
463
- if (input === "s") {
758
+ if (input === "s" && focusArea !== "content" && focusArea !== "filter") {
464
759
  if (viewMode === "startup" && selectedPage) {
465
760
  setStatusMessage("Complete each required startup step before saving. Use Update + Next to continue.");
466
761
  return;
467
762
  }
468
- finish({
469
- overrides,
470
- viewMode
471
- });
763
+ finishWithOverrides(overrides);
472
764
  return;
473
765
  }
474
766
  if (focusArea === "environment") {
767
+ if (viewMode === "startup") {
768
+ return;
769
+ }
475
770
  if (key.leftArrow) {
476
771
  setFilterIndex((current) => Math.max(0, current - 1));
477
772
  setPageIndex(0);
478
773
  return;
479
774
  }
480
775
  if (key.rightArrow) {
481
- setFilterIndex((current) => Math.min(CONFIG_FILTERS.length - 1, current + 1));
776
+ setFilterIndex((current) => Math.min(FULL_CONFIG_FILTERS.length - 1, current + 1));
482
777
  setPageIndex(0);
483
778
  return;
484
779
  }
485
780
  }
486
- if (focusArea === "mode") {
487
- if (key.leftArrow || key.rightArrow || key.return) {
488
- setViewMode((current) => current === "startup" ? "full" : "startup");
781
+ if (focusArea === "filter" && viewMode === "full") {
782
+ if (isCtrlVPaste(input, key)) {
783
+ const clipboardText = readLinuxClipboardText();
784
+ if (!clipboardText) {
785
+ setStatusMessage("Ctrl+V clipboard paste is unavailable. Use right-click/menu paste or install wl-paste, xclip, or xsel.");
786
+ return;
787
+ }
788
+ const next = applyConfigInputInsertion({ value: filterQuery, cursor: filterCursor }, clipboardText);
789
+ setFilterQuery(next.value);
790
+ setFilterCursor(next.cursor);
791
+ setStatusMessage("Filtered the variable list from the clipboard.");
792
+ return;
793
+ }
794
+ if (key.home) {
795
+ setFilterCursor(0);
796
+ return;
797
+ }
798
+ if (key.end) {
799
+ setFilterCursor(filterQuery.length);
800
+ return;
801
+ }
802
+ if (key.leftArrow) {
803
+ setFilterCursor((current) => Math.max(0, current - 1));
804
+ return;
805
+ }
806
+ if (key.rightArrow) {
807
+ setFilterCursor((current) => Math.min(filterQuery.length, current + 1));
808
+ return;
809
+ }
810
+ if (key.backspace) {
811
+ const next = deleteBackward(filterQuery, filterCursor);
812
+ setFilterQuery(next.value);
813
+ setFilterCursor(next.cursor);
814
+ return;
815
+ }
816
+ if (key.delete) {
817
+ const next = deleteForward(filterQuery, filterCursor);
818
+ setFilterQuery(next.value);
819
+ setFilterCursor(next.cursor);
820
+ return;
821
+ }
822
+ if (!key.ctrl && !key.meta && input && !key.return && !key.upArrow && !key.downArrow && !key.pageDown && !key.pageUp) {
823
+ const next = applyConfigInputInsertion({ value: filterQuery, cursor: filterCursor }, input);
824
+ setFilterQuery(next.value);
825
+ setFilterCursor(next.cursor);
489
826
  return;
490
827
  }
491
828
  }
@@ -508,6 +845,26 @@ async function runCliConfigEditor(context, options = {}) {
508
845
  }
509
846
  }
510
847
  if (focusArea === "content") {
848
+ if (selectedPage && isCtrlVPaste(input, key)) {
849
+ const clipboardText = readLinuxClipboardText();
850
+ if (!clipboardText) {
851
+ setStatusMessage("Ctrl+V clipboard paste is unavailable. Use right-click/menu paste or install wl-paste, xclip, or xsel.");
852
+ return;
853
+ }
854
+ const next = applyConfigInputInsertion({ value: draftValue, cursor: cursorPosition }, clipboardText);
855
+ setDrafts((current) => ({ ...current, [selectedPage.key]: next.value }));
856
+ setCursorPositions((current) => ({ ...current, [selectedPage.key]: next.cursor }));
857
+ setStatusMessage(`Pasted text into ${selectedPage.entry.id}.`);
858
+ return;
859
+ }
860
+ if (selectedPage && key.home) {
861
+ setCursorPositions((current) => ({ ...current, [selectedPage.key]: 0 }));
862
+ return;
863
+ }
864
+ if (selectedPage && key.end) {
865
+ setCursorPositions((current) => ({ ...current, [selectedPage.key]: draftValue.length }));
866
+ return;
867
+ }
511
868
  if (selectedPage && key.leftArrow) {
512
869
  setCursorPositions((current) => ({ ...current, [selectedPage.key]: Math.max(0, cursorPosition - 1) }));
513
870
  return;
@@ -545,13 +902,13 @@ async function runCliConfigEditor(context, options = {}) {
545
902
  return;
546
903
  }
547
904
  if (selectedPage && key.return && viewMode === "startup") {
548
- saveCurrentDraft(true);
905
+ void saveCurrentDraft(true);
549
906
  return;
550
907
  }
551
908
  if (!key.ctrl && !key.meta && input && selectedPage && !key.upArrow && !key.downArrow && !key.pageDown && !key.pageUp) {
552
- const nextValue = insertAt(draftValue, input, cursorPosition);
553
- setDrafts((current) => ({ ...current, [selectedPage.key]: nextValue }));
554
- setCursorPositions((current) => ({ ...current, [selectedPage.key]: cursorPosition + input.length }));
909
+ const next = applyConfigInputInsertion({ value: draftValue, cursor: cursorPosition }, input);
910
+ setDrafts((current) => ({ ...current, [selectedPage.key]: next.value }));
911
+ setCursorPositions((current) => ({ ...current, [selectedPage.key]: next.cursor }));
555
912
  return;
556
913
  }
557
914
  }
@@ -565,30 +922,39 @@ async function runCliConfigEditor(context, options = {}) {
565
922
  return;
566
923
  }
567
924
  if (key.return) {
568
- activateAction(actions[actionIndex] ?? actions[0] ?? "Save and Continue");
925
+ void activateAction(actions[actionIndex] ?? actions[0] ?? "Finish");
569
926
  }
570
927
  }
571
928
  });
572
- for (const [index, item] of envRects.entries()) {
573
- clickRegions.push({
574
- id: `env-${item.item}`,
575
- rect: item.rect,
576
- onClick: () => {
577
- setFilterIndex(index);
578
- setPageIndex(0);
579
- setFocusArea("environment");
580
- }
581
- });
582
- }
583
- for (const [index, item] of modeRects.entries()) {
584
- clickRegions.push({
585
- id: `mode-${item.item}`,
586
- rect: item.rect,
587
- onClick: () => {
588
- setViewMode(CONFIG_VIEW_MODES[index] ?? "startup");
589
- setFocusArea("mode");
590
- }
591
- });
929
+ usePaste((text) => {
930
+ if (focusAreaRef.current === "filter" && viewModeRef.current === "full") {
931
+ const next2 = applyConfigInputInsertion({ value: filterQueryRef.current, cursor: filterCursorRef.current }, text);
932
+ setFilterQuery(next2.value);
933
+ setFilterCursor(next2.cursor);
934
+ setStatusMessage("Filtered the variable list.");
935
+ return;
936
+ }
937
+ const currentPage = selectedPageRef.current;
938
+ if (focusAreaRef.current !== "content" || !currentPage) {
939
+ return;
940
+ }
941
+ const next = applyConfigInputInsertion({ value: draftValueRef.current, cursor: cursorPositionRef.current }, text);
942
+ setDrafts((current) => ({ ...current, [currentPage.key]: next.value }));
943
+ setCursorPositions((current) => ({ ...current, [currentPage.key]: next.cursor }));
944
+ setStatusMessage(`Pasted text into ${currentPage.entry.id}.`);
945
+ }, { isActive: true });
946
+ if (viewMode === "full") {
947
+ for (const [index, item] of envRects.entries()) {
948
+ clickRegions.push({
949
+ id: `env-${item.item}`,
950
+ rect: item.rect,
951
+ onClick: () => {
952
+ setFilterIndex(index);
953
+ setPageIndex(0);
954
+ setFocusArea("environment");
955
+ }
956
+ });
957
+ }
592
958
  }
593
959
  clickRegions.push({
594
960
  id: "detail-focus",
@@ -598,6 +964,13 @@ async function runCliConfigEditor(context, options = {}) {
598
964
  }
599
965
  });
600
966
  if (viewMode === "full") {
967
+ clickRegions.push({
968
+ id: "filter-focus",
969
+ rect: filterRect,
970
+ onClick: () => {
971
+ setFocusArea("filter");
972
+ }
973
+ });
601
974
  for (let index = 0; index < visibleSidebar.length; index += 1) {
602
975
  const page = visibleSidebar[index];
603
976
  if (!page) {
@@ -605,7 +978,7 @@ async function runCliConfigEditor(context, options = {}) {
605
978
  }
606
979
  clickRegions.push({
607
980
  id: `sidebar-${page.key}`,
608
- rect: { x: 1, y: layout.topBarHeight + 2 + index, width: layout.sidebarWidth - 2, height: 1 },
981
+ rect: { x: 1, y: layout.topBarHeight + sidebarFilterHeight + 2 + index, width: layout.sidebarWidth - 2, height: 1 },
609
982
  onClick: () => {
610
983
  setPageIndex(safeSidebarOffset + index);
611
984
  setFocusArea("sidebar");
@@ -621,37 +994,30 @@ async function runCliConfigEditor(context, options = {}) {
621
994
  onClick: () => {
622
995
  setFocusArea("actions");
623
996
  setActionIndex(index);
624
- activateAction(item.label);
997
+ void activateAction(item.label);
625
998
  }
626
999
  });
627
1000
  }
1001
+ const titleLine = truncateLine(
1002
+ `Treeseed Config ${currentContext.project.name} (${currentContext.project.slug}) GH cfg:${configReadiness.github.configured ? "ok" : "miss"} CF cfg:${configReadiness.cloudflare.configured ? "ok" : "miss"} RW cfg:${configReadiness.railway.configured ? "ok" : "miss"}`,
1003
+ layout.columns
1004
+ );
1005
+ const statusTail = viewMode === "full" ? `Env ${selectedFilter}` : "";
1006
+ const toolsLine = truncateLine(
1007
+ `gh:${options.toolAvailability?.githubCli?.available ? "ok" : "miss"} wr:${options.toolAvailability?.wranglerCli?.available ? "ok" : "miss"} rw:${options.toolAvailability?.railwayCli?.available ? "ok" : "miss"} act:${options.toolAvailability?.ghActExtension?.available ? "ok" : "miss"} dk:${options.toolAvailability?.dockerDaemon?.available ? "ok" : "miss"} sec:${options.secretSession?.status?.unlocked ? "on" : "off"}${statusTail ? ` ${statusTail}` : ""}`,
1008
+ layout.columns
1009
+ );
628
1010
  const topBar = React.createElement(
629
1011
  React.Fragment,
630
1012
  null,
631
- React.createElement(Text, { color: "cyan", bold: true }, truncateLine(`Treeseed Config ${context.project.name} (${context.project.slug}) GH:${authStatus?.gh?.authenticated ? "ok" : "missing"} CF:${authStatus?.wrangler?.authenticated ? "ok" : "missing"} RW:${authStatus?.railway?.authenticated ? "ok" : "missing"}`, layout.columns)),
632
- React.createElement(
633
- Box,
634
- { width: layout.columns },
635
- React.createElement(TopTabs, {
636
- width: Math.floor(layout.columns / 2) - 1,
637
- title: "Env",
638
- items: CONFIG_FILTERS.map((filter) => ({ id: filter, label: filter })),
639
- activeId: selectedFilter,
640
- focused: focusArea === "environment"
641
- }),
642
- React.createElement(TopTabs, {
643
- width: Math.ceil(layout.columns / 2),
644
- title: "View",
645
- items: CONFIG_VIEW_MODES.map((mode) => ({ id: mode, label: mode === "startup" ? "wizard" : "full" })),
646
- activeId: viewMode,
647
- focused: focusArea === "mode"
648
- })
649
- )
1013
+ React.createElement(Text, { color: "cyan", bold: true }, titleLine),
1014
+ React.createElement(Text, { color: "gray" }, toolsLine),
1015
+ viewMode === "full" ? React.createElement(Text, { color: focusArea === "environment" ? "cyan" : "gray" }, truncateLine(`Env ${FULL_CONFIG_FILTERS.map((filter) => filter === selectedFilter ? `[${filter}]` : filter).join(" ")}`, layout.columns)) : React.createElement(Text, { color: "gray" }, truncateLine(`Wizard mode across ${currentContext.scopes.join(", ")}.`, layout.columns))
650
1016
  );
651
1017
  const footer = React.createElement(StatusBar, {
652
1018
  width: layout.columns,
653
1019
  accent: focusArea === "content",
654
- primary: viewMode === "full" ? "Tab cycles controls. Type in the input when focused. Sidebar arrows or wheel change variables. Detail PgUp/PgDn or wheel scroll help text." : "Wizard input is live when focused. Type to edit, Left/Right move the cursor, Enter updates and advances, PgUp/PgDn or wheel scroll help text.",
1020
+ primary: viewMode === "full" ? `Tab cycles env, filter, list, editor, and actions. Type in Filter to narrow variables. Sidebar arrows${options.mouseEnabled === true ? " or wheel" : ""} change selection.` : `Type or paste to edit. Left/Right move the cursor, Home/End jump, Enter updates and advances.`,
655
1021
  secondary: statusMessage
656
1022
  });
657
1023
  if (viewMode === "startup") {
@@ -661,7 +1027,7 @@ async function runCliConfigEditor(context, options = {}) {
661
1027
  React.createElement(ScrollPanel, {
662
1028
  width: layout.columns,
663
1029
  height: layout.detailHeight,
664
- title: startupStep ? `Startup Wizard ${startupStep.index + 1}/${startupStep.total}` : "Startup Wizard",
1030
+ title: startupStep ? `Required Setup ${Math.max(0, startupStep.total - startupStep.index - 1)} left` : "Required Setup",
665
1031
  lines: detailPanel.lines,
666
1032
  focused: focusArea === "content",
667
1033
  tone: "accent",
@@ -674,12 +1040,13 @@ async function runCliConfigEditor(context, options = {}) {
674
1040
  React.createElement(TextInputField, {
675
1041
  width: layout.columns,
676
1042
  height: layout.inputHeight,
677
- label: selectedPage.entry.sensitivity === "secret" ? "New value" : "New value",
1043
+ label: "Value",
678
1044
  focused: focusArea === "content",
679
1045
  value: draftValue,
680
1046
  cursorPosition,
681
1047
  secret: selectedPage.entry.sensitivity === "secret",
682
- placeholder: selectedPage.suggestedValue || "(empty)"
1048
+ placeholder: "",
1049
+ helperText: ""
683
1050
  }),
684
1051
  React.createElement(
685
1052
  Box,
@@ -697,31 +1064,45 @@ async function runCliConfigEditor(context, options = {}) {
697
1064
  ) : React.createElement(EmptyState, {
698
1065
  width: layout.columns,
699
1066
  height: layout.bodyHeight,
700
- title: "Startup Complete",
701
- message: "No startup configuration items remain for the selected environment. You can finish now or switch to the full editor for advanced settings."
1067
+ title: "Required Setup Complete",
1068
+ message: "The required setup flow is complete."
702
1069
  });
703
1070
  return React.createElement(AppFrame, { layout, topBar, body: body2, footer });
704
1071
  }
705
1072
  const body = React.createElement(
706
1073
  Box,
707
1074
  { width: layout.columns, height: layout.bodyHeight, overflow: "hidden" },
708
- React.createElement(SidebarList, {
709
- width: layout.sidebarWidth,
710
- height: layout.bodyHeight,
711
- title: "Variables",
712
- focused: focusArea === "sidebar",
713
- scrollState: {
714
- offset: safeSidebarOffset,
715
- viewportSize: sidebarViewportSize,
716
- totalSize: pages.length
717
- },
718
- items: visibleSidebar.map((page, index) => ({
719
- id: page.key,
720
- label: page.entry.id,
721
- active: safeSidebarOffset + index === safePageIndex,
722
- tone: page.required ? "required" : "normal"
723
- }))
724
- }),
1075
+ React.createElement(
1076
+ Box,
1077
+ { flexDirection: "column", width: layout.sidebarWidth, height: layout.bodyHeight, overflow: "hidden" },
1078
+ React.createElement(TextInputField, {
1079
+ width: layout.sidebarWidth,
1080
+ height: sidebarFilterHeight,
1081
+ label: "Filter",
1082
+ focused: focusArea === "filter",
1083
+ value: filterQuery,
1084
+ cursorPosition: filterCursor,
1085
+ placeholder: "id, label, group, cluster",
1086
+ helperText: "Type to narrow by id, label, group, or cluster."
1087
+ }),
1088
+ React.createElement(SidebarList, {
1089
+ width: layout.sidebarWidth,
1090
+ height: sidebarHeight,
1091
+ title: filterQuery ? `Variables (${pages.length})` : "Variables",
1092
+ focused: focusArea === "sidebar",
1093
+ scrollState: {
1094
+ offset: safeSidebarOffset,
1095
+ viewportSize: sidebarViewportSize,
1096
+ totalSize: pages.length
1097
+ },
1098
+ items: visibleSidebar.map((page, index) => ({
1099
+ id: page.key,
1100
+ label: page.entry.id,
1101
+ active: safeSidebarOffset + index === safePageIndex,
1102
+ tone: page.required ? "required" : "normal"
1103
+ }))
1104
+ })
1105
+ ),
725
1106
  React.createElement(Text, null, " "),
726
1107
  React.createElement(
727
1108
  Box,
@@ -745,12 +1126,13 @@ async function runCliConfigEditor(context, options = {}) {
745
1126
  React.createElement(TextInputField, {
746
1127
  width: layout.contentWidth,
747
1128
  height: layout.inputHeight,
748
- label: "New value",
1129
+ label: "Value",
749
1130
  focused: focusArea === "content",
750
1131
  value: draftValue,
751
1132
  cursorPosition,
752
1133
  secret: selectedPage.entry.sensitivity === "secret",
753
- placeholder: selectedPage.suggestedValue || "(empty)"
1134
+ placeholder: "",
1135
+ helperText: ""
754
1136
  }),
755
1137
  React.createElement(
756
1138
  Box,
@@ -779,7 +1161,11 @@ async function runCliConfigEditor(context, options = {}) {
779
1161
  });
780
1162
  }
781
1163
  export {
1164
+ applyConfigInputInsertion,
782
1165
  buildCliConfigPages,
783
1166
  computeConfigViewportLayout,
1167
+ filterCliConfigPages,
1168
+ normalizeConfigInputChunk,
1169
+ readLinuxClipboardText,
784
1170
  runCliConfigEditor
785
1171
  };