@unscientificjszhai/howto 1.0.0 → 1.0.1

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.
Files changed (40) hide show
  1. package/README.CN.md +32 -3
  2. package/README.md +32 -3
  3. package/THIRD_PARTY_NOTICES.md +5 -4
  4. package/dist/ai/errors.js +7 -23
  5. package/dist/ai/gemini.js +67 -27
  6. package/dist/ai/openai.js +35 -27
  7. package/dist/cli.js +12 -1
  8. package/dist/config-file.js +97 -51
  9. package/dist/config.js +7 -6
  10. package/dist/errors.js +17 -16
  11. package/dist/execute.js +12 -25
  12. package/dist/index.js +84 -95
  13. package/dist/init/InitializationApp.js +175 -32
  14. package/dist/init/index.js +35 -33
  15. package/dist/init/state.js +46 -23
  16. package/dist/prompt.js +4 -2
  17. package/dist/safety/dangerous-command.js +262 -93
  18. package/dist/shell/command-analysis.js +373 -0
  19. package/dist/terminal-text.js +33 -0
  20. package/dist/ui/App.js +64 -25
  21. package/dist/ui/ConfirmView.js +163 -67
  22. package/dist/ui/InteractiveSessionProvider.js +19 -0
  23. package/dist/ui/ResolvePlaceholdersView.js +107 -27
  24. package/dist/ui/SelectCommandView.js +73 -23
  25. package/dist/ui/SelectedCommandDisplay.js +11 -9
  26. package/dist/ui/interactive-session.js +407 -0
  27. package/dist/ui/paste-framing.js +105 -0
  28. package/dist/ui/placeholder-logic.js +28 -16
  29. package/dist/ui/resize-safe-output.js +73 -0
  30. package/dist/ui/run-interactive-command.js +55 -0
  31. package/dist/ui/single-line-preview.js +28 -0
  32. package/dist/ui/text-input.js +53 -0
  33. package/dist/ui/use-keyboard-input.js +12 -0
  34. package/dist/ui/use-paste-aware-input.js +7 -0
  35. package/dist/user-visible-error.js +19 -0
  36. package/dist/validation/ai-response.js +11 -4
  37. package/dist/validation/command-tool.js +25 -127
  38. package/dist/validation/generated-commands.js +5 -16
  39. package/dist/version.js +38 -0
  40. package/package.json +18 -15
package/dist/config.js CHANGED
@@ -11,7 +11,6 @@ export function hasExplicitAiProvider(options, env, fileConfig = {}) {
11
11
  return (pickConfigValue(options.aiProvider, env.HOWTO_AI_PROVIDER, fileConfig.aiProvider) !== undefined);
12
12
  }
13
13
  export function loadConfig(options, env, fileConfig = {}) {
14
- var _a, _b, _c;
15
14
  const aiProviderValue = pickConfigValue(options.aiProvider, env.HOWTO_AI_PROVIDER, fileConfig.aiProvider);
16
15
  if (aiProviderValue === undefined) {
17
16
  throw new ConfigError("AI provider is not configured. Set --ai-provider or HOWTO_AI_PROVIDER, or run howto in an interactive terminal to initialize it.");
@@ -21,11 +20,14 @@ export function loadConfig(options, env, fileConfig = {}) {
21
20
  aiProvider,
22
21
  gemini: {
23
22
  apiKey: pickConfigValue(options.geminiApiKey, env.HOWTO_GEMINI_API_KEY, fileConfig.geminiApiKey),
24
- model: (_a = pickConfigValue(options.geminiModel, env.HOWTO_GEMINI_MODEL, fileConfig.geminiModel)) !== null && _a !== void 0 ? _a : DEFAULT_GEMINI_MODEL,
23
+ model: pickConfigValue(options.geminiModel, env.HOWTO_GEMINI_MODEL, fileConfig.geminiModel) ??
24
+ DEFAULT_GEMINI_MODEL,
25
25
  },
26
26
  openai: {
27
- apiKey: (_b = pickConfigValue(options.openaiApiKey, env.HOWTO_OPENAI_API_KEY, fileConfig.openaiApiKey)) !== null && _b !== void 0 ? _b : "",
28
- model: (_c = pickConfigValue(options.openaiModel, env.HOWTO_OPENAI_MODEL, fileConfig.openaiModel)) !== null && _c !== void 0 ? _c : DEFAULT_OPENAI_MODEL,
27
+ apiKey: pickConfigValue(options.openaiApiKey, env.HOWTO_OPENAI_API_KEY, fileConfig.openaiApiKey) ??
28
+ "",
29
+ model: pickConfigValue(options.openaiModel, env.HOWTO_OPENAI_MODEL, fileConfig.openaiModel) ??
30
+ DEFAULT_OPENAI_MODEL,
29
31
  baseUrl: pickConfigValue(options.openaiApiUrl, env.HOWTO_OPENAI_API_URL, fileConfig.openaiApiUrl),
30
32
  },
31
33
  structuredOutput: parseStructuredOutput(pickConfigValue(options.structuredOutput, env.HOWTO_STRUCTURED_OUTPUT, fileConfig.structuredOutput)),
@@ -36,8 +38,7 @@ export function loadConfig(options, env, fileConfig = {}) {
36
38
  return config;
37
39
  }
38
40
  function pickConfigValue(cliValue, envValue, fileValue) {
39
- var _a;
40
- return (_a = cliValue !== null && cliValue !== void 0 ? cliValue : envValue) !== null && _a !== void 0 ? _a : fileValue;
41
+ return cliValue ?? envValue ?? fileValue;
41
42
  }
42
43
  function parseAiProvider(value) {
43
44
  if (value === "openai" || value === "gemini") {
package/dist/errors.js CHANGED
@@ -4,7 +4,16 @@ import { ConfigError } from "./config.js";
4
4
  import { AiResponseValidationError } from "./validation/ai-response.js";
5
5
  import { InteractionCancelledError, InteractiveTtyError } from "./ui/tty.js";
6
6
  import { PlaceholderResolutionError } from "./ui/placeholder-logic.js";
7
+ import { sanitizeUserVisibleErrorMessage } from "./user-visible-error.js";
8
+ export { sanitizeUserVisibleErrorMessage };
9
+ export function getUserVisibleErrorMessage(error) {
10
+ if (error instanceof AiProviderError) {
11
+ return `AI provider request failed (provider: ${error.provider}, model: ${sanitizeUserVisibleErrorMessage(error.model)})`;
12
+ }
13
+ return sanitizeUserVisibleErrorMessage(error.message);
14
+ }
7
15
  export class AppError extends Error {
16
+ exitCode;
8
17
  constructor(message, exitCode) {
9
18
  super(message);
10
19
  this.name = "AppError";
@@ -13,30 +22,30 @@ export class AppError extends Error {
13
22
  }
14
23
  export function toAppError(error) {
15
24
  if (error instanceof AppError) {
16
- return error;
25
+ return new AppError(sanitizeUserVisibleErrorMessage(error.message), error.exitCode);
17
26
  }
18
27
  if (error instanceof CliParseError) {
19
- return new AppError(`Error: ${error.message}\n${USAGE}`, 2);
28
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}\n${USAGE}`, 2);
20
29
  }
21
30
  if (error instanceof ConfigError) {
22
- return new AppError(`Configuration error: ${error.message}`, 2);
31
+ return new AppError(`Configuration error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
23
32
  }
24
33
  if (error instanceof AiProviderError) {
25
- return new AppError(error.message, 1);
34
+ return new AppError(getUserVisibleErrorMessage(error), 1);
26
35
  }
27
36
  if (error instanceof AiResponseValidationError) {
28
- return new AppError(`AI response format error: ${error.message}`, 2);
37
+ return new AppError(`AI response format error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
29
38
  }
30
39
  if (error instanceof InteractiveTtyError) {
31
- return new AppError(`Error: ${error.message}`, 2);
40
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
32
41
  }
33
42
  if (error instanceof PlaceholderResolutionError) {
34
- return new AppError(`Error: ${error.message}`, 2);
43
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
35
44
  }
36
45
  if (error instanceof InteractionCancelledError) {
37
46
  return new AppError("", 130);
38
47
  }
39
- return new AppError(`Error: ${sanitizeErrorMessage(getErrorMessage(error))}`, 1);
48
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(getErrorMessage(error))}`, 1);
40
49
  }
41
50
  function getErrorMessage(error) {
42
51
  if (error instanceof Error && error.message.trim() !== "") {
@@ -47,11 +56,3 @@ function getErrorMessage(error) {
47
56
  }
48
57
  return "unknown error";
49
58
  }
50
- function sanitizeErrorMessage(message) {
51
- const singleLine = message.replace(/\s+/g, " ").trim();
52
- const redacted = singleLine
53
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
54
- .replace(/(api[-_ ]?key["'\s:=]+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]")
55
- .replace(/(authorization["'\s:=]+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]");
56
- return redacted.length > 220 ? `${redacted.slice(0, 217)}...` : redacted;
57
- }
package/dist/execute.js CHANGED
@@ -1,38 +1,25 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  import { spawn } from "child_process";
11
2
  import { constants as osConstants } from "os";
12
- export function executeCommand(command_1) {
13
- return __awaiter(this, arguments, void 0, function* (command, options = {}) {
14
- var _a, _b, _c;
15
- const env = (_a = options.env) !== null && _a !== void 0 ? _a : process.env;
16
- const platform = (_b = options.platform) !== null && _b !== void 0 ? _b : process.platform;
17
- const spawnCommand = (_c = options.spawnCommand) !== null && _c !== void 0 ? _c : spawnCommandWithNodeSpawn;
18
- const child = platform === "win32"
19
- ? spawnCommand(command, { shell: env.SHELL || true, stdio: "inherit" }, { stdio: "inherit" })
20
- : spawnCommand(resolveShell(env), ["-lc", `exec ${command}`], { stdio: "inherit" });
21
- return new Promise((resolve, reject) => {
22
- child.once("error", reject);
23
- child.once("close", (code, signal) => {
24
- resolve(resolveProcessExitCode(code, signal));
25
- });
3
+ export async function executeCommand(command, options = {}) {
4
+ const env = options.env ?? process.env;
5
+ const platform = options.platform ?? process.platform;
6
+ const spawnCommand = options.spawnCommand ?? spawnCommandWithNodeSpawn;
7
+ const child = platform === "win32"
8
+ ? spawnCommand(command, { shell: env.SHELL || true, stdio: "inherit" }, { stdio: "inherit" })
9
+ : spawnCommand(resolveShell(env), ["-c", command], { stdio: "inherit" });
10
+ return new Promise((resolve, reject) => {
11
+ child.once("error", reject);
12
+ child.once("close", (code, signal) => {
13
+ resolve(resolveProcessExitCode(code, signal));
26
14
  });
27
15
  });
28
16
  }
29
17
  export function resolveProcessExitCode(code, signal) {
30
- var _a;
31
18
  if (typeof code === "number") {
32
19
  return code;
33
20
  }
34
21
  if (signal !== null) {
35
- return 128 + ((_a = osConstants.signals[signal]) !== null && _a !== void 0 ? _a : 1);
22
+ return 128 + (osConstants.signals[signal] ?? 1);
36
23
  }
37
24
  return 1;
38
25
  }
package/dist/index.js CHANGED
@@ -1,17 +1,6 @@
1
1
  #!/usr/bin/env node
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- import React from "react";
12
2
  import { realpathSync } from "node:fs";
13
3
  import { fileURLToPath } from "node:url";
14
- import { render } from "ink";
15
4
  import { CliParseError, parseCliArgs } from "./cli.js";
16
5
  import { ConfigError, hasExplicitAiProvider, loadConfig } from "./config.js";
17
6
  import { readUserConfigFile } from "./config-file.js";
@@ -20,102 +9,102 @@ import { buildCommandGenerationPrompt, createProviderPromptRequest } from "./pro
20
9
  import { checkCommandInPath } from "./validation/command-tool.js";
21
10
  import { generateValidatedCommandCandidates } from "./validation/generated-commands.js";
22
11
  import { ensureInteractiveTty } from "./ui/tty.js";
23
- import { executeCommand } from "./execute.js";
24
12
  import { toAppError } from "./errors.js";
25
- import { App } from "./ui/App.js";
13
+ import { createInteractiveSession } from "./ui/interactive-session.js";
14
+ import { runInteractiveCommand } from "./ui/run-interactive-command.js";
26
15
  import { initializeConfig } from "./init/index.js";
27
- function run(argv) {
28
- return __awaiter(this, void 0, void 0, function* () {
29
- try {
30
- const parsedCli = parseCliArgs(argv);
31
- if (parsedCli.options.init) {
32
- ensureInteractiveTty(process.stdin, process.stdout);
33
- yield initializeConfig({
34
- cliOptions: parsedCli.options,
35
- env: process.env,
36
- input: process.stdin,
37
- output: process.stdout,
38
- });
39
- return { exitCode: 0 };
40
- }
41
- const fileConfig = yield readUserConfigFile();
42
- if (parsedCli.question === undefined) {
43
- throw new CliParseError("missing question");
44
- }
45
- if (!hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)) {
46
- if (parsedCli.options.print) {
47
- throw new ConfigError("AI provider is not configured. --print cannot run initialization; run howto --init or set --ai-provider, HOWTO_AI_PROVIDER, or ~/.howto/config.json.");
48
- }
49
- ensureInteractiveTty(process.stdin, process.stdout);
50
- }
51
- const config = hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)
52
- ? loadConfig(parsedCli.options, process.env, fileConfig)
53
- : yield initializeConfig({
54
- cliOptions: parsedCli.options,
55
- env: process.env,
56
- input: process.stdin,
57
- output: process.stdout,
58
- });
59
- const useCommandPathCheck = parsedCli.useCommand === undefined
60
- ? undefined
61
- : checkCommandInPath(parsedCli.useCommand, process.env);
62
- if (!parsedCli.options.print) {
63
- ensureInteractiveTty(process.stdin, process.stdout);
64
- }
65
- const promptRequest = createProviderPromptRequest({
66
- question: parsedCli.question,
67
- arguments: parsedCli.arguments,
68
- useCommand: parsedCli.useCommand,
69
- structuredOutput: config.structuredOutput,
16
+ import { renderTerminalSafeText } from "./terminal-text.js";
17
+ import { readPackageVersion } from "./version.js";
18
+ async function run(argv) {
19
+ let session;
20
+ const getSession = () => {
21
+ ensureInteractiveTty(process.stdin, process.stdout);
22
+ session ??= createInteractiveSession({ input: process.stdin, output: process.stdout });
23
+ return session;
24
+ };
25
+ try {
26
+ const parsedCli = parseCliArgs(argv);
27
+ if (parsedCli.options.version) {
28
+ console.log(readPackageVersion());
29
+ return { exitCode: 0 };
30
+ }
31
+ if (parsedCli.options.init) {
32
+ ensureInteractiveTty(process.stdin, process.stdout);
33
+ await initializeConfig({
34
+ cliOptions: parsedCli.options,
35
+ env: process.env,
36
+ session: getSession(),
70
37
  });
71
- const { systemPrompt, userPrompt } = buildCommandGenerationPrompt(promptRequest);
72
- const provider = createCommandProvider(config);
38
+ return { exitCode: 0 };
39
+ }
40
+ const fileConfig = await readUserConfigFile();
41
+ if (parsedCli.question === undefined) {
42
+ throw new CliParseError("missing question");
43
+ }
44
+ if (!hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)) {
73
45
  if (parsedCli.options.print) {
74
- const candidates = yield generateValidatedCommandCandidates(provider, Object.assign(Object.assign({}, promptRequest), { systemPrompt,
75
- userPrompt }));
76
- candidates.forEach((candidate) => {
77
- console.log(candidate.command);
78
- });
79
- return { exitCode: 0 };
80
- }
81
- if (useCommandPathCheck !== undefined && !useCommandPathCheck.found) {
82
- console.error(`Warning: requested command "${useCommandPathCheck.command}" was not found in PATH. Review before executing any generated command.`);
46
+ throw new ConfigError("AI provider is not configured. --print cannot run initialization; run howto --init or set --ai-provider, HOWTO_AI_PROVIDER, or ~/.howto/config.json.");
83
47
  }
84
- let finalCommand;
85
- let appError;
86
- const { waitUntilExit, unmount, clear } = render(React.createElement(App, { provider: provider, request: Object.assign(Object.assign({}, promptRequest), { systemPrompt, userPrompt }), onSuccess: (command) => {
87
- finalCommand = command;
88
- clear();
89
- unmount();
90
- }, onError: (error) => {
91
- appError = error;
92
- clear();
93
- unmount();
94
- } }));
95
- yield waitUntilExit();
96
- if (appError) {
97
- throw appError;
98
- }
99
- if (finalCommand === undefined) {
100
- return { exitCode: 1 };
101
- }
102
- const exitCode = yield executeCommand(finalCommand);
103
- return { exitCode };
48
+ ensureInteractiveTty(process.stdin, process.stdout);
104
49
  }
105
- catch (error) {
106
- const appError = toAppError(error);
107
- if (appError.message !== "") {
108
- console.error(appError.message);
109
- }
110
- return { exitCode: appError.exitCode };
50
+ const config = hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)
51
+ ? loadConfig(parsedCli.options, process.env, fileConfig)
52
+ : await initializeConfig({
53
+ cliOptions: parsedCli.options,
54
+ env: process.env,
55
+ session: getSession(),
56
+ });
57
+ const useCommandPathCheck = parsedCli.useCommand === undefined
58
+ ? undefined
59
+ : checkCommandInPath(parsedCli.useCommand, process.env);
60
+ if (!parsedCli.options.print) {
61
+ ensureInteractiveTty(process.stdin, process.stdout);
111
62
  }
112
- });
63
+ const promptRequest = createProviderPromptRequest({
64
+ question: parsedCli.question,
65
+ arguments: parsedCli.arguments,
66
+ useCommand: parsedCli.useCommand,
67
+ structuredOutput: config.structuredOutput,
68
+ });
69
+ const { systemPrompt, userPrompt } = buildCommandGenerationPrompt(promptRequest);
70
+ const provider = createCommandProvider(config);
71
+ if (parsedCli.options.print) {
72
+ const candidates = await generateValidatedCommandCandidates(provider, {
73
+ ...promptRequest,
74
+ systemPrompt,
75
+ userPrompt,
76
+ });
77
+ candidates.forEach((candidate) => {
78
+ console.log(candidate.command);
79
+ });
80
+ return { exitCode: 0 };
81
+ }
82
+ if (useCommandPathCheck !== undefined && !useCommandPathCheck.found) {
83
+ console.error(`Warning: requested command "${renderTerminalSafeText(useCommandPathCheck.command)}" was not found in PATH. Review before executing any generated command.`);
84
+ }
85
+ const exitCode = await runInteractiveCommand({
86
+ session: getSession(),
87
+ provider,
88
+ request: { ...promptRequest, systemPrompt, userPrompt },
89
+ });
90
+ return { exitCode };
91
+ }
92
+ catch (error) {
93
+ const appError = toAppError(error);
94
+ if (appError.message !== "") {
95
+ console.error(appError.message);
96
+ }
97
+ return { exitCode: appError.exitCode };
98
+ }
99
+ finally {
100
+ session?.dispose();
101
+ }
113
102
  }
114
103
  function resolveEntrypointPath(path) {
115
104
  try {
116
105
  return realpathSync(path);
117
106
  }
118
- catch (_a) {
107
+ catch {
119
108
  return path;
120
109
  }
121
110
  }
@@ -1,19 +1,41 @@
1
- import React, { useState } from "react";
2
- import { Box, Text, useInput } from "ink";
1
+ import React, { useRef, useState } from "react";
2
+ import { Box, Text } from "ink";
3
+ import { getUserVisibleErrorMessage } from "../errors.js";
4
+ import { usePhysicalStdoutRows } from "../ui/resize-safe-output.js";
5
+ import { toSingleLinePreview } from "../ui/single-line-preview.js";
6
+ import { useInteractiveSession } from "../ui/InteractiveSessionProvider.js";
7
+ import { usePasteAwareInput } from "../ui/use-paste-aware-input.js";
3
8
  import { applyInitializationInput, createInitialInitializationState, getProviderOptions, } from "./state.js";
9
+ const RICH_LAYOUT_MIN_ROWS = 15;
4
10
  export const InitializationApp = ({ onSubmit, onComplete, onCancel, onError }) => {
11
+ const session = useInteractiveSession();
12
+ const rows = usePhysicalStdoutRows();
5
13
  const [state, setState] = useState(createInitialInitializationState);
14
+ const stateRef = useRef(state);
6
15
  const [submitStatus, setSubmitStatus] = useState("editing");
16
+ const submitStatusRef = useRef(submitStatus);
7
17
  const [errorMessage, setErrorMessage] = useState("");
8
- useInput((input, key) => {
9
- const update = applyInitializationInput(state, { input, key });
18
+ const frameRows = Math.max(0, rows - 1);
19
+ const handleInput = (event) => {
20
+ if (frameRows === 0) {
21
+ return;
22
+ }
23
+ if (submitStatusRef.current !== "editing") {
24
+ return;
25
+ }
26
+ const update = applyInitializationInput(stateRef.current, event);
27
+ stateRef.current = update.state;
10
28
  setState(update.state);
11
29
  if (update.cancelled) {
30
+ session.suspendViewInput();
31
+ submitStatusRef.current = "cancelled";
12
32
  setSubmitStatus("cancelled");
13
33
  onCancel();
14
34
  return;
15
35
  }
16
36
  if (update.completedValues !== undefined) {
37
+ session.suspendViewInput();
38
+ submitStatusRef.current = "saving";
17
39
  setSubmitStatus("saving");
18
40
  onSubmit(update.completedValues)
19
41
  .then((config) => {
@@ -21,33 +43,72 @@ export const InitializationApp = ({ onSubmit, onComplete, onCancel, onError }) =
21
43
  })
22
44
  .catch((error) => {
23
45
  const appError = normalizeError(error);
24
- setErrorMessage(appError.message);
46
+ setErrorMessage(getUserVisibleErrorMessage(appError));
47
+ submitStatusRef.current = "error";
25
48
  setSubmitStatus("error");
26
49
  onError(appError);
27
50
  });
28
51
  }
29
- }, { isActive: submitStatus === "editing" });
52
+ };
53
+ usePasteAwareInput({
54
+ onInput: (input, key) => {
55
+ handleInput({ type: "keyboard", input, key });
56
+ },
57
+ onPaste: (input) => {
58
+ handleInput({ type: "paste", input });
59
+ },
60
+ isInputActive: frameRows === 0 || submitStatus === "editing",
61
+ isPasteActive: frameRows > 0 && submitStatus === "editing",
62
+ });
63
+ let content;
30
64
  if (submitStatus === "saving") {
31
- return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
32
- React.createElement(Text, { color: "green" }, "Saving howto configuration...")));
65
+ content = (React.createElement(InitializationStatusLine, { frameRows: frameRows, message: "Saving howto configuration...", color: "green" }));
66
+ }
67
+ else if (submitStatus === "error") {
68
+ content = (React.createElement(InitializationStatusLine, { frameRows: frameRows, message: `Error: ${errorMessage}`, color: "red" }));
33
69
  }
34
- if (submitStatus === "error") {
35
- return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
36
- React.createElement(Text, { color: "red" },
37
- "Error: ",
38
- errorMessage)));
70
+ else if (submitStatus === "cancelled") {
71
+ content = (React.createElement(InitializationStatusLine, { frameRows: frameRows, message: "Initialization cancelled.", dimColor: true }));
39
72
  }
40
- if (submitStatus === "cancelled") {
41
- return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
42
- React.createElement(Text, { dimColor: true }, "Initialization cancelled.")));
73
+ else if (state.step === "provider") {
74
+ content = React.createElement(ProviderStep, { selectedIndex: state.selectedIndex, frameRows: frameRows });
43
75
  }
44
- if (state.step === "provider") {
45
- return React.createElement(ProviderStep, { selectedIndex: state.selectedIndex });
76
+ else {
77
+ content = React.createElement(InputStep, { state: state, frameRows: frameRows });
46
78
  }
47
- return React.createElement(InputStep, { state: state });
79
+ return (React.createElement(Box, { display: frameRows > 0 ? "flex" : "none", flexDirection: "column", maxHeight: frameRows, overflowX: "hidden", overflowY: "hidden" }, content));
48
80
  };
49
- const ProviderStep = ({ selectedIndex }) => {
81
+ const ClippedLine = ({ children, color, dimColor = false, prefix, prefixWidth }) => (React.createElement(Box, { width: "100%", height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" }, prefix === undefined ? (React.createElement(Text, { color: color, dimColor: dimColor }, children)) : (React.createElement(React.Fragment, null,
82
+ React.createElement(Box, { width: prefixWidth, flexShrink: 0 },
83
+ React.createElement(Text, { color: color, dimColor: dimColor }, prefix)),
84
+ React.createElement(Box, { flexGrow: 1, flexShrink: 1, minWidth: 1, height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" },
85
+ React.createElement(Text, { color: color, dimColor: dimColor }, children))))));
86
+ const ProviderStep = ({ selectedIndex, frameRows, }) => {
50
87
  const providers = getProviderOptions();
88
+ const selectedProvider = selectedIndex === null ? undefined : providers[selectedIndex];
89
+ const providerSelectionLine = selectedProvider === "openai"
90
+ ? "> openai 2 gemini"
91
+ : selectedProvider === "gemini"
92
+ ? "1 openai > gemini"
93
+ : "1 openai 2 gemini";
94
+ if (frameRows < RICH_LAYOUT_MIN_ROWS) {
95
+ if (frameRows === 1) {
96
+ const compactSelection = selectedProvider === "openai" ? "O" : selectedProvider === "gemini" ? "G" : "-";
97
+ return (React.createElement(Box, { height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" },
98
+ React.createElement(Text, null, `O/G:${compactSelection} UD Ent 1/2 Esc`)));
99
+ }
100
+ if (frameRows <= 3) {
101
+ return (React.createElement(Box, { flexDirection: "column", height: frameRows, maxHeight: frameRows, overflowX: "hidden", overflowY: "hidden" },
102
+ frameRows === 3 && React.createElement(Text, { color: "green" }, "? Choose provider"),
103
+ React.createElement(ClippedLine, null, providerSelectionLine),
104
+ React.createElement(Text, { dimColor: true }, "UD Enter 1/2 Esc")));
105
+ }
106
+ return (React.createElement(Box, { flexDirection: "column", maxHeight: 4, overflowX: "hidden", overflowY: "hidden" },
107
+ React.createElement(ClippedLine, null, "howto needs an AI provider before it can call AI."),
108
+ React.createElement(ClippedLine, { color: "green" }, "? Choose provider"),
109
+ React.createElement(ClippedLine, null, providerSelectionLine),
110
+ React.createElement(ClippedLine, { dimColor: true }, "UD Enter 1/2 Esc")));
111
+ }
51
112
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
52
113
  React.createElement(Text, null, "howto needs an AI provider before it can call AI."),
53
114
  React.createElement(Box, { height: 1 },
@@ -65,27 +126,109 @@ const ProviderStep = ({ selectedIndex }) => {
65
126
  React.createElement(Box, { flexShrink: 0, marginTop: 1 },
66
127
  React.createElement(Text, { dimColor: true }, "Use Up/Down or 1/2 to choose, Enter to continue, Esc or Ctrl+C to cancel."))));
67
128
  };
68
- const InputStep = ({ state, }) => {
129
+ const InputStep = ({ state, frameRows }) => {
69
130
  const currentField = state.fields[state.fieldIndex];
131
+ const shortLabel = getShortFieldLabel(currentField);
132
+ if (frameRows < RICH_LAYOUT_MIN_ROWS) {
133
+ const fieldValue = getFieldValuePreview(currentField);
134
+ const fieldLine = `${shortLabel}: ${fieldValue}`;
135
+ const statusLine = state.errorMessage === undefined ? fieldLine : `! ${shortLabel} required`;
136
+ if (frameRows === 1) {
137
+ const compactField = state.errorMessage === undefined
138
+ ? `${shortLabel}:${getCompactFieldValuePreview(currentField)}`
139
+ : `!${shortLabel}:req`;
140
+ return (React.createElement(Box, { height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" },
141
+ React.createElement(Box, { flexGrow: 1, flexShrink: 1, minWidth: 1, height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" },
142
+ React.createElement(Text, null, compactField)),
143
+ React.createElement(Box, { flexShrink: 0 },
144
+ React.createElement(Text, { dimColor: true }, " Ent Esc ^C"))));
145
+ }
146
+ if (frameRows <= 3) {
147
+ return (React.createElement(Box, { flexDirection: "column", height: frameRows, maxHeight: frameRows, overflowX: "hidden", overflowY: "hidden" },
148
+ frameRows === 3 && (React.createElement(ClippedLine, { color: state.errorMessage === undefined ? "green" : "red" }, state.errorMessage === undefined ? `? Configure ${state.provider}` : statusLine)),
149
+ frameRows === 3 || state.errorMessage === undefined ? (React.createElement(ClippedLine, { prefix: `${shortLabel}:`, prefixWidth: shortLabel.length + 2 }, fieldValue)) : (React.createElement(ClippedLine, null, statusLine)),
150
+ React.createElement(Text, { dimColor: true }, "Ent next Esc back ^C")));
151
+ }
152
+ const defaultPreview = currentField.defaultValue !== undefined && currentField.value === ""
153
+ ? `Default: ${toSingleLinePreview(currentField.defaultValue)}`
154
+ : undefined;
155
+ const guidance = defaultPreview ?? getFullFieldGuidance(currentField);
156
+ return (React.createElement(Box, { flexDirection: "column", maxHeight: 4, overflowX: "hidden", overflowY: "hidden" },
157
+ React.createElement(ClippedLine, { color: state.errorMessage === undefined ? "green" : "red" }, state.errorMessage === undefined ? `? Configure ${state.provider}` : statusLine),
158
+ React.createElement(ClippedLine, { prefix: `${shortLabel}:`, prefixWidth: shortLabel.length + 2 }, fieldValue),
159
+ guidance !== undefined && React.createElement(ClippedLine, { dimColor: true }, guidance),
160
+ React.createElement(ClippedLine, { dimColor: true }, "Ent next Esc back ^C")));
161
+ }
70
162
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
71
163
  React.createElement(Text, { color: "green" },
72
164
  "? Configure ",
73
165
  state.provider),
74
166
  React.createElement(Box, { flexDirection: "column", marginTop: 1 }, state.fields.map((field, index) => (React.createElement(FieldRow, { key: field.name, field: field, isActive: index === state.fieldIndex })))),
75
- currentField.defaultValue !== undefined && currentField.value === "" && (React.createElement(Text, { dimColor: true },
76
- "Default: ",
77
- currentField.defaultValue)),
78
- state.errorMessage !== undefined && React.createElement(Text, { color: "red" }, state.errorMessage),
167
+ getFullFieldGuidance(currentField) !== undefined && (React.createElement(ClippedLine, { dimColor: true }, getFullFieldGuidance(currentField))),
168
+ currentField.defaultValue !== undefined && currentField.value === "" && (React.createElement(ClippedLine, { dimColor: true, prefix: "Default:", prefixWidth: 9 }, toSingleLinePreview(currentField.defaultValue))),
169
+ state.errorMessage !== undefined && (React.createElement(ClippedLine, { color: "red" }, toSingleLinePreview(state.errorMessage))),
79
170
  React.createElement(Box, { flexShrink: 0, marginTop: 1 },
80
171
  React.createElement(Text, { dimColor: true }, "Press Enter for next value, Esc to go back, Ctrl+C to cancel."))));
81
172
  };
82
- const FieldRow = ({ field, isActive, }) => (React.createElement(Box, null,
83
- React.createElement(Text, { color: isActive ? "cyan" : undefined }, isActive ? "> " : " "),
84
- React.createElement(Text, null,
85
- field.label,
86
- ": "),
87
- React.createElement(Text, null, field.value),
88
- isActive && React.createElement(Text, { backgroundColor: "white" }, " ")));
173
+ const FieldRow = ({ field, isActive, }) => {
174
+ const shortLabel = getShortFieldLabel(field);
175
+ return (React.createElement(Box, { height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" },
176
+ React.createElement(Box, { width: 2, flexShrink: 0 },
177
+ React.createElement(Text, { color: isActive ? "cyan" : undefined }, isActive ? "> " : " ")),
178
+ React.createElement(Box, { width: shortLabel.length + 2, flexShrink: 0 },
179
+ React.createElement(Text, null,
180
+ shortLabel,
181
+ ":")),
182
+ React.createElement(Box, { flexShrink: 1, minWidth: 1, height: 1, maxHeight: 1, overflowX: "hidden", overflowY: "hidden" },
183
+ React.createElement(Text, null, toSingleLinePreview(field.value))),
184
+ isActive && (React.createElement(Box, { width: 1, flexShrink: 0 },
185
+ React.createElement(Text, { backgroundColor: "white" }, " ")))));
186
+ };
187
+ const InitializationStatusLine = ({ frameRows, message, color, dimColor = false }) => {
188
+ const status = (React.createElement(ClippedLine, { color: color, dimColor: dimColor }, toSingleLinePreview(message)));
189
+ if (frameRows < 4) {
190
+ return status;
191
+ }
192
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 }, status));
193
+ };
194
+ function getShortFieldLabel(field) {
195
+ switch (field.name) {
196
+ case "apiKey":
197
+ return "Key";
198
+ case "model":
199
+ return "Model";
200
+ case "openaiBaseUrl":
201
+ return "URL";
202
+ }
203
+ }
204
+ function getFullFieldGuidance(field) {
205
+ switch (field.name) {
206
+ case "apiKey":
207
+ return field.required ? "Required." : "Optional; empty allowed.";
208
+ case "model":
209
+ return undefined;
210
+ case "openaiBaseUrl":
211
+ return "Empty uses official OpenAI URL.";
212
+ }
213
+ }
214
+ function getFieldValuePreview(field) {
215
+ if (field.value !== "") {
216
+ return toSingleLinePreview(field.value);
217
+ }
218
+ if (field.defaultValue !== undefined) {
219
+ return toSingleLinePreview(field.defaultValue);
220
+ }
221
+ return field.required ? "<required>" : "<empty>";
222
+ }
223
+ function getCompactFieldValuePreview(field) {
224
+ if (field.value !== "") {
225
+ return toSingleLinePreview(field.value);
226
+ }
227
+ if (field.defaultValue !== undefined) {
228
+ return toSingleLinePreview(field.defaultValue);
229
+ }
230
+ return field.required ? "req" : "-";
231
+ }
89
232
  function normalizeError(error) {
90
233
  if (error instanceof Error) {
91
234
  return error;