takt: task-1771426242274 (#311)
This commit is contained in:
parent
43f6fa6ade
commit
340996c57e
@ -15,7 +15,6 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
confirm: vi.fn(() => true),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
@ -51,7 +50,6 @@ vi.mock('../features/pipeline/index.js', () => ({
|
|||||||
vi.mock('../features/interactive/index.js', () => ({
|
vi.mock('../features/interactive/index.js', () => ({
|
||||||
interactiveMode: vi.fn(),
|
interactiveMode: vi.fn(),
|
||||||
selectInteractiveMode: vi.fn(() => 'assistant'),
|
selectInteractiveMode: vi.fn(() => 'assistant'),
|
||||||
selectRecentSession: vi.fn(() => null),
|
|
||||||
passthroughMode: vi.fn(),
|
passthroughMode: vi.fn(),
|
||||||
quietMode: vi.fn(),
|
quietMode: vi.fn(),
|
||||||
personaMode: vi.fn(),
|
personaMode: vi.fn(),
|
||||||
@ -78,6 +76,7 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
||||||
resolveConfigValue: vi.fn((_: string, key: string) => (key === 'piece' ? 'default' : false)),
|
resolveConfigValue: vi.fn((_: string, key: string) => (key === 'piece' ? 'default' : false)),
|
||||||
resolveConfigValues: vi.fn(() => ({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' })),
|
resolveConfigValues: vi.fn(() => ({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' })),
|
||||||
|
loadPersonaSessions: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/constants.js', () => ({
|
vi.mock('../shared/constants.js', () => ({
|
||||||
@ -107,11 +106,11 @@ vi.mock('../app/cli/helpers.js', () => ({
|
|||||||
|
|
||||||
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
||||||
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
||||||
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
import { resolveConfigValues } from '../infra/config/index.js';
|
import { resolveConfigValues, loadPersonaSessions } from '../infra/config/index.js';
|
||||||
import { confirm } from '../shared/prompt/index.js';
|
|
||||||
import { isDirectTask } from '../app/cli/helpers.js';
|
import { isDirectTask } from '../app/cli/helpers.js';
|
||||||
import { executeDefaultAction } from '../app/cli/routing.js';
|
import { executeDefaultAction } from '../app/cli/routing.js';
|
||||||
|
import { info } from '../shared/ui/index.js';
|
||||||
import type { GitHubIssue } from '../infra/github/types.js';
|
import type { GitHubIssue } from '../infra/github/types.js';
|
||||||
|
|
||||||
const mockCheckGhCli = vi.mocked(checkGhCli);
|
const mockCheckGhCli = vi.mocked(checkGhCli);
|
||||||
@ -123,10 +122,10 @@ const mockDeterminePiece = vi.mocked(determinePiece);
|
|||||||
const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
|
const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
|
||||||
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
||||||
const mockInteractiveMode = vi.mocked(interactiveMode);
|
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||||
const mockSelectRecentSession = vi.mocked(selectRecentSession);
|
const mockLoadPersonaSessions = vi.mocked(loadPersonaSessions);
|
||||||
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
|
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
|
||||||
const mockConfirm = vi.mocked(confirm);
|
|
||||||
const mockIsDirectTask = vi.mocked(isDirectTask);
|
const mockIsDirectTask = vi.mocked(isDirectTask);
|
||||||
|
const mockInfo = vi.mocked(info);
|
||||||
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
||||||
|
|
||||||
function createMockIssue(number: number): GitHubIssue {
|
function createMockIssue(number: number): GitHubIssue {
|
||||||
@ -148,7 +147,6 @@ beforeEach(() => {
|
|||||||
// Default setup
|
// Default setup
|
||||||
mockDeterminePiece.mockResolvedValue('default');
|
mockDeterminePiece.mockResolvedValue('default');
|
||||||
mockInteractiveMode.mockResolvedValue({ action: 'execute', task: 'summarized task' });
|
mockInteractiveMode.mockResolvedValue({ action: 'execute', task: 'summarized task' });
|
||||||
mockConfirm.mockResolvedValue(true);
|
|
||||||
mockIsDirectTask.mockReturnValue(false);
|
mockIsDirectTask.mockReturnValue(false);
|
||||||
mockParseIssueNumbers.mockReturnValue([]);
|
mockParseIssueNumbers.mockReturnValue([]);
|
||||||
mockTaskRunnerListAllTaskItems.mockReturnValue([]);
|
mockTaskRunnerListAllTaskItems.mockReturnValue([]);
|
||||||
@ -481,41 +479,43 @@ describe('Issue resolution in routing', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('session selection with provider=claude', () => {
|
describe('--continue option', () => {
|
||||||
it('should pass selected session ID to interactiveMode when provider is claude', async () => {
|
it('should load saved session and pass to interactiveMode when --continue is specified', async () => {
|
||||||
// Given
|
// Given
|
||||||
|
mockOpts.continue = true;
|
||||||
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockLoadPersonaSessions.mockReturnValue({ interactive: 'saved-session-123' });
|
||||||
mockSelectRecentSession.mockResolvedValue('session-xyz');
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|
||||||
// Then: selectRecentSession should be called
|
// Then: loadPersonaSessions should be called with provider
|
||||||
expect(mockSelectRecentSession).toHaveBeenCalledWith('/test/cwd', 'en');
|
expect(mockLoadPersonaSessions).toHaveBeenCalledWith('/test/cwd', 'claude');
|
||||||
|
|
||||||
// Then: interactiveMode should receive the session ID as 4th argument
|
// Then: interactiveMode should receive the saved session ID
|
||||||
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
'/test/cwd',
|
'/test/cwd',
|
||||||
undefined,
|
undefined,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
'session-xyz',
|
'saved-session-123',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockConfirm).toHaveBeenCalledWith('Choose a previous session?', false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call selectRecentSession when user selects no in confirmation', async () => {
|
it('should show message and start new session when --continue has no saved session', async () => {
|
||||||
// Given
|
// Given
|
||||||
|
mockOpts.continue = true;
|
||||||
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
||||||
mockConfirm.mockResolvedValue(false);
|
mockLoadPersonaSessions.mockReturnValue({});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|
||||||
// Then
|
// Then: info message about no session
|
||||||
expect(mockConfirm).toHaveBeenCalledWith('Choose a previous session?', false);
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
expect(mockSelectRecentSession).not.toHaveBeenCalled();
|
'No previous assistant session found. Starting a new session.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then: interactiveMode should be called with undefined session ID
|
||||||
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
'/test/cwd',
|
'/test/cwd',
|
||||||
undefined,
|
undefined,
|
||||||
@ -524,15 +524,12 @@ describe('Issue resolution in routing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call selectRecentSession when provider is not claude', async () => {
|
it('should not load persona sessions when --continue is not specified', async () => {
|
||||||
// Given
|
|
||||||
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'openai' });
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|
||||||
// Then: selectRecentSession should NOT be called
|
// Then: loadPersonaSessions should NOT be called
|
||||||
expect(mockSelectRecentSession).not.toHaveBeenCalled();
|
expect(mockLoadPersonaSessions).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// Then: interactiveMode should be called with undefined session ID
|
// Then: interactiveMode should be called with undefined session ID
|
||||||
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
@ -544,14 +541,11 @@ describe('Issue resolution in routing', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('run session reference', () => {
|
describe('default assistant mode (no --continue)', () => {
|
||||||
it('should not prompt run session reference in default interactive flow', async () => {
|
it('should start new session without loading saved sessions', async () => {
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|
||||||
expect(mockConfirm).not.toHaveBeenCalledWith(
|
expect(mockLoadPersonaSessions).not.toHaveBeenCalled();
|
||||||
"Reference a previous run's results?",
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
'/test/cwd',
|
'/test/cwd',
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
215
src/__tests__/conversationLoop-resume.test.ts
Normal file
215
src/__tests__/conversationLoop-resume.test.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Tests for /resume command and initializeSession changes.
|
||||||
|
*
|
||||||
|
* Verifies:
|
||||||
|
* - initializeSession returns sessionId: undefined (no implicit auto-load)
|
||||||
|
* - /resume command calls selectRecentSession and updates sessionId
|
||||||
|
* - /resume with cancel does not change sessionId
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
setupRawStdin,
|
||||||
|
restoreStdin,
|
||||||
|
toRawInputs,
|
||||||
|
createMockProvider,
|
||||||
|
createScenarioProvider,
|
||||||
|
type MockProviderCapture,
|
||||||
|
} from './helpers/stdinSimulator.js';
|
||||||
|
|
||||||
|
// --- Infrastructure mocks ---
|
||||||
|
|
||||||
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||||
|
loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })),
|
||||||
|
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/providers/index.js', () => ({
|
||||||
|
getProvider: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
createLogger: () => ({ info: vi.fn(), debug: vi.fn(), error: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/context.js', () => ({
|
||||||
|
isQuietMode: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
loadPersonaSessions: vi.fn(() => ({})),
|
||||||
|
updatePersonaSession: vi.fn(),
|
||||||
|
getProjectConfigDir: vi.fn(() => '/tmp'),
|
||||||
|
loadSessionState: vi.fn(() => null),
|
||||||
|
clearSessionState: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
blankLine: vi.fn(),
|
||||||
|
StreamDisplay: vi.fn().mockImplementation(() => ({
|
||||||
|
createHandler: vi.fn(() => vi.fn()),
|
||||||
|
flush: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
|
selectOption: vi.fn().mockResolvedValue('execute'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockSelectRecentSession = vi.fn<(cwd: string, lang: 'en' | 'ja') => Promise<string | null>>();
|
||||||
|
|
||||||
|
vi.mock('../features/interactive/sessionSelector.js', () => ({
|
||||||
|
selectRecentSession: (...args: [string, 'en' | 'ja']) => mockSelectRecentSession(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/i18n/index.js', () => ({
|
||||||
|
getLabel: vi.fn((_key: string, _lang: string) => 'Mock label'),
|
||||||
|
getLabelObject: vi.fn(() => ({
|
||||||
|
intro: 'Intro',
|
||||||
|
resume: 'Resume',
|
||||||
|
noConversation: 'No conversation',
|
||||||
|
summarizeFailed: 'Summarize failed',
|
||||||
|
continuePrompt: 'Continue?',
|
||||||
|
proposed: 'Proposed:',
|
||||||
|
actionPrompt: 'What next?',
|
||||||
|
playNoTask: 'No task for /play',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
actions: { execute: 'Execute', saveTask: 'Save', continue: 'Continue' },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
|
import { getProvider } from '../infra/providers/index.js';
|
||||||
|
import { selectOption } from '../shared/prompt/index.js';
|
||||||
|
import { info as logInfo } from '../shared/ui/index.js';
|
||||||
|
import { initializeSession, runConversationLoop, type SessionContext } from '../features/interactive/conversationLoop.js';
|
||||||
|
|
||||||
|
const mockGetProvider = vi.mocked(getProvider);
|
||||||
|
const mockSelectOption = vi.mocked(selectOption);
|
||||||
|
const mockLogInfo = vi.mocked(logInfo);
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
function setupProvider(responses: string[]): MockProviderCapture {
|
||||||
|
const { provider, capture } = createMockProvider(responses);
|
||||||
|
mockGetProvider.mockReturnValue(provider);
|
||||||
|
return capture;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionContext(overrides: Partial<SessionContext> = {}): SessionContext {
|
||||||
|
const { provider } = createMockProvider([]);
|
||||||
|
mockGetProvider.mockReturnValue(provider);
|
||||||
|
return {
|
||||||
|
provider: provider as SessionContext['provider'],
|
||||||
|
providerType: 'mock' as SessionContext['providerType'],
|
||||||
|
model: undefined,
|
||||||
|
lang: 'en',
|
||||||
|
personaName: 'interactive',
|
||||||
|
sessionId: undefined,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultStrategy = {
|
||||||
|
systemPrompt: 'test system prompt',
|
||||||
|
allowedTools: ['Read'],
|
||||||
|
transformPrompt: (msg: string) => msg,
|
||||||
|
introMessage: 'Test intro',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockSelectOption.mockResolvedValue('execute');
|
||||||
|
mockSelectRecentSession.mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restoreStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// initializeSession: no implicit session auto-load
|
||||||
|
// =================================================================
|
||||||
|
describe('initializeSession', () => {
|
||||||
|
it('should return sessionId as undefined (no implicit auto-load)', () => {
|
||||||
|
const ctx = initializeSession('/test/cwd', 'interactive');
|
||||||
|
|
||||||
|
expect(ctx.sessionId).toBeUndefined();
|
||||||
|
expect(ctx.personaName).toBe('interactive');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// /resume command
|
||||||
|
// =================================================================
|
||||||
|
describe('/resume command', () => {
|
||||||
|
it('should call selectRecentSession and update sessionId when session selected', async () => {
|
||||||
|
// Given: /resume → select session → /cancel
|
||||||
|
setupRawStdin(toRawInputs(['/resume', '/cancel']));
|
||||||
|
setupProvider([]);
|
||||||
|
mockSelectRecentSession.mockResolvedValue('selected-session-abc');
|
||||||
|
|
||||||
|
const ctx = createSessionContext();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await runConversationLoop('/test', ctx, defaultStrategy, undefined, undefined);
|
||||||
|
|
||||||
|
// Then: selectRecentSession called
|
||||||
|
expect(mockSelectRecentSession).toHaveBeenCalledWith('/test', 'en');
|
||||||
|
|
||||||
|
// Then: info about loaded session displayed
|
||||||
|
expect(mockLogInfo).toHaveBeenCalledWith('Mock label');
|
||||||
|
|
||||||
|
// Then: cancelled at the end
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change sessionId when user cancels session selection', async () => {
|
||||||
|
// Given: /resume → cancel selection → /cancel
|
||||||
|
setupRawStdin(toRawInputs(['/resume', '/cancel']));
|
||||||
|
setupProvider([]);
|
||||||
|
mockSelectRecentSession.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const ctx = createSessionContext();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await runConversationLoop('/test', ctx, defaultStrategy, undefined, undefined);
|
||||||
|
|
||||||
|
// Then: selectRecentSession called but returned null
|
||||||
|
expect(mockSelectRecentSession).toHaveBeenCalledWith('/test', 'en');
|
||||||
|
|
||||||
|
// Then: cancelled
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use resumed session for subsequent AI calls', async () => {
|
||||||
|
// Given: /resume → select session → send message → /cancel
|
||||||
|
setupRawStdin(toRawInputs(['/resume', 'hello world', '/cancel']));
|
||||||
|
mockSelectRecentSession.mockResolvedValue('resumed-session-xyz');
|
||||||
|
|
||||||
|
const { provider, capture } = createScenarioProvider([
|
||||||
|
{ content: 'AI response' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ctx: SessionContext = {
|
||||||
|
provider: provider as SessionContext['provider'],
|
||||||
|
providerType: 'mock' as SessionContext['providerType'],
|
||||||
|
model: undefined,
|
||||||
|
lang: 'en',
|
||||||
|
personaName: 'interactive',
|
||||||
|
sessionId: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await runConversationLoop('/test', ctx, defaultStrategy, undefined, undefined);
|
||||||
|
|
||||||
|
// Then: AI call should use the resumed session ID
|
||||||
|
expect(capture.sessionIds[0]).toBe('resumed-session-xyz');
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -51,7 +51,8 @@ program
|
|||||||
.option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation')
|
.option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation')
|
||||||
.option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)')
|
.option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)')
|
||||||
.option('--create-worktree <yes|no>', 'Skip the worktree prompt by explicitly specifying yes or no')
|
.option('--create-worktree <yes|no>', 'Skip the worktree prompt by explicitly specifying yes or no')
|
||||||
.option('-q, --quiet', 'Minimal output mode: suppress AI output (for CI)');
|
.option('-q, --quiet', 'Minimal output mode: suppress AI output (for CI)')
|
||||||
|
.option('-c, --continue', 'Continue from the last assistant session');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run pre-action hook: common initialization for all commands.
|
* Run pre-action hook: common initialization for all commands.
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { info, error as logError, withProgress } from '../../shared/ui/index.js';
|
import { info, error as logError, withProgress } from '../../shared/ui/index.js';
|
||||||
import { confirm } from '../../shared/prompt/index.js';
|
|
||||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getLabel } from '../../shared/i18n/index.js';
|
import { getLabel } from '../../shared/i18n/index.js';
|
||||||
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
||||||
@ -15,7 +14,6 @@ import { executePipeline } from '../../features/pipeline/index.js';
|
|||||||
import {
|
import {
|
||||||
interactiveMode,
|
interactiveMode,
|
||||||
selectInteractiveMode,
|
selectInteractiveMode,
|
||||||
selectRecentSession,
|
|
||||||
passthroughMode,
|
passthroughMode,
|
||||||
quietMode,
|
quietMode,
|
||||||
personaMode,
|
personaMode,
|
||||||
@ -23,7 +21,7 @@ import {
|
|||||||
dispatchConversationAction,
|
dispatchConversationAction,
|
||||||
type InteractiveModeResult,
|
type InteractiveModeResult,
|
||||||
} from '../../features/interactive/index.js';
|
} from '../../features/interactive/index.js';
|
||||||
import { getPieceDescription, resolveConfigValue, resolveConfigValues } from '../../infra/config/index.js';
|
import { getPieceDescription, resolveConfigValue, resolveConfigValues, loadPersonaSessions } from '../../infra/config/index.js';
|
||||||
import { program, resolvedCwd, pipelineMode } from './program.js';
|
import { program, resolvedCwd, pipelineMode } from './program.js';
|
||||||
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
||||||
import { loadTaskHistory } from './taskHistory.js';
|
import { loadTaskHistory } from './taskHistory.js';
|
||||||
@ -172,17 +170,14 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
switch (selectedMode) {
|
switch (selectedMode) {
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
let selectedSessionId: string | undefined;
|
let selectedSessionId: string | undefined;
|
||||||
const provider = globalConfig.provider;
|
if (opts.continue === true) {
|
||||||
if (provider === 'claude') {
|
const providerType = globalConfig.provider;
|
||||||
const shouldSelectSession = await confirm(
|
const savedSessions = loadPersonaSessions(resolvedCwd, providerType);
|
||||||
getLabel('interactive.sessionSelector.confirm', lang),
|
const savedSessionId = savedSessions['interactive'];
|
||||||
false,
|
if (savedSessionId) {
|
||||||
);
|
selectedSessionId = savedSessionId;
|
||||||
if (shouldSelectSession) {
|
} else {
|
||||||
const sessionId = await selectRecentSession(resolvedCwd, lang);
|
info(getLabel('interactive.continueNoSession', lang));
|
||||||
if (sessionId) {
|
|
||||||
selectedSessionId = sessionId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = await interactiveMode(resolvedCwd, initialInput, pieceContext, selectedSessionId);
|
result = await interactiveMode(resolvedCwd, initialInput, pieceContext, selectedSessionId);
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
|||||||
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
||||||
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
||||||
import { readMultilineInput } from './lineEditor.js';
|
import { readMultilineInput } from './lineEditor.js';
|
||||||
|
import { selectRecentSession } from './sessionSelector.js';
|
||||||
import { EXIT_SIGINT } from '../../shared/exitCodes.js';
|
import { EXIT_SIGINT } from '../../shared/exitCodes.js';
|
||||||
import {
|
import {
|
||||||
type PieceContext,
|
type PieceContext,
|
||||||
@ -55,7 +56,11 @@ export interface SessionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize provider, session, and language for interactive conversation.
|
* Initialize provider and language for interactive conversation.
|
||||||
|
*
|
||||||
|
* Session ID is always undefined (new session).
|
||||||
|
* Callers that need session continuity pass sessionId explicitly
|
||||||
|
* (e.g., --continue flag or /resume command).
|
||||||
*/
|
*/
|
||||||
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
||||||
const globalConfig = resolveConfigValues(cwd, ['language', 'provider', 'model']);
|
const globalConfig = resolveConfigValues(cwd, ['language', 'provider', 'model']);
|
||||||
@ -66,10 +71,8 @@ export function initializeSession(cwd: string, personaName: string): SessionCont
|
|||||||
const providerType = globalConfig.provider as ProviderType;
|
const providerType = globalConfig.provider as ProviderType;
|
||||||
const provider = getProvider(providerType);
|
const provider = getProvider(providerType);
|
||||||
const model = globalConfig.model as string | undefined;
|
const model = globalConfig.model as string | undefined;
|
||||||
const savedSessions = loadPersonaSessions(cwd, providerType);
|
|
||||||
const sessionId: string | undefined = savedSessions[personaName];
|
|
||||||
|
|
||||||
return { provider, providerType, model, lang, personaName, sessionId };
|
return { provider, providerType, model, lang, personaName, sessionId: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -305,6 +308,15 @@ export async function runConversationLoop(
|
|||||||
return { action: 'cancel', task: '' };
|
return { action: 'cancel', task: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trimmed === '/resume') {
|
||||||
|
const selectedId = await selectRecentSession(cwd, ctx.lang);
|
||||||
|
if (selectedId) {
|
||||||
|
sessionId = selectedId;
|
||||||
|
info(getLabel('interactive.resumeSessionLoaded', ctx.lang));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
history.push({ role: 'user', content: trimmed });
|
history.push({ role: 'user', content: trimmed });
|
||||||
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interactive:
|
|||||||
conversationLabel: "Conversation:"
|
conversationLabel: "Conversation:"
|
||||||
noTranscript: "(No local transcript. Summarize the current session context.)"
|
noTranscript: "(No local transcript. Summarize the current session context.)"
|
||||||
ui:
|
ui:
|
||||||
intro: "Interactive mode - describe your task. Commands: /go (execute), /play (run now), /cancel (exit)"
|
intro: "Interactive mode - describe your task. Commands: /go (execute), /play (run now), /resume (load session), /cancel (exit)"
|
||||||
resume: "Resuming previous session"
|
resume: "Resuming previous session"
|
||||||
noConversation: "No conversation yet. Please describe your task first."
|
noConversation: "No conversation yet. Please describe your task first."
|
||||||
summarizeFailed: "Failed to summarize conversation. Please try again."
|
summarizeFailed: "Failed to summarize conversation. Please try again."
|
||||||
@ -39,8 +39,9 @@ interactive:
|
|||||||
confirm: "Reference a previous run's results?"
|
confirm: "Reference a previous run's results?"
|
||||||
prompt: "Select a run to reference:"
|
prompt: "Select a run to reference:"
|
||||||
noRuns: "No previous runs found."
|
noRuns: "No previous runs found."
|
||||||
|
continueNoSession: "No previous assistant session found. Starting a new session."
|
||||||
|
resumeSessionLoaded: "Session loaded. Subsequent AI calls will use this session."
|
||||||
sessionSelector:
|
sessionSelector:
|
||||||
confirm: "Choose a previous session?"
|
|
||||||
prompt: "Resume from a recent session?"
|
prompt: "Resume from a recent session?"
|
||||||
newSession: "New session"
|
newSession: "New session"
|
||||||
newSessionDescription: "Start a fresh conversation"
|
newSessionDescription: "Start a fresh conversation"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interactive:
|
|||||||
conversationLabel: "会話:"
|
conversationLabel: "会話:"
|
||||||
noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)"
|
noTranscript: "(ローカル履歴なし。現在のセッション文脈を要約してください。)"
|
||||||
ui:
|
ui:
|
||||||
intro: "対話モード - タスク内容を入力してください。コマンド: /go(実行), /play(即実行), /cancel(終了)"
|
intro: "対話モード - タスク内容を入力してください。コマンド: /go(実行), /play(即実行), /resume(セッション読込), /cancel(終了)"
|
||||||
resume: "前回のセッションを再開します"
|
resume: "前回のセッションを再開します"
|
||||||
noConversation: "まだ会話がありません。まずタスク内容を入力してください。"
|
noConversation: "まだ会話がありません。まずタスク内容を入力してください。"
|
||||||
summarizeFailed: "会話の要約に失敗しました。再度お試しください。"
|
summarizeFailed: "会話の要約に失敗しました。再度お試しください。"
|
||||||
@ -39,8 +39,9 @@ interactive:
|
|||||||
confirm: "前回の実行結果を参照しますか?"
|
confirm: "前回の実行結果を参照しますか?"
|
||||||
prompt: "参照するrunを選択してください:"
|
prompt: "参照するrunを選択してください:"
|
||||||
noRuns: "前回のrunが見つかりませんでした。"
|
noRuns: "前回のrunが見つかりませんでした。"
|
||||||
|
continueNoSession: "前回のアシスタントセッションが見つかりません。新しいセッションで開始します。"
|
||||||
|
resumeSessionLoaded: "セッションを読み込みました。以降のAI呼び出しでこのセッションを使用します。"
|
||||||
sessionSelector:
|
sessionSelector:
|
||||||
confirm: "前回セッションを選択しますか?"
|
|
||||||
prompt: "直近のセッションを引き継ぎますか?"
|
prompt: "直近のセッションを引き継ぎますか?"
|
||||||
newSession: "新しいセッション"
|
newSession: "新しいセッション"
|
||||||
newSessionDescription: "新しい会話を始める"
|
newSessionDescription: "新しい会話を始める"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user