Skip to content

[codex] Scaffold ollin tui prompts port - #1

Closed
gocanto wants to merge 676 commits into
mainfrom
chore/laravel-prompts-port
Closed

gocanto wants to merge 676 commits into
mainfrom
chore/laravel-prompts-port

Conversation

@gocanto

@gocanto gocanto commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • scaffold the ollin-tui pnpm/Turbo monorepo
  • add the initial @ollin/tui public helper surface for the upstream prompt toolkit port
  • add shared TypeScript config, artefact paths, local Vitest acceptance coverage, and the upstream parity map
  • add CI that only runs when the draft PR is marked ready for review

Validation

  • pnpm typecheck
  • pnpm test

Notes

This PR must remain draft while implementation continues phase by phase. It should not be marked ready for review yet.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new TypeScript-based terminal user interface (TUI) library, @ollin/tui, which aims for behavioral parity with laravel/prompts. The changes include the core monorepo scaffold, prompt helpers, environment management, and testing utilities. Feedback was provided regarding critical error handling in promptUntilValid, type safety improvements for multisearch, and stricter validation for choice selection.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/tui/src/prompt.ts Outdated
Comment on lines +46 to +60
while (true) {
const value = await read();
const required = ensureRequired(value, options.required);
const validation = required ?? (await validationMessage(value, options.validate));

if (!validation) {
return value;
}

if (!environment.interactive) {
throw new PromptValidationError(validation);
}

environment.error.write(renderError(validation));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The promptUntilValid function does not catch PromptValidationError thrown by the read callback. Prompts like number, select, and search throw PromptValidationError when validation fails. Without a try/catch block in the loop, any invalid input will crash the prompt process instead of displaying the error and re-prompting the user.

  while (true) {
    try {
      const value = await read();
      const required = ensureRequired(value, options.required);
      const validation = required ?? (await validationMessage(value, options.validate));

      if (!validation) {
        return value;
      }

      if (!environment.interactive) {
        throw new PromptValidationError(validation);
      }

      environment.error.write(renderError(validation));
    } catch (error) {
      if (error instanceof PromptValidationError) {
        if (!environment.interactive) {
          throw error;
        }
        environment.error.write(renderError(error.message));
      } else {
        throw error;
      }
    }
  }

Comment thread packages/tui/src/types.ts Outdated
Comment on lines +64 to +66
export type SearchPromptOptions<T> = BasePromptOptions<T> & {
options: Array<ChoiceInput<T>> | ((query: string) => MaybePromise<Array<ChoiceInput<T>>>);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current type definition for multisearch uses SearchPromptOptions<T[]>, which incorrectly forces the individual choices to have a value of type T[] instead of T. We should introduce a dedicated MultiSearchPromptOptions<T> type where options contains ChoiceInput<T> but the prompt itself returns T[].

export type SearchPromptOptions<T> = BasePromptOptions<T> & {
  options: Array<ChoiceInput<T>> | ((query: string) => MaybePromise<Array<ChoiceInput<T>>>);
};

export type MultiSearchPromptOptions<T> = BasePromptOptions<T[]> & {
  options: Array<ChoiceInput<T>> | ((query: string) => MaybePromise<Array<ChoiceInput<T>>>);
};

Comment thread packages/tui/src/prompts/choices.ts Outdated
Comment on lines +3 to +11
import type {
Choice,
ChoiceInput,
ConfirmPromptOptions,
MultiSelectPromptOptions,
SearchPromptOptions,
SelectPromptOptions,
TextPromptOptions
} from '#tui/types';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Import the newly created MultiSearchPromptOptions type from #tui/types.

Suggested change
import type {
Choice,
ChoiceInput,
ConfirmPromptOptions,
MultiSelectPromptOptions,
SearchPromptOptions,
SelectPromptOptions,
TextPromptOptions
} from '#tui/types';
import type {
Choice,
ChoiceInput,
ConfirmPromptOptions,
MultiSearchPromptOptions,
MultiSelectPromptOptions,
SearchPromptOptions,
SelectPromptOptions,
TextPromptOptions
} from '#tui/types';

Comment thread packages/tui/src/prompts/choices.ts Outdated
Comment on lines +113 to +129
export const multisearch = async <T>(options: SearchPromptOptions<T[]>): Promise<T[]> => {
return promptUntilValid(options, async () => {
const query = await ask(options.message, options.hint);
const source = typeof options.options === 'function' ? await options.options(query) : options.options;
const choices = normalizeChoices(source);

if (query.trim() === '' && options.default !== undefined) {
return options.default;
}

return query
.split(',')
.map((part) => findChoice(choices, part.trim()))
.filter((choice): choice is Choice<T[]> => choice !== undefined && !choice.disabled)
.flatMap((choice) => choice.value);
});
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update multisearch to use MultiSearchPromptOptions<T> so that choices are correctly typed as Choice<T> and mapped to their individual values instead of using flatMap on T[].

Suggested change
export const multisearch = async <T>(options: SearchPromptOptions<T[]>): Promise<T[]> => {
return promptUntilValid(options, async () => {
const query = await ask(options.message, options.hint);
const source = typeof options.options === 'function' ? await options.options(query) : options.options;
const choices = normalizeChoices(source);
if (query.trim() === '' && options.default !== undefined) {
return options.default;
}
return query
.split(',')
.map((part) => findChoice(choices, part.trim()))
.filter((choice): choice is Choice<T[]> => choice !== undefined && !choice.disabled)
.flatMap((choice) => choice.value);
});
};
export const multisearch = async <T>(options: MultiSearchPromptOptions<T>): Promise<T[]> => {
return promptUntilValid(options, async () => {
const query = await ask(options.message, options.hint);
const source = typeof options.options === 'function' ? await options.options(query) : options.options;
const choices = normalizeChoices(source);
if (query.trim() === '' && options.default !== undefined) {
return options.default;
}
return query
.split(',')
.map((part) => findChoice(choices, part.trim()))
.filter((choice): choice is Choice<T> => choice !== undefined && !choice.disabled)
.map((choice) => choice.value);
});
};

Comment thread packages/tui/src/index.ts Outdated
PromptInput,
PromptOutput,
PromptValue,
SearchPromptOptions,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Export the new MultiSearchPromptOptions type.

Suggested change
SearchPromptOptions,
MultiSearchPromptOptions,
SearchPromptOptions,

Comment thread packages/tui/src/prompts/choices.ts Outdated
Comment on lines +26 to +34
const findChoice = <T>(choices: Array<Choice<T>>, answer: string): Choice<T> | undefined => {
const index = Number.parseInt(answer, 10);

if (!Number.isNaN(index)) {
return choices[index - 1];
}

return choices.find((choice) => choice.label === answer || String(choice.value) === answer);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Number.parseInt directly can lead to false positives (e.g., '1abc' parses to 1 and matches the first choice). We should strictly check if the answer is a numeric string before parsing it as an index.

const findChoice = <T>(choices: Array<Choice<T>>, answer: string): Choice<T> | undefined => {
  if (/^\d+$/.test(answer)) {
    const index = Number.parseInt(answer, 10);
    return choices[index - 1];
  }

  return choices.find((choice) => choice.label === answer || String(choice.value) === answer);
};

Comment thread packages/tui/src/prompts/choices.ts Outdated
Comment on lines +79 to +84
return answer
.split(',')
.map((part) => findChoice(choices, part.trim()))
.filter((choice): choice is Choice<T> => choice !== undefined && !choice.disabled)
.map((choice) => choice.value);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In multiselect, invalid comma-separated options are silently filtered out and ignored. If a user makes a typo (e.g., 'option1, opton2'), the second option is silently dropped instead of raising a validation error. Consider validating that all entered parts map to valid choices.

@gocanto gocanto closed this Jun 11, 2026
@gocanto
gocanto deleted the chore/laravel-prompts-port branch June 11, 2026 05:46
@gocanto gocanto reopened this Jun 11, 2026
gocanto and others added 29 commits June 17, 2026 10:31
@gocanto gocanto closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant