progressをわかりやすくする

This commit is contained in:
nrslib 2026-02-10 21:44:42 +09:00
parent 79ee353990
commit 3fa99ae0f7
10 changed files with 106 additions and 52 deletions

View File

@ -18,6 +18,7 @@ vi.mock('../shared/ui/index.js', () => ({
info: vi.fn(),
blankLine: vi.fn(),
error: vi.fn(),
withProgress: vi.fn(async (_start, _done, operation) => operation()),
}));
vi.mock('../shared/utils/index.js', async (importOriginal) => ({

View File

@ -11,6 +11,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../shared/ui/index.js', () => ({
info: vi.fn(),
error: vi.fn(),
withProgress: vi.fn(async (_start, _done, operation) => operation()),
}));
vi.mock('../shared/utils/index.js', async (importOriginal) => ({

View File

@ -28,14 +28,23 @@ vi.mock('../infra/task/summarize.js', () => ({
summarizeTaskName: vi.fn(),
}));
vi.mock('../shared/ui/index.js', () => ({
info: vi.fn(),
error: vi.fn(),
success: vi.fn(),
header: vi.fn(),
status: vi.fn(),
setLogLevel: vi.fn(),
}));
vi.mock('../shared/ui/index.js', () => {
const info = vi.fn();
return {
info,
error: vi.fn(),
success: vi.fn(),
header: vi.fn(),
status: vi.fn(),
setLogLevel: vi.fn(),
withProgress: vi.fn(async (start, done, operation) => {
info(start);
const result = await operation();
info(typeof done === 'function' ? done(result) : done);
return result;
}),
};
});
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),

View File

@ -40,14 +40,23 @@ vi.mock('../infra/task/summarize.js', async (importOriginal) => ({
summarizeTaskName: vi.fn(),
}));
vi.mock('../shared/ui/index.js', () => ({
header: vi.fn(),
info: vi.fn(),
error: vi.fn(),
success: vi.fn(),
status: vi.fn(),
blankLine: vi.fn(),
}));
vi.mock('../shared/ui/index.js', () => {
const info = vi.fn();
return {
header: vi.fn(),
info,
error: vi.fn(),
success: vi.fn(),
status: vi.fn(),
blankLine: vi.fn(),
withProgress: vi.fn(async (start, done, operation) => {
info(start);
const result = await operation();
info(typeof done === 'function' ? done(result) : done);
return result;
}),
};
});
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),

View File

@ -5,7 +5,7 @@
* pipeline mode, or interactive mode.
*/
import { info, error } from '../../shared/ui/index.js';
import { info, error, withProgress } from '../../shared/ui/index.js';
import { getErrorMessage } from '../../shared/utils/index.js';
import { getLabel } from '../../shared/i18n/index.js';
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
@ -35,23 +35,24 @@ import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from '
* Returns resolved issues and the formatted task text for interactive mode.
* Throws on gh CLI unavailability or fetch failure.
*/
function resolveIssueInput(
async function resolveIssueInput(
issueOption: number | undefined,
task: string | undefined,
): { issues: GitHubIssue[]; initialInput: string } | null {
): Promise<{ issues: GitHubIssue[]; initialInput: string } | null> {
if (issueOption) {
info('Fetching GitHub Issue...');
const ghStatus = checkGhCli();
if (!ghStatus.available) {
throw new Error(ghStatus.error);
}
const issue = fetchIssue(issueOption);
info(`GitHub Issue fetched: #${issue.number} ${issue.title}`);
const issue = await withProgress(
'Fetching GitHub Issue...',
(fetchedIssue) => `GitHub Issue fetched: #${fetchedIssue.number} ${fetchedIssue.title}`,
async () => fetchIssue(issueOption),
);
return { issues: [issue], initialInput: formatIssueAsTask(issue) };
}
if (task && isDirectTask(task)) {
info('Fetching GitHub Issue...');
const ghStatus = checkGhCli();
if (!ghStatus.available) {
throw new Error(ghStatus.error);
@ -61,8 +62,11 @@ function resolveIssueInput(
if (issueNumbers.length === 0) {
throw new Error(`Invalid issue reference: ${task}`);
}
const issues = issueNumbers.map((n) => fetchIssue(n));
info(`GitHub Issues fetched: ${issues.map((issue) => `#${issue.number}`).join(', ')}`);
const issues = await withProgress(
'Fetching GitHub Issue...',
(fetchedIssues) => `GitHub Issues fetched: ${fetchedIssues.map((issue) => `#${issue.number}`).join(', ')}`,
async () => issueNumbers.map((n) => fetchIssue(n)),
);
return { issues, initialInput: issues.map(formatIssueAsTask).join('\n\n---\n\n') };
}
@ -118,7 +122,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
let initialInput: string | undefined = task;
try {
const issueResult = resolveIssueInput(opts.issue as number | undefined, task);
const issueResult = await resolveIssueInput(opts.issue as number | undefined, task);
if (issueResult) {
selectOptions.issues = issueResult.issues;
initialInput = issueResult.initialInput;

View File

@ -7,7 +7,7 @@
import * as path from 'node:path';
import * as fs from 'node:fs';
import { promptInput, confirm } from '../../../shared/prompt/index.js';
import { success, info, error } from '../../../shared/ui/index.js';
import { success, info, error, withProgress } from '../../../shared/ui/index.js';
import { TaskRunner, type TaskFileData } from '../../../infra/task/index.js';
import { determinePiece } from '../execute/selectAndExecute.js';
import { createLogger, getErrorMessage, generateReportDir } from '../../../shared/utils/index.js';
@ -177,13 +177,16 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
if (isIssueReference(trimmedTask)) {
// Issue reference: fetch issue and use directly as task content
info('Fetching GitHub Issue...');
try {
taskContent = resolveIssueTask(trimmedTask);
const numbers = parseIssueNumbers([trimmedTask]);
const primaryIssueNumber = numbers[0];
taskContent = await withProgress(
'Fetching GitHub Issue...',
primaryIssueNumber ? `GitHub Issue fetched: #${primaryIssueNumber}` : 'GitHub Issue fetched',
async () => resolveIssueTask(trimmedTask),
);
if (numbers.length > 0) {
issueNumber = numbers[0];
info(`GitHub Issue fetched: #${issueNumber}`);
}
} catch (e) {
const msg = getErrorMessage(e);

View File

@ -4,7 +4,7 @@
import { loadGlobalConfig } from '../../../infra/config/index.js';
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
import { info } from '../../../shared/ui/index.js';
import { info, withProgress } from '../../../shared/ui/index.js';
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
export interface ResolvedTaskExecution {
@ -60,23 +60,27 @@ export async function resolveTaskExecution(
if (data.worktree) {
throwIfAborted(abortSignal);
baseBranch = getCurrentBranch(defaultCwd);
info('Generating branch name...');
const taskSlug = await summarizeTaskName(task.content, { cwd: defaultCwd });
info(`Branch name generated: ${taskSlug}`);
const taskSlug = await withProgress(
'Generating branch name...',
(slug) => `Branch name generated: ${slug}`,
() => summarizeTaskName(task.content, { cwd: defaultCwd }),
);
throwIfAborted(abortSignal);
info('Creating clone...');
const result = createSharedClone(defaultCwd, {
worktree: data.worktree,
branch: data.branch,
taskSlug,
issueNumber: data.issue,
});
const result = await withProgress(
'Creating clone...',
(cloneResult) => `Clone created: ${cloneResult.path} (branch: ${cloneResult.branch})`,
async () => createSharedClone(defaultCwd, {
worktree: data.worktree,
branch: data.branch,
taskSlug,
issueNumber: data.issue,
}),
);
throwIfAborted(abortSignal);
execCwd = result.path;
branch = result.branch;
isWorktree = true;
info(`Clone created: ${result.path} (branch: ${result.branch})`);
}
const execPiece = data.piece || defaultPiece;

View File

@ -19,7 +19,7 @@ import {
import { confirm } from '../../../shared/prompt/index.js';
import { createSharedClone, autoCommitAndPush, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
import { info, error, success } from '../../../shared/ui/index.js';
import { info, error, success, withProgress } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/index.js';
import { createPullRequest, buildPrBody, pushBranch } from '../../../infra/github/index.js';
import { executeTask } from './taskExecution.js';
@ -113,16 +113,20 @@ export async function confirmAndCreateWorktree(
const baseBranch = getCurrentBranch(cwd);
info('Generating branch name...');
const taskSlug = await summarizeTaskName(task, { cwd });
info(`Branch name generated: ${taskSlug}`);
const taskSlug = await withProgress(
'Generating branch name...',
(slug) => `Branch name generated: ${slug}`,
() => summarizeTaskName(task, { cwd }),
);
info('Creating clone...');
const result = createSharedClone(cwd, {
worktree: true,
taskSlug,
});
info(`Clone created: ${result.path} (branch: ${result.branch})`);
const result = await withProgress(
'Creating clone...',
(cloneResult) => `Clone created: ${cloneResult.path} (branch: ${cloneResult.branch})`,
async () => createSharedClone(cwd, {
worktree: true,
taskSlug,
}),
);
return { execCwd: result.path, isWorktree: true, branch: result.branch, baseBranch };
}

17
src/shared/ui/Progress.ts Normal file
View File

@ -0,0 +1,17 @@
import { info } from './LogManager.js';
export type ProgressCompletionMessage<T> = string | ((result: T) => string);
export async function withProgress<T>(
startMessage: string,
completionMessage: ProgressCompletionMessage<T>,
operation: () => Promise<T>,
): Promise<T> {
info(startMessage);
const result = await operation();
const message = typeof completionMessage === 'function'
? completionMessage(result)
: completionMessage;
info(message);
return result;
}

View File

@ -31,3 +31,5 @@ export { Spinner } from './Spinner.js';
export { StreamDisplay, type ProgressInfo } from './StreamDisplay.js';
export { TaskPrefixWriter } from './TaskPrefixWriter.js';
export { withProgress, type ProgressCompletionMessage } from './Progress.js';