From 482be760b3327fcee918f31836af6b542609499d Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Thu, 22 Oct 2026 16:08:10 +0530 Subject: [PATCH 1/7] fix(cli): load the dev entry point's error handler from src, not dist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bin/dev.js runs the TypeScript sources under ts-node, but required its standalone failure handler from ../dist/core/failure. On a fresh clone with no build it crashed with "Cannot find module '../dist/core/failure'" before any command ran, and with a stale dist it used an outdated handler instead of the source being edited. It now requires ../src/core/failure, which ts-node compiles on the fly. Found in review of #248. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- bin/dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/dev.js b/bin/dev.js index 7d7de4cd..055f5701 100755 --- a/bin/dev.js +++ b/bin/dev.js @@ -1,7 +1,7 @@ #!/usr/bin/env node_modules/.bin/ts-node (async () => { const oclif = await import('@oclif/core'); - const { handleStandaloneFailure } = require('../dist/core/failure'); + const { handleStandaloneFailure } = require('../src/core/failure'); process.env.NODE_ENV = 'development'; oclif.settings.debug = true; From ee7e6d88aea810d13451e495564cb5514ce2abee Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Mon, 26 Oct 2026 13:59:34 +0530 Subject: [PATCH 2/7] fix(cli): resolve the dev entry point's error handler when it fails, so plain `node bin/dev` works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requiring ../src/core/failure at startup only worked under the ts-node shebang (./bin/dev.js). `node bin/dev` is plain Node: it ignores the shebang, and ts-node is registered only once oclif's development mode loads a TypeScript command, so the require crashed every command with "Cannot find module '../src/core/failure'". The handler is now resolved when a failure happens: the source handler when ts-node is registered, else the build's, else oclif's own handler. Checked under `node bin/dev` and ./bin/dev.js, each with and without a dist/: commands run, and an unknown command, an unknown flag and an API error each print one `error:` line with their exit code. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- bin/dev.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/bin/dev.js b/bin/dev.js index 055f5701..d4476116 100755 --- a/bin/dev.js +++ b/bin/dev.js @@ -1,13 +1,28 @@ #!/usr/bin/env node_modules/.bin/ts-node (async () => { const oclif = await import('@oclif/core'); - const { handleStandaloneFailure } = require('../src/core/failure'); process.env.NODE_ENV = 'development'; oclif.settings.debug = true; + // The source handler loads only once ts-node is registered: always under the ts-node shebang, but under plain + // `node bin/dev` only after oclif has loaded a TypeScript command. Fall back to the build, then to oclif. + function failureHandler() { + for (const path of ['../src/core/failure', '../dist/core/failure']) { + try { + return require(path).handleStandaloneFailure; + } catch (error) { + if (error.code !== 'MODULE_NOT_FOUND') { + throw error; + } + } + } + + return oclif.Errors.handle; + } + await oclif .run(process.argv.slice(2), __dirname) .then(() => oclif.flush()) - .catch(handleStandaloneFailure); + .catch((err) => failureHandler()(err)); })(); From 78f315240b572d9c7c72c5f1feef92f82508f816 Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Mon, 26 Oct 2026 14:07:45 +0530 Subject: [PATCH 3/7] chore(cli): drop the comment from the dev entry point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- bin/dev.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/dev.js b/bin/dev.js index d4476116..2ad69a49 100755 --- a/bin/dev.js +++ b/bin/dev.js @@ -5,8 +5,6 @@ process.env.NODE_ENV = 'development'; oclif.settings.debug = true; - // The source handler loads only once ts-node is registered: always under the ts-node shebang, but under plain - // `node bin/dev` only after oclif has loaded a TypeScript command. Fall back to the build, then to oclif. function failureHandler() { for (const path of ['../src/core/failure', '../dist/core/failure']) { try { From c46fd34291bc22e92922ed598aadaf20f7f47515 Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Mon, 26 Oct 2026 15:54:39 +0530 Subject: [PATCH 4/7] feat(prompts): show five options at a time, and stop the org and project pickers wrapping round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every arrow-key list and checklist (framework preset, response mode, Contentstack Authentication, project type, branch, the env:update and variables checklists) now shows at most five options and scrolls for the rest: PROMPT_PAGE_SIZE drops from 15 to 5. Those lists already passed loop: false, so the arrow stops at the last option. The organization and project pickers (search lists) wrapped from the last entry to the first and scrolled endlessly: inquirer-search-list's onDownKey/onUpKey wrap round, and its bundled inquirer 3 paginator repeats the list three times. LaunchSearchList now stops the arrow at both ends, defaults pageSize to PROMPT_PAGE_SIZE, and swaps the paginator for scrollWindow, a window that stops at both ends instead of repeating the list. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- AGENTS.md | 8 +- src/core/prompt.test.ts | 8 +- src/core/prompt.ts | 2 +- src/core/search-list.test.ts | 80 ++++++++++++++++++- src/core/search-list.ts | 33 +++++++- .../environment.update.prompt.test.ts | 2 +- 6 files changed, 123 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 339578d7..0e00b1d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -378,8 +378,8 @@ payload inline. on an arrow or a digit and ignores every letter, so text can never become the answer and nothing needs validating. Every choice prompt (`askOption`, and the checkbox `askChoices`) passes `loop: false`, so the arrow stops at the last entry instead of wrapping to the first, and a `pageSize` of the choice count -capped at `PROMPT_PAGE_SIZE` (15), so a short list shows whole and a 100-entry picker scrolls in a -fixed window. `askChoices` refuses an empty selection at the prompt, naming . Project type, branch, Framework preset, Response mode and Contentstack +capped at `PROMPT_PAGE_SIZE` (5), so a list of five or fewer shows whole and anything longer - the +eleven framework presets, the env:update checklist - scrolls five at a time in a fixed window. `askChoices` refuses an empty selection at the prompt, naming . Project type, branch, Framework preset, Response mode and Contentstack Authentication are all asked this way. Prefer it: the `❯` always shows what Enter will take, where a search box hides the list the moment the filter matches nothing. The branch picker is on this side despite fetching `PICKER_PAGE_SIZE` branches, because `noteTruncation` already prints the escape @@ -401,6 +401,10 @@ answers with an `UnlistedAnswer`. The wrapper installs `refuseUnlisted` over `th its constructor, so that answer never leaves the prompt: inquirer renders `"my-site" is not one of the options. Pick one from the list.` and asks again. A caller's own `validate` is wrapped, not replaced, and still sees every listed answer. +The same wrapper makes the pickers scroll like the lists: `PROMPT_PAGE_SIZE` rows at a time, and the +arrow stops at the first and last entry. Upstream, the up and down arrow handlers wrap round and the +bundled inquirer 3 paginator repeats the list three times to look endless, so the wrapper overrides both +arrow handlers and swaps the paginator for `scrollWindow`, a window that stops at both ends. Together these are the prompt-side twin of `oneOf` on the flag side (`--cs-auth yes` has always been exit 2); the two now agree, and no prompt module needs its own check. diff --git a/src/core/prompt.test.ts b/src/core/prompt.test.ts index 2fc3f9f2..311d3b49 100644 --- a/src/core/prompt.test.ts +++ b/src/core/prompt.test.ts @@ -86,9 +86,9 @@ describe('prompt helpers', () => { it.each([ [3, 3], - [15, 15], - [16, 15], - [100, 15], + [5, 5], + [6, 5], + [100, 5], ])('shows %i choices %i rows at a time, so a short list is shown whole and a long one scrolls', async (count, rows) => { const { asked, ux } = scriptedUx(['c0']); const choices = Array.from({ length: count }, (_, index) => ({ name: `C${index}`, value: `c${index}` })); @@ -96,7 +96,7 @@ describe('prompt helpers', () => { await askOption(ux, 'Pick one', choices); expect((asked[0] as { pageSize: number }).pageSize).toBe(rows); - expect(PROMPT_PAGE_SIZE).toBe(15); + expect(PROMPT_PAGE_SIZE).toBe(5); }); it('cancels rather than accepting nothing at a list of options', async () => { diff --git a/src/core/prompt.ts b/src/core/prompt.ts index b2d47b46..36513f9d 100644 --- a/src/core/prompt.ts +++ b/src/core/prompt.ts @@ -76,7 +76,7 @@ export async function askOptionalText( return text === '' ? undefined : text; } -export const PROMPT_PAGE_SIZE = 15; +export const PROMPT_PAGE_SIZE = 5; function scrolling(choices: Choice[]): { loop: false; pageSize: number } { return { loop: false, pageSize: Math.min(choices.length, PROMPT_PAGE_SIZE) }; diff --git a/src/core/search-list.test.ts b/src/core/search-list.test.ts index 502b5817..98ecb6ff 100644 --- a/src/core/search-list.test.ts +++ b/src/core/search-list.test.ts @@ -1,3 +1,4 @@ +import { PROMPT_PAGE_SIZE } from './prompt'; import type { SearchListClass } from './search-list'; import { UnlistedAnswer, @@ -5,6 +6,7 @@ import { launchSearchList, refuseUnlisted, registerSearchList, + scrollWindow, startingPointer, unlistedMessage, utilitiesLoader, @@ -14,8 +16,12 @@ const load = utilitiesLoader(); const SearchList = load('inquirer-search-list') as SearchListClass; interface Prompt { - opt: { validate: (value: unknown, answers?: unknown) => unknown }; + opt: { validate: (value: unknown, answers?: unknown) => unknown; pageSize?: number }; pointer: number; + paginator: { paginate: unknown }; + render(): void; + onDownKey(): void; + onUpKey(): void; rl: { line: string }; selected: unknown; filterChoices(): void; @@ -73,6 +79,56 @@ describe('launchSearchList', () => { expect(prompt.getCurrentValue()).toBe('Gatsby'); }); + it('stops at the last choice instead of wrapping round to the first', () => { + const prompt = open({ default: 'Other' }); + prompt.render = jest.fn(); + + prompt.onDownKey(); + + expect(prompt.pointer).toBe(2); + expect(prompt.render).toHaveBeenCalled(); + }); + + it('stops at the first choice instead of wrapping round to the last', () => { + const prompt = open({}); + prompt.render = jest.fn(); + + prompt.onUpKey(); + + expect(prompt.pointer).toBe(0); + }); + + it('still moves between choices inside the list', () => { + const prompt = open({}); + prompt.render = jest.fn(); + + prompt.onDownKey(); + prompt.onDownKey(); + prompt.onUpKey(); + + expect(prompt.pointer).toBe(1); + }); + + it('stops at the last match of what was typed', () => { + const prompt = typed(open({}), 'Oth'); + prompt.render = jest.fn(); + + prompt.onDownKey(); + + expect(prompt.pointer).toBe(0); + }); + + it('shows PROMPT_PAGE_SIZE choices at a time through a window that does not repeat the list', () => { + const prompt = open({}); + + expect(prompt.opt.pageSize).toBe(PROMPT_PAGE_SIZE); + expect(prompt.paginator.paginate).toBe(scrollWindow); + }); + + it('keeps a page size the question sets itself', () => { + expect(open({ pageSize: 3 }).opt.pageSize).toBe(3); + }); + it('submits the highlighted match of what was typed', () => { const prompt = typed(open({}), 'Oth'); @@ -162,6 +218,28 @@ describe('launchSearchList', () => { }); }); +describe('scrollWindow', () => { + const LINES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'].join('\n'); + const HINT = '\u001b[2m(Move up and down to reveal more choices)\u001b[22m'; + const shown = (active: number) => scrollWindow(LINES, active, 5).split('\n'); + + it('shows a list that fits the page whole, with no hint', () => { + expect(scrollWindow('a\nb\nc', 2, 5)).toBe('a\nb\nc'); + }); + + it('starts at the top of a longer list and says there are more', () => { + expect(shown(0)).toEqual(['a', 'b', 'c', 'd', 'e', HINT]); + }); + + it('keeps the highlight in the middle while scrolling down', () => { + expect(shown(4)).toEqual(['c', 'd', 'e', 'f', 'g', HINT]); + }); + + it('ends at the last choice and never shows the first one again after it', () => { + expect(shown(7)).toEqual(['d', 'e', 'f', 'g', 'h', HINT]); + }); +}); + describe('answerFor', () => { it('answers with the value of the choice whose name was typed in full', () => { expect(answerFor(CHOICES, 'NextJs')).toBe('NextJs'); diff --git a/src/core/search-list.ts b/src/core/search-list.ts index 912dcbb6..d44bdc18 100644 --- a/src/core/search-list.ts +++ b/src/core/search-list.ts @@ -1,5 +1,8 @@ import { createRequire } from 'node:module'; +import { PROMPT_PAGE_SIZE } from './prompt'; +import { styled } from './style'; + export interface SearchChoice { name?: string; value: unknown; @@ -8,11 +11,13 @@ export interface SearchChoice { export type Validator = (value: unknown, answers?: unknown) => unknown; export interface SearchListPrompt { - opt: { default?: unknown; validate: Validator }; + opt: { default?: unknown; validate: Validator; pageSize?: number }; rl: { line: string }; pointer: number; list: SearchChoice[]; filterList: SearchChoice[]; + paginator: { paginate(output: string, active: number, pageSize?: number): string }; + render(): void; getCurrentValue(line?: unknown): unknown; } @@ -51,12 +56,38 @@ export function startingPointer(list: SearchChoice[], initial: unknown): number return Math.max(index, 0); } +export function scrollWindow(output: string, active: number, pageSize = PROMPT_PAGE_SIZE): string { + const lines = output.split('\n'); + + if (lines.length <= pageSize) { + return output; + } + + const top = Math.min(Math.max(active - Math.floor(pageSize / 2), 0), lines.length - pageSize); + + return [...lines.slice(top, top + pageSize), styled('(Move up and down to reveal more choices)', 'dim', true)].join( + '\n', + ); +} + export function launchSearchList(SearchList: SearchListClass): SearchListClass { return class LaunchSearchList extends SearchList { constructor(...params: never[]) { super(...params); this.pointer = startingPointer(this.list, this.opt.default); this.opt.validate = refuseUnlisted(this.opt.validate); + this.opt.pageSize = this.opt.pageSize ?? PROMPT_PAGE_SIZE; + this.paginator = { paginate: scrollWindow }; + } + + onDownKey(): void { + this.pointer = Math.min(this.pointer + 1, Math.max(this.filterList.length - 1, 0)); + this.render(); + } + + onUpKey(): void { + this.pointer = Math.max(this.pointer - 1, 0); + this.render(); } getCurrentValue(line?: unknown): unknown { diff --git a/src/environments/environment.update.prompt.test.ts b/src/environments/environment.update.prompt.test.ts index 67167332..75911e12 100644 --- a/src/environments/environment.update.prompt.test.ts +++ b/src/environments/environment.update.prompt.test.ts @@ -77,7 +77,7 @@ describe('promptForEnvironmentUpdate', () => { { name: 'Password protection', value: 'passwordProtection' }, ], loop: false, - pageSize: 12, + pageSize: 5, validate: expect.any(Function), }); }); From 7f1122484af6d3271b31c66241346cb968c718ef Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Mon, 26 Oct 2026 15:55:35 +0530 Subject: [PATCH 5/7] chore: refresh the Talisman checksums for the prompt files the paging change touched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each file already had an entry; the checksums went stale when the files changed. The findings are existing content (askSecret, password-protection prompts), not credentials. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- .talismanrc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.talismanrc b/.talismanrc index 918dfb0f..3476800c 100644 --- a/.talismanrc +++ b/.talismanrc @@ -148,7 +148,7 @@ fileignoreconfig: ignore_detectors: - filename - filename: src/environments/environment.update.prompt.test.ts - checksum: b83aa8da125e6a45b455cf03c50fe82cc41890343be9be0c2ccf0a01d09bd160 + checksum: aba0798b045427e567582dc8ff446156985d1cebb73cbbabd65ec34b8d1251f1 ignore_detectors: - filename - filename: src/environments/environment.presenter.ts @@ -233,7 +233,7 @@ fileignoreconfig: - "a project name passed on argv to the project resolver" - "expect\\(Object.keys\\(DEPENDENCIES\\)\\).toEqual\\(\\['project', 'env'\\]\\)" - filename: AGENTS.md - checksum: 4766a4bf28eab95d90878b825979cef70c83a79a24fb9450a52d617ce0933f78 + checksum: 815d3195b0af017107f7a575c4dbb4f16dc09c460826361ed8842c529292cb65 allowed_patterns: - "Do not add an exemption to a guard to make a new file pass" - "passes its own error wording to `client.request\\(req, MESSAGES\\)`" @@ -319,9 +319,9 @@ fileignoreconfig: - filename: src/environments/environment.errors.ts checksum: 3e17db3577ba944633f97063d208bdec57c833d4b6c38e203796a9fea43baacc - filename: src/core/prompt.ts - checksum: 3285bdbccc31cba5b50eafe8ed8be4750af6339078a3eaee6df1e0630979aa2c + checksum: 392a98c458361eaf4e32d9e1bf253a34d1aca0566fd0b8c8d1bd78d1c6bc193c - filename: src/core/prompt.test.ts - checksum: 2deacd402e8d7f79458ee6d3917205c6dbe5cd7e6a98a2e5c49e37ac1a9d57c4 + checksum: 18a42e513d9d762a38c6d3161e3c2414f17eb7ed6e1e44fdcd2c3c4ddd446afc - filename: test/integration/environments-update.test.ts checksum: 4cd0106bf1881d373d23f063314627057d85cab0c43b793f013ffc2ca8859036 - filename: src/commands/launch/env/update.ts From 9687f5aa1bd61b4bbcf3417f05de09a549ac2d97 Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Mon, 26 Oct 2026 16:23:29 +0530 Subject: [PATCH 6/7] =?UTF-8?q?feat(prompts):=20say=20"(Use=20=E2=86=91/?= =?UTF-8?q?=E2=86=93=20to=20see=20more=20options)"=20under=20a=20list=20th?= =?UTF-8?q?at=20scrolls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every scrolling list, checklist and picker showed inquirer's "(Move up and down to reveal more choices)". The list and checkbox prompts are registered as LaunchPaginated subclasses that swap that hint for MORE_CHOICES_HINT, and scrollWindow (the org and project pickers) prints the same text. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- src/core/search-list.test.ts | 42 +++++++++++++++++++++++++++++++++--- src/core/search-list.ts | 29 +++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/core/search-list.test.ts b/src/core/search-list.test.ts index 98ecb6ff..daf2cbc9 100644 --- a/src/core/search-list.test.ts +++ b/src/core/search-list.test.ts @@ -1,8 +1,10 @@ import { PROMPT_PAGE_SIZE } from './prompt'; import type { SearchListClass } from './search-list'; import { + MORE_CHOICES_HINT, UnlistedAnswer, answerFor, + launchPaginated, launchSearchList, refuseUnlisted, registerSearchList, @@ -220,7 +222,7 @@ describe('launchSearchList', () => { describe('scrollWindow', () => { const LINES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'].join('\n'); - const HINT = '\u001b[2m(Move up and down to reveal more choices)\u001b[22m'; + const HINT = '\u001b[2m(Use ↑/↓ to see more options)\u001b[22m'; const shown = (active: number) => scrollWindow(LINES, active, 5).split('\n'); it('shows a list that fits the page whole, with no hint', () => { @@ -240,6 +242,34 @@ describe('scrollWindow', () => { }); }); +describe('launchPaginated', () => { + interface Paginated { + paginator: { paginate(output: string, active: number): string }; + } + + class Upstream { + paginator = { + paginate: (output: string) => `${output}\n\u001b[2m(Move up and down to reveal more choices)\u001b[22m`, + }; + } + + it('points at the up and down arrows instead of the upstream wording', () => { + const Prompt = launchPaginated(Upstream as never) as unknown as new () => Paginated; + + expect(new Prompt().paginator.paginate('a\nb', 0)).toBe(`a\nb\n\u001b[2m${MORE_CHOICES_HINT}\u001b[22m`); + expect(MORE_CHOICES_HINT).toBe('(Use ↑/↓ to see more options)'); + }); + + it('leaves a page without the hint untouched', () => { + class Short { + paginator = { paginate: (output: string) => output }; + } + const Prompt = launchPaginated(Short as never) as unknown as new () => Paginated; + + expect(new Prompt().paginator.paginate('a\nb', 0)).toBe('a\nb'); + }); +}); + describe('answerFor', () => { it('answers with the value of the choice whose name was typed in full', () => { expect(answerFor(CHOICES, 'NextJs')).toBe('NextJs'); @@ -302,8 +332,8 @@ describe('registerSearchList', () => { registerSearchList(fakeLoad); - expect(loaded).toEqual(['inquirer', 'inquirer-search-list']); - expect(registered).toHaveLength(1); + expect(loaded).toEqual(['inquirer', 'inquirer-search-list', 'inquirer/lib/prompts/list', 'inquirer/lib/prompts/checkbox']); + expect(registered.map(([name]) => name)).toEqual(['search-list', 'list', 'checkbox']); expect(registered[0][0]).toBe('search-list'); expect((registered[0][1] as SearchListClass).prototype).toBeInstanceOf(SearchList); expect(registered[0][1]).not.toBe(SearchList); @@ -315,5 +345,11 @@ describe('registerSearchList', () => { registerSearchList(); expect((inquirer.prompt.prompts['search-list'] as SearchListClass).prototype).toBeInstanceOf(SearchList); + for (const type of ['list', 'checkbox']) { + const Upstream = load(`inquirer/lib/prompts/${type}`) as SearchListClass; + + expect((inquirer.prompt.prompts[type] as SearchListClass).prototype).toBeInstanceOf(Upstream); + expect(inquirer.prompt.prompts[type]).not.toBe(Upstream); + } }); }); diff --git a/src/core/search-list.ts b/src/core/search-list.ts index d44bdc18..2d93cc2f 100644 --- a/src/core/search-list.ts +++ b/src/core/search-list.ts @@ -16,7 +16,7 @@ export interface SearchListPrompt { pointer: number; list: SearchChoice[]; filterList: SearchChoice[]; - paginator: { paginate(output: string, active: number, pageSize?: number): string }; + paginator: Paginator; render(): void; getCurrentValue(line?: unknown): unknown; } @@ -56,6 +56,27 @@ export function startingPointer(list: SearchChoice[], initial: unknown): number return Math.max(index, 0); } +export const MORE_CHOICES_HINT = '(Use ↑/↓ to see more options)'; + +const UPSTREAM_HINT = '(Move up and down to reveal more choices)'; + +interface Paginator { + paginate(output: string, active: number, pageSize?: number): string; +} + +export type PaginatedClass = new (...params: never[]) => { paginator: Paginator }; + +export function launchPaginated(Prompt: PaginatedClass): PaginatedClass { + return class LaunchPaginated extends Prompt { + constructor(...params: never[]) { + super(...params); + const paginate = this.paginator.paginate.bind(this.paginator); + this.paginator.paginate = (output, active, pageSize) => + paginate(output, active, pageSize).replace(UPSTREAM_HINT, MORE_CHOICES_HINT); + } + }; +} + export function scrollWindow(output: string, active: number, pageSize = PROMPT_PAGE_SIZE): string { const lines = output.split('\n'); @@ -65,7 +86,7 @@ export function scrollWindow(output: string, active: number, pageSize = PROMPT_P const top = Math.min(Math.max(active - Math.floor(pageSize / 2), 0), lines.length - pageSize); - return [...lines.slice(top, top + pageSize), styled('(Move up and down to reveal more choices)', 'dim', true)].join( + return [...lines.slice(top, top + pageSize), styled(MORE_CHOICES_HINT, 'dim', true)].join( '\n', ); } @@ -130,4 +151,8 @@ export function registerSearchList(load: ModuleLoader = utilitiesLoader()): void const inquirer = load('inquirer') as PromptRegistry; inquirer.registerPrompt('search-list', launchSearchList(load('inquirer-search-list') as SearchListClass)); + + for (const type of ['list', 'checkbox']) { + inquirer.registerPrompt(type, launchPaginated(load(`inquirer/lib/prompts/${type}`) as PaginatedClass)); + } } From abd143f8f43aa8a7a7cb372ea3256e63f43f8bd6 Mon Sep 17 00:00:00 2001 From: SakshiKoli-CS Date: Mon, 26 Oct 2026 18:11:29 +0530 Subject: [PATCH 7/7] feat(prompts): show seven options at a time instead of five MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROMPT_PAGE_SIZE goes from 5 to 7, so every arrow-key list, checklist and the org and project pickers show seven options before scrolling. The framework presets show seven of eleven, and the env:update checklist opens on Name through Output directory. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Harshi Shah Co-Authored-By: Claude --- .talismanrc | 8 ++++---- AGENTS.md | 4 ++-- src/core/prompt.test.ts | 8 ++++---- src/core/prompt.ts | 2 +- src/environments/environment.update.prompt.test.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.talismanrc b/.talismanrc index 8a9b32d8..711d546d 100644 --- a/.talismanrc +++ b/.talismanrc @@ -143,7 +143,7 @@ fileignoreconfig: ignore_detectors: - filename - filename: src/environments/environment.update.prompt.test.ts - checksum: aba0798b045427e567582dc8ff446156985d1cebb73cbbabd65ec34b8d1251f1 + checksum: 6653e245baac00cef348acfc0aa2f32d03a7ff63ca3c320ee56a0c6250775004 ignore_detectors: - filename - filename: src/environments/environment.presenter.ts @@ -228,7 +228,7 @@ fileignoreconfig: - "a project name passed on argv to the project resolver" - "expect\\(Object.keys\\(DEPENDENCIES\\)\\).toEqual\\(\\['project', 'env'\\]\\)" - filename: AGENTS.md - checksum: 0c3a7b0168b801761d2c0f9b60457b4dfda1c2455ffd1cbae9aaeaf024b97a20 + checksum: c3276952d1eb16ea53f89363c9a70b59a19efc21fe087ea89ee70946c4b7cfc7 allowed_patterns: - "Do not add an exemption to a guard to make a new file pass" - "passes its own error wording to `client.request\\(req, MESSAGES\\)`" @@ -314,9 +314,9 @@ fileignoreconfig: - filename: src/environments/environment.errors.ts checksum: 3e17db3577ba944633f97063d208bdec57c833d4b6c38e203796a9fea43baacc - filename: src/core/prompt.ts - checksum: 392a98c458361eaf4e32d9e1bf253a34d1aca0566fd0b8c8d1bd78d1c6bc193c + checksum: e9154ec91c36f7a8e8c2f91ec13fa554cfbf5fe3b8506db44fb7c68bdef8efa8 - filename: src/core/prompt.test.ts - checksum: 18a42e513d9d762a38c6d3161e3c2414f17eb7ed6e1e44fdcd2c3c4ddd446afc + checksum: 7be5497acf4cee870667751d44aa230e309784df76dc9e95d5ec2c4e6e6a57e0 - filename: test/integration/environments-update.test.ts checksum: 4cd0106bf1881d373d23f063314627057d85cab0c43b793f013ffc2ca8859036 - filename: src/commands/launch/env/update.ts diff --git a/AGENTS.md b/AGENTS.md index 41232c34..d6988291 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -384,8 +384,8 @@ payload inline. on an arrow or a digit and ignores every letter, so text can never become the answer and nothing needs validating. Every choice prompt (`askOption`, and the checkbox `askChoices`) passes `loop: false`, so the arrow stops at the last entry instead of wrapping to the first, and a `pageSize` of the choice count -capped at `PROMPT_PAGE_SIZE` (5), so a list of five or fewer shows whole and anything longer - the -eleven framework presets, the env:update checklist - scrolls five at a time in a fixed window. `askChoices` refuses an empty selection at the prompt, naming . Project type, branch, Framework preset, Response mode and Contentstack +capped at `PROMPT_PAGE_SIZE` (7), so a list of seven or fewer shows whole and anything longer - the +eleven framework presets, the env:update checklist - scrolls seven at a time in a fixed window. `askChoices` refuses an empty selection at the prompt, naming . Project type, branch, Framework preset, Response mode and Contentstack Authentication are all asked this way. Prefer it: the `❯` always shows what Enter will take, where a search box hides the list the moment the filter matches nothing. The branch picker is on this side despite fetching `PICKER_PAGE_SIZE` branches, because `noteTruncation` already prints the escape diff --git a/src/core/prompt.test.ts b/src/core/prompt.test.ts index 311d3b49..69bca1f7 100644 --- a/src/core/prompt.test.ts +++ b/src/core/prompt.test.ts @@ -86,9 +86,9 @@ describe('prompt helpers', () => { it.each([ [3, 3], - [5, 5], - [6, 5], - [100, 5], + [7, 7], + [8, 7], + [100, 7], ])('shows %i choices %i rows at a time, so a short list is shown whole and a long one scrolls', async (count, rows) => { const { asked, ux } = scriptedUx(['c0']); const choices = Array.from({ length: count }, (_, index) => ({ name: `C${index}`, value: `c${index}` })); @@ -96,7 +96,7 @@ describe('prompt helpers', () => { await askOption(ux, 'Pick one', choices); expect((asked[0] as { pageSize: number }).pageSize).toBe(rows); - expect(PROMPT_PAGE_SIZE).toBe(5); + expect(PROMPT_PAGE_SIZE).toBe(7); }); it('cancels rather than accepting nothing at a list of options', async () => { diff --git a/src/core/prompt.ts b/src/core/prompt.ts index 36513f9d..f6cac47a 100644 --- a/src/core/prompt.ts +++ b/src/core/prompt.ts @@ -76,7 +76,7 @@ export async function askOptionalText( return text === '' ? undefined : text; } -export const PROMPT_PAGE_SIZE = 5; +export const PROMPT_PAGE_SIZE = 7; function scrolling(choices: Choice[]): { loop: false; pageSize: number } { return { loop: false, pageSize: Math.min(choices.length, PROMPT_PAGE_SIZE) }; diff --git a/src/environments/environment.update.prompt.test.ts b/src/environments/environment.update.prompt.test.ts index 75911e12..311488f8 100644 --- a/src/environments/environment.update.prompt.test.ts +++ b/src/environments/environment.update.prompt.test.ts @@ -77,7 +77,7 @@ describe('promptForEnvironmentUpdate', () => { { name: 'Password protection', value: 'passwordProtection' }, ], loop: false, - pageSize: 5, + pageSize: 7, validate: expect.any(Function), }); });