commandmate 0.2.0 → 0.2.2
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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +13 -13
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route.js.nft.json +1 -1
- package/.next/server/app/api/external-apps/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/external-apps/route.js.nft.json +1 -1
- package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/excluded/route.js +8 -8
- package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/restore/route.js +7 -7
- package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/route.js +5 -5
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/scan/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/sync/route.js +5 -5
- package/.next/server/app/api/repositories/sync/route.js.nft.json +1 -1
- package/.next/server/app/api/slash-commands.body +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/capture/route.js +2 -2
- package/.next/server/app/api/worktrees/[id]/cli-tool/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +5 -5
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +9 -9
- package/.next/server/chunks/5488.js +5 -5
- package/.next/server/chunks/7536.js +1 -1
- package/.next/server/chunks/8174.js +6 -6
- package/.next/server/chunks/9367.js +2 -2
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/6568-c65d7e4d7db7655b.js +1 -0
- package/.next/static/chunks/{2957-327e43ef4c12808f.js → 9325-9e98829c1e75f42f.js} +1 -1
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/{page-9e5adf57cbbbdf05.js → page-7eb14f8043796805.js} +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-912c3c4c66821d99.js +1 -0
- package/.next/static/css/d4b58a1129eff6af.css +3 -0
- package/.next/trace +5 -5
- package/dist/server/server.js +25 -2
- package/dist/server/src/config/auto-yes-config.js +53 -0
- package/dist/server/src/lib/auto-yes-manager.js +94 -11
- package/dist/server/src/lib/claude-poller.js +4 -0
- package/dist/server/src/lib/claude-session.js +54 -19
- package/dist/server/src/lib/cli-patterns.js +100 -4
- package/dist/server/src/lib/cli-tools/codex.js +16 -3
- package/dist/server/src/lib/db-repository.js +482 -0
- package/dist/server/src/lib/pasted-text-helper.js +58 -0
- package/dist/server/src/lib/prompt-detector.js +199 -109
- package/dist/server/src/lib/response-poller.js +74 -27
- package/dist/server/src/lib/tmux.js +48 -0
- package/package.json +1 -1
- package/.next/static/chunks/6568-38a33aa67d82e12b.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-d64624eb67af57c0.js +0 -1
- package/.next/static/css/28be35e4727ae7ef.css +0 -3
- /package/.next/static/{bdUePCj-b9Gv5okYGp49O → HhG0EHeG9E4wTJ4sqnLdv}/_buildManifest.js +0 -0
- /package/.next/static/{bdUePCj-b9Gv5okYGp49O → HhG0EHeG9E4wTJ4sqnLdv}/_ssgManifest.js +0 -0
package/dist/server/server.js
CHANGED
|
@@ -37,6 +37,8 @@ const response_poller_1 = require("./src/lib/response-poller");
|
|
|
37
37
|
const auto_yes_manager_1 = require("./src/lib/auto-yes-manager");
|
|
38
38
|
const db_migrations_1 = require("./src/lib/db-migrations");
|
|
39
39
|
const env_1 = require("./src/lib/env");
|
|
40
|
+
const db_repository_1 = require("./src/lib/db-repository");
|
|
41
|
+
const db_1 = require("./src/lib/db");
|
|
40
42
|
const dev = process.env.NODE_ENV !== 'production';
|
|
41
43
|
const hostname = (0, env_1.getEnvByKey)('CM_BIND') || '127.0.0.1';
|
|
42
44
|
const port = parseInt((0, env_1.getEnvByKey)('CM_PORT') || '3000', 10);
|
|
@@ -76,8 +78,29 @@ app.prepare().then(() => {
|
|
|
76
78
|
repositoryPaths.forEach((path, i) => {
|
|
77
79
|
console.log(` ${i + 1}. ${path}`);
|
|
78
80
|
});
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
+
// Issue #202: Register environment variable repositories and filter out excluded ones
|
|
82
|
+
// registerAndFilterRepositories() encapsulates the ordering constraint:
|
|
83
|
+
// registration MUST happen before filtering (see design policy Section 4)
|
|
84
|
+
const { filteredPaths, excludedPaths, excludedCount } = (0, db_repository_1.registerAndFilterRepositories)(db, repositoryPaths);
|
|
85
|
+
if (excludedCount > 0) {
|
|
86
|
+
console.log(`Excluded repositories: ${excludedCount}, Active repositories: ${filteredPaths.length}`);
|
|
87
|
+
// SF-SEC-003: Log excluded repository paths for audit/troubleshooting
|
|
88
|
+
excludedPaths.forEach(p => {
|
|
89
|
+
console.log(` [excluded] ${p}`);
|
|
90
|
+
});
|
|
91
|
+
// Issue #202: Remove worktrees of excluded repositories from DB
|
|
92
|
+
// Without this, worktree records remain in DB and appear in the UI
|
|
93
|
+
for (const excludedPath of excludedPaths) {
|
|
94
|
+
const resolvedPath = (0, db_repository_1.resolveRepositoryPath)(excludedPath);
|
|
95
|
+
const worktreeIds = (0, db_1.getWorktreeIdsByRepository)(db, resolvedPath);
|
|
96
|
+
if (worktreeIds.length > 0) {
|
|
97
|
+
const result = (0, db_1.deleteWorktreesByIds)(db, worktreeIds);
|
|
98
|
+
console.log(` Removed ${result.deletedCount} worktree(s) from excluded repository: ${resolvedPath}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Scan filtered repositories (excluded repos are skipped)
|
|
103
|
+
const worktrees = await (0, worktrees_1.scanMultipleRepositories)(filteredPaths);
|
|
81
104
|
// Sync to database
|
|
82
105
|
(0, worktrees_1.syncWorktreesToDB)(db, worktrees);
|
|
83
106
|
console.log(`✓ Total: ${worktrees.length} worktree(s) synced to database`);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-Yes Configuration Constants
|
|
4
|
+
*
|
|
5
|
+
* Shared config for Auto-Yes duration settings.
|
|
6
|
+
* Used by both server (auto-yes-manager.ts, route.ts) and client
|
|
7
|
+
* (AutoYesConfirmDialog.tsx, AutoYesToggle.tsx) components.
|
|
8
|
+
*
|
|
9
|
+
* Issue #225: Duration selection feature
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DURATION_LABELS = exports.DEFAULT_AUTO_YES_DURATION = exports.ALLOWED_DURATIONS = void 0;
|
|
13
|
+
exports.isAllowedDuration = isAllowedDuration;
|
|
14
|
+
exports.formatTimeRemaining = formatTimeRemaining;
|
|
15
|
+
/** Allowed Auto-Yes durations in milliseconds */
|
|
16
|
+
exports.ALLOWED_DURATIONS = [3600000, 10800000, 28800000];
|
|
17
|
+
/** Default Auto-Yes duration (1 hour = 3600000ms) */
|
|
18
|
+
exports.DEFAULT_AUTO_YES_DURATION = 3600000;
|
|
19
|
+
/** UI display labels for each duration value */
|
|
20
|
+
exports.DURATION_LABELS = {
|
|
21
|
+
3600000: '1時間',
|
|
22
|
+
10800000: '3時間',
|
|
23
|
+
28800000: '8時間',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Type guard: check whether a value is a valid AutoYesDuration.
|
|
27
|
+
* Replaces `as AutoYesDuration` casts with a runtime-safe check.
|
|
28
|
+
*/
|
|
29
|
+
function isAllowedDuration(value) {
|
|
30
|
+
return typeof value === 'number' && exports.ALLOWED_DURATIONS.includes(value);
|
|
31
|
+
}
|
|
32
|
+
/** Milliseconds per second */
|
|
33
|
+
const MS_PER_SECOND = 1000;
|
|
34
|
+
/** Milliseconds per minute */
|
|
35
|
+
const MS_PER_MINUTE = 60000;
|
|
36
|
+
/** Milliseconds per hour */
|
|
37
|
+
const MS_PER_HOUR = 3600000;
|
|
38
|
+
/**
|
|
39
|
+
* Format remaining time as MM:SS (under 1 hour) or H:MM:SS (1 hour or more).
|
|
40
|
+
*
|
|
41
|
+
* Extracted from AutoYesToggle.tsx for direct testability and reuse.
|
|
42
|
+
* Negative remaining time is clamped to 0.
|
|
43
|
+
*/
|
|
44
|
+
function formatTimeRemaining(expiresAt) {
|
|
45
|
+
const remaining = Math.max(0, expiresAt - Date.now());
|
|
46
|
+
const hours = Math.floor(remaining / MS_PER_HOUR);
|
|
47
|
+
const minutes = Math.floor((remaining % MS_PER_HOUR) / MS_PER_MINUTE);
|
|
48
|
+
const seconds = Math.floor((remaining % MS_PER_MINUTE) / MS_PER_SECOND);
|
|
49
|
+
if (hours > 0) {
|
|
50
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
51
|
+
}
|
|
52
|
+
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
53
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* auto-yes responses when browser tabs are in background.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.POLLING_INTERVAL_MS = void 0;
|
|
12
|
+
exports.THINKING_CHECK_LINE_COUNT = exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.POLLING_INTERVAL_MS = void 0;
|
|
13
13
|
exports.isValidWorktreeId = isValidWorktreeId;
|
|
14
14
|
exports.calculateBackoffInterval = calculateBackoffInterval;
|
|
15
15
|
exports.isAutoYesExpired = isAutoYesExpired;
|
|
@@ -28,6 +28,7 @@ const auto_yes_resolver_1 = require("./auto-yes-resolver");
|
|
|
28
28
|
const tmux_1 = require("./tmux");
|
|
29
29
|
const manager_1 = require("./cli-tools/manager");
|
|
30
30
|
const cli_patterns_1 = require("./cli-patterns");
|
|
31
|
+
const auto_yes_config_1 = require("../config/auto-yes-config");
|
|
31
32
|
// =============================================================================
|
|
32
33
|
// Constants (Issue #138)
|
|
33
34
|
// =============================================================================
|
|
@@ -39,8 +40,16 @@ exports.MAX_BACKOFF_MS = 60000;
|
|
|
39
40
|
exports.MAX_CONSECUTIVE_ERRORS = 5;
|
|
40
41
|
/** Maximum concurrent pollers (DoS protection) */
|
|
41
42
|
exports.MAX_CONCURRENT_POLLERS = 50;
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Number of lines from the end to check for thinking indicators (Issue #191)
|
|
45
|
+
* Matches detectPrompt()'s multiple_choice scan range (50 lines in prompt-detector.ts)
|
|
46
|
+
* to ensure Issue #161 Layer 1 defense covers the same scope as prompt detection.
|
|
47
|
+
*
|
|
48
|
+
* IMPORTANT: This value is semantically coupled to the hardcoded 50 in
|
|
49
|
+
* prompt-detector.ts detectMultipleChoicePrompt() (L268: Math.max(0, lines.length - 50)).
|
|
50
|
+
* See SF-001 in Stage 1 review. A cross-reference test validates this coupling.
|
|
51
|
+
*/
|
|
52
|
+
exports.THINKING_CHECK_LINE_COUNT = 50;
|
|
44
53
|
/** Worktree ID validation pattern (security: prevent command injection) */
|
|
45
54
|
const WORKTREE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
46
55
|
/** In-memory storage for auto-yes states (globalThis for hot reload persistence) */
|
|
@@ -106,14 +115,17 @@ function getAutoYesState(worktreeId) {
|
|
|
106
115
|
}
|
|
107
116
|
/**
|
|
108
117
|
* Set the auto-yes enabled state for a worktree
|
|
118
|
+
* @param duration - Optional duration in milliseconds (must be an ALLOWED_DURATIONS value).
|
|
119
|
+
* Defaults to DEFAULT_AUTO_YES_DURATION (1 hour) when omitted.
|
|
109
120
|
*/
|
|
110
|
-
function setAutoYesEnabled(worktreeId, enabled) {
|
|
121
|
+
function setAutoYesEnabled(worktreeId, enabled, duration) {
|
|
111
122
|
if (enabled) {
|
|
112
123
|
const now = Date.now();
|
|
124
|
+
const effectiveDuration = duration ?? auto_yes_config_1.DEFAULT_AUTO_YES_DURATION;
|
|
113
125
|
const state = {
|
|
114
126
|
enabled: true,
|
|
115
127
|
enabledAt: now,
|
|
116
|
-
expiresAt: now +
|
|
128
|
+
expiresAt: now + effectiveDuration,
|
|
117
129
|
};
|
|
118
130
|
autoYesStates.set(worktreeId, state);
|
|
119
131
|
return state;
|
|
@@ -210,12 +222,29 @@ async function pollAutoYes(worktreeId, cliToolId) {
|
|
|
210
222
|
// 2.5. Skip prompt detection during thinking state (Issue #161, Layer 1)
|
|
211
223
|
// This prevents false positive detection of numbered lists in CLI output
|
|
212
224
|
// while Claude is actively processing (thinking/planning).
|
|
213
|
-
|
|
225
|
+
//
|
|
226
|
+
// Issue #191: Apply windowing to detectThinking() to prevent stale thinking
|
|
227
|
+
// summary lines (e.g., "· Simmering…") from blocking prompt detection.
|
|
228
|
+
// Window size matches detectPrompt()'s multiple_choice scan range (50 lines).
|
|
229
|
+
//
|
|
230
|
+
// Safety: Claude CLI does not emit prompts during thinking, so narrowing
|
|
231
|
+
// the window cannot cause false auto-responses (see IA-003 in design doc).
|
|
232
|
+
//
|
|
233
|
+
// Processing order: stripAnsi -> split -> slice -> join
|
|
234
|
+
// stripAnsi is applied BEFORE split to ensure ANSI escape sequences spanning
|
|
235
|
+
// line boundaries do not affect line counting (IA-002).
|
|
236
|
+
//
|
|
237
|
+
// Boundary case: if buffer has fewer than 50 lines, slice(-50) returns the
|
|
238
|
+
// entire array (Array.prototype.slice specification), which is safe degradation
|
|
239
|
+
// equivalent to pre-fix behavior (IA-001).
|
|
240
|
+
const recentLines = cleanOutput.split('\n').slice(-exports.THINKING_CHECK_LINE_COUNT).join('\n');
|
|
241
|
+
if ((0, cli_patterns_1.detectThinking)(cliToolId, recentLines)) {
|
|
214
242
|
scheduleNextPoll(worktreeId, cliToolId);
|
|
215
243
|
return;
|
|
216
244
|
}
|
|
217
245
|
// 3. Detect prompt
|
|
218
|
-
const
|
|
246
|
+
const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
|
|
247
|
+
const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput, promptOptions);
|
|
219
248
|
if (!promptDetection.isPrompt || !promptDetection.promptData) {
|
|
220
249
|
// No prompt detected, schedule next poll
|
|
221
250
|
scheduleNextPoll(worktreeId, cliToolId);
|
|
@@ -232,10 +261,64 @@ async function pollAutoYes(worktreeId, cliToolId) {
|
|
|
232
261
|
const manager = manager_1.CLIToolManager.getInstance();
|
|
233
262
|
const cliTool = manager.getTool(cliToolId);
|
|
234
263
|
const sessionName = cliTool.getSessionName(worktreeId);
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
// Issue #193: Claude Code AskUserQuestion uses cursor-based navigation
|
|
265
|
+
// (Arrow/Space/Enter), not number input. Detect multi-choice and send
|
|
266
|
+
// appropriate key sequence instead of typing the number.
|
|
267
|
+
const isClaudeMultiChoice = cliToolId === 'claude'
|
|
268
|
+
&& promptDetection.promptData?.type === 'multiple_choice'
|
|
269
|
+
&& /^\d+$/.test(answer);
|
|
270
|
+
if (isClaudeMultiChoice && promptDetection.promptData?.type === 'multiple_choice') {
|
|
271
|
+
const targetNum = parseInt(answer, 10);
|
|
272
|
+
const mcOptions = promptDetection.promptData.options;
|
|
273
|
+
const defaultOption = mcOptions.find(o => o.isDefault);
|
|
274
|
+
const defaultNum = defaultOption?.number ?? 1;
|
|
275
|
+
const offset = targetNum - defaultNum;
|
|
276
|
+
// Detect multi-select (checkbox) prompts by checking for [ ] in option labels.
|
|
277
|
+
const isMultiSelect = mcOptions.some(o => /^\[[ x]\] /.test(o.label));
|
|
278
|
+
if (isMultiSelect) {
|
|
279
|
+
// Multi-select: toggle checkbox, then navigate to "Next" and submit
|
|
280
|
+
const checkboxCount = mcOptions.filter(o => /^\[[ x]\] /.test(o.label)).length;
|
|
281
|
+
const keys = [];
|
|
282
|
+
// 1. Navigate to target option
|
|
283
|
+
if (offset > 0) {
|
|
284
|
+
for (let i = 0; i < offset; i++)
|
|
285
|
+
keys.push('Down');
|
|
286
|
+
}
|
|
287
|
+
else if (offset < 0) {
|
|
288
|
+
for (let i = 0; i < Math.abs(offset); i++)
|
|
289
|
+
keys.push('Up');
|
|
290
|
+
}
|
|
291
|
+
// 2. Space to toggle checkbox
|
|
292
|
+
keys.push('Space');
|
|
293
|
+
// 3. Navigate to "Next" button (positioned right after all checkbox options)
|
|
294
|
+
const downToNext = checkboxCount - targetNum + 1;
|
|
295
|
+
for (let i = 0; i < downToNext; i++)
|
|
296
|
+
keys.push('Down');
|
|
297
|
+
// 4. Enter to submit
|
|
298
|
+
keys.push('Enter');
|
|
299
|
+
await (0, tmux_1.sendSpecialKeys)(sessionName, keys);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Single-select: navigate and Enter to select
|
|
303
|
+
const keys = [];
|
|
304
|
+
if (offset > 0) {
|
|
305
|
+
for (let i = 0; i < offset; i++)
|
|
306
|
+
keys.push('Down');
|
|
307
|
+
}
|
|
308
|
+
else if (offset < 0) {
|
|
309
|
+
for (let i = 0; i < Math.abs(offset); i++)
|
|
310
|
+
keys.push('Up');
|
|
311
|
+
}
|
|
312
|
+
keys.push('Enter');
|
|
313
|
+
await (0, tmux_1.sendSpecialKeys)(sessionName, keys);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// Standard CLI prompt: send text + Enter (y/n, Approve?, etc.)
|
|
318
|
+
await (0, tmux_1.sendKeys)(sessionName, answer, false);
|
|
319
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
320
|
+
await (0, tmux_1.sendKeys)(sessionName, '', true);
|
|
321
|
+
}
|
|
239
322
|
// 6. Update timestamp
|
|
240
323
|
updateLastServerResponseTimestamp(worktreeId, Date.now());
|
|
241
324
|
// 7. Reset error count on success
|
|
@@ -138,6 +138,8 @@ function extractClaudeResponse(output, lastCapturedLine) {
|
|
|
138
138
|
}
|
|
139
139
|
// Check if this is an interactive prompt (yes/no or multiple choice)
|
|
140
140
|
// Interactive prompts don't have the ">" prompt and separator, so we need to detect them separately
|
|
141
|
+
// TODO [Issue #193]: This code path is unreachable (claude-poller.ts is superseded by response-poller.ts).
|
|
142
|
+
// When refactoring/removing claude-poller.ts, apply stripAnsi() + buildDetectPromptOptions() here.
|
|
141
143
|
if (!isThinking) {
|
|
142
144
|
const fullOutput = lines.join('\n');
|
|
143
145
|
const promptDetection = (0, prompt_detector_1.detectPrompt)(fullOutput);
|
|
@@ -196,6 +198,8 @@ async function checkForResponse(worktreeId) {
|
|
|
196
198
|
return false;
|
|
197
199
|
}
|
|
198
200
|
// Response is complete! Check if it's a prompt
|
|
201
|
+
// TODO [Issue #193]: This code path is unreachable (claude-poller.ts is superseded by response-poller.ts).
|
|
202
|
+
// When refactoring/removing claude-poller.ts, apply stripAnsi() + buildDetectPromptOptions() here.
|
|
199
203
|
const promptDetection = (0, prompt_detector_1.detectPrompt)(result.response);
|
|
200
204
|
if (promptDetection.isPrompt) {
|
|
201
205
|
// This is a prompt - save as prompt message
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages Claude CLI sessions within tmux for each worktree
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.CLAUDE_PROMPT_POLL_INTERVAL = exports.CLAUDE_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_POST_PROMPT_DELAY = exports.CLAUDE_INIT_POLL_INTERVAL = exports.CLAUDE_INIT_TIMEOUT = void 0;
|
|
7
|
+
exports.CLAUDE_PROMPT_POLL_INTERVAL = exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_POST_PROMPT_DELAY = exports.CLAUDE_INIT_POLL_INTERVAL = exports.CLAUDE_INIT_TIMEOUT = void 0;
|
|
8
8
|
exports.getSessionName = getSessionName;
|
|
9
9
|
exports.isClaudeInstalled = isClaudeInstalled;
|
|
10
10
|
exports.isClaudeRunning = isClaudeRunning;
|
|
@@ -17,6 +17,7 @@ exports.stopClaudeSession = stopClaudeSession;
|
|
|
17
17
|
exports.restartClaudeSession = restartClaudeSession;
|
|
18
18
|
const tmux_1 = require("./tmux");
|
|
19
19
|
const cli_patterns_1 = require("./cli-patterns");
|
|
20
|
+
const pasted_text_helper_1 = require("./pasted-text-helper");
|
|
20
21
|
const child_process_1 = require("child_process");
|
|
21
22
|
const util_1 = require("util");
|
|
22
23
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
@@ -42,6 +43,9 @@ function getErrorMessage(error) {
|
|
|
42
43
|
* - Authenticate with Anthropic servers (if needed)
|
|
43
44
|
* - Display the interactive prompt
|
|
44
45
|
*
|
|
46
|
+
* This timeout also covers trust dialog auto-response time (typically <1s).
|
|
47
|
+
* When reducing this value, consider dialog response overhead.
|
|
48
|
+
*
|
|
45
49
|
* 15 seconds provides headroom for slower networks or cold starts.
|
|
46
50
|
*/
|
|
47
51
|
exports.CLAUDE_INIT_TIMEOUT = 15000;
|
|
@@ -75,6 +79,24 @@ exports.CLAUDE_POST_PROMPT_DELAY = 500;
|
|
|
75
79
|
* if it's still processing a previous request.
|
|
76
80
|
*/
|
|
77
81
|
exports.CLAUDE_PROMPT_WAIT_TIMEOUT = 5000;
|
|
82
|
+
/**
|
|
83
|
+
* Prompt wait timeout before message send (milliseconds).
|
|
84
|
+
*
|
|
85
|
+
* Used exclusively by sendMessageToClaude() to limit how long we wait
|
|
86
|
+
* for Claude to return to a prompt state before sending a user message.
|
|
87
|
+
* This is separate from CLAUDE_PROMPT_WAIT_TIMEOUT (5000ms, the default
|
|
88
|
+
* for waitForPrompt()) because sendMessageToClaude() may be called
|
|
89
|
+
* shortly after session initialization, where Claude CLI needs additional
|
|
90
|
+
* time to become ready.
|
|
91
|
+
*
|
|
92
|
+
* Relationship to other timeout constants:
|
|
93
|
+
* - CLAUDE_PROMPT_WAIT_TIMEOUT (5000ms): Default for waitForPrompt()
|
|
94
|
+
* - CLAUDE_SEND_PROMPT_WAIT_TIMEOUT (10000ms): sendMessageToClaude() specific
|
|
95
|
+
* - CLAUDE_INIT_TIMEOUT (15000ms): Session initialization timeout
|
|
96
|
+
*
|
|
97
|
+
* @see Issue #187 - Constant unification for sendMessageToClaude timeout
|
|
98
|
+
*/
|
|
99
|
+
exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = 10000;
|
|
78
100
|
/**
|
|
79
101
|
* Prompt wait polling interval (milliseconds)
|
|
80
102
|
*
|
|
@@ -227,7 +249,6 @@ async function waitForPrompt(sessionName, timeout = exports.CLAUDE_PROMPT_WAIT_T
|
|
|
227
249
|
* await startClaudeSession({
|
|
228
250
|
* worktreeId: 'feature-foo',
|
|
229
251
|
* worktreePath: '/path/to/worktree',
|
|
230
|
-
* baseUrl: 'http://localhost:3000',
|
|
231
252
|
* });
|
|
232
253
|
* ```
|
|
233
254
|
*/
|
|
@@ -262,21 +283,33 @@ async function startClaudeSession(options) {
|
|
|
262
283
|
const pollInterval = exports.CLAUDE_INIT_POLL_INTERVAL;
|
|
263
284
|
const startTime = Date.now();
|
|
264
285
|
let initialized = false;
|
|
286
|
+
let trustDialogHandled = false;
|
|
265
287
|
while (Date.now() - startTime < maxWaitTime) {
|
|
266
288
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
267
289
|
try {
|
|
268
290
|
const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
|
|
269
|
-
// Claude is ready when we see the prompt
|
|
270
|
-
// Use
|
|
291
|
+
// Claude is ready when we see the prompt (DRY-001)
|
|
292
|
+
// Use CLAUDE_PROMPT_PATTERN from cli-patterns.ts for consistency
|
|
271
293
|
// Strip ANSI escape sequences before pattern matching (Issue #152)
|
|
294
|
+
// Note: CLAUDE_SEPARATOR_PATTERN was removed from initialization check (Issue #187, P1-1)
|
|
295
|
+
// because separator early-detection caused premature returns before prompt was ready
|
|
272
296
|
const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
|
|
273
|
-
if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)
|
|
297
|
+
if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(cleanOutput)) {
|
|
274
298
|
// Wait for stability after prompt detection (CONS-007, DOC-001)
|
|
275
299
|
await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
|
|
276
300
|
console.log(`Claude initialized in ${Date.now() - startTime}ms`);
|
|
277
301
|
initialized = true;
|
|
278
302
|
break;
|
|
279
303
|
}
|
|
304
|
+
// Issue #201: Detect trust dialog and auto-respond with Enter
|
|
305
|
+
// Condition order: CLAUDE_PROMPT_PATTERN (above) is checked first for shortest path
|
|
306
|
+
if (!trustDialogHandled && cli_patterns_1.CLAUDE_TRUST_DIALOG_PATTERN.test(cleanOutput)) {
|
|
307
|
+
await (0, tmux_1.sendKeys)(sessionName, '', true);
|
|
308
|
+
trustDialogHandled = true;
|
|
309
|
+
// TODO: Log output method unification (console.log vs createLogger) to be addressed in a separate Issue (SF-002)
|
|
310
|
+
console.log('Trust dialog detected, sending Enter to confirm');
|
|
311
|
+
// Continue polling to wait for prompt detection
|
|
312
|
+
}
|
|
280
313
|
}
|
|
281
314
|
catch {
|
|
282
315
|
// Ignore capture errors during initialization
|
|
@@ -312,23 +345,26 @@ async function sendMessageToClaude(worktreeId, message) {
|
|
|
312
345
|
throw new Error(`Claude session ${sessionName} does not exist. Start the session first.`);
|
|
313
346
|
}
|
|
314
347
|
// Verify prompt state before sending (CONS-006, DRY-001)
|
|
315
|
-
// Use -50 lines to ensure we capture the prompt even with status bars
|
|
316
348
|
const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -50 });
|
|
317
|
-
// Strip ANSI escape sequences before pattern matching (Issue #152)
|
|
318
349
|
if (!cli_patterns_1.CLAUDE_PROMPT_PATTERN.test((0, cli_patterns_1.stripAnsi)(output))) {
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
await waitForPrompt(sessionName, 10000);
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
// Log warning but don't block - Claude might be in a special state
|
|
326
|
-
console.warn(`[sendMessageToClaude] Prompt not detected, sending anyway`);
|
|
327
|
-
}
|
|
350
|
+
// Path B: Prompt not detected - wait for it (P1: throw on timeout)
|
|
351
|
+
await waitForPrompt(sessionName, exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT);
|
|
328
352
|
}
|
|
353
|
+
// P0: Stability delay after prompt detection (both Path A and Path B)
|
|
354
|
+
// Same delay as startClaudeSession() to ensure Claude CLI input handler is ready
|
|
355
|
+
// NOTE: This 500ms delay also applies to 2nd+ messages. Currently acceptable since
|
|
356
|
+
// Claude CLI response time (seconds to tens of seconds) dwarfs this overhead.
|
|
357
|
+
// If future batch-send use cases arise, this could be optimized to first-message-only,
|
|
358
|
+
// but that optimization is deferred per YAGNI principle. (ref: F-3)
|
|
359
|
+
await new Promise((resolve) => setTimeout(resolve, exports.CLAUDE_POST_PROMPT_DELAY));
|
|
329
360
|
// Send message using sendKeys consistently (CONS-001)
|
|
330
|
-
await (0, tmux_1.sendKeys)(sessionName, message, false);
|
|
331
|
-
await (0, tmux_1.sendKeys)(sessionName, '', true);
|
|
361
|
+
await (0, tmux_1.sendKeys)(sessionName, message, false);
|
|
362
|
+
await (0, tmux_1.sendKeys)(sessionName, '', true);
|
|
363
|
+
// Issue #212: Detect [Pasted text] and resend Enter for multi-line messages
|
|
364
|
+
// MF-001: Single-line messages skip detection (+0ms overhead)
|
|
365
|
+
if (message.includes('\n')) {
|
|
366
|
+
await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
|
|
367
|
+
}
|
|
332
368
|
console.log(`Sent message to Claude session: ${sessionName}`);
|
|
333
369
|
}
|
|
334
370
|
/**
|
|
@@ -403,7 +439,6 @@ async function stopClaudeSession(worktreeId) {
|
|
|
403
439
|
* await restartClaudeSession({
|
|
404
440
|
* worktreeId: 'feature-foo',
|
|
405
441
|
* worktreePath: '/path/to/worktree',
|
|
406
|
-
* baseUrl: 'http://localhost:3000',
|
|
407
442
|
* });
|
|
408
443
|
* ```
|
|
409
444
|
*/
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* Shared between response-poller.ts and API routes
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.GEMINI_PROMPT_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
|
|
7
|
+
exports.GEMINI_PROMPT_PATTERN = exports.MAX_PASTED_TEXT_RETRIES = exports.PASTED_TEXT_DETECT_DELAY = exports.PASTED_TEXT_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_TRUST_DIALOG_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
|
|
8
8
|
exports.detectThinking = detectThinking;
|
|
9
9
|
exports.getCliToolPatterns = getCliToolPatterns;
|
|
10
10
|
exports.stripAnsi = stripAnsi;
|
|
11
|
+
exports.buildDetectPromptOptions = buildDetectPromptOptions;
|
|
11
12
|
const logger_1 = require("./logger");
|
|
12
13
|
const logger = (0, logger_1.createLogger)('cli-patterns');
|
|
13
14
|
/**
|
|
@@ -23,8 +24,14 @@ exports.CLAUDE_SPINNER_CHARS = [
|
|
|
23
24
|
* Claude thinking pattern
|
|
24
25
|
* Matches spinner character followed by activity text ending with …
|
|
25
26
|
* The text can contain spaces (e.g., "Verifying implementation (dead code detection)…")
|
|
27
|
+
*
|
|
28
|
+
* Alternative 2: "esc to interrupt" status bar text (Issue #XXX)
|
|
29
|
+
* Claude Code shows "esc to interrupt" in the terminal status bar during active processing.
|
|
30
|
+
* Previous pattern required closing paren `to interrupt\)` matching `(esc to interrupt)`,
|
|
31
|
+
* but Claude Code v2.x status bar format uses `· esc to interrupt ·` without parens.
|
|
32
|
+
* Updated to match `esc to interrupt` which covers both formats.
|
|
26
33
|
*/
|
|
27
|
-
exports.CLAUDE_THINKING_PATTERN = new RegExp(`[${exports.CLAUDE_SPINNER_CHARS.join('')}]\\s+.+…|to interrupt
|
|
34
|
+
exports.CLAUDE_THINKING_PATTERN = new RegExp(`[${exports.CLAUDE_SPINNER_CHARS.join('')}]\\s+.+…|esc to interrupt`, 'm');
|
|
28
35
|
/**
|
|
29
36
|
* Codex thinking pattern
|
|
30
37
|
* Matches activity indicators like "• Planning", "• Searching", etc.
|
|
@@ -45,6 +52,19 @@ exports.CLAUDE_PROMPT_PATTERN = /^[>❯](\s*$|\s+\S)/m;
|
|
|
45
52
|
* Claude separator pattern
|
|
46
53
|
*/
|
|
47
54
|
exports.CLAUDE_SEPARATOR_PATTERN = /^─{10,}$/m;
|
|
55
|
+
/**
|
|
56
|
+
* Claude trust dialog pattern (Issue #201)
|
|
57
|
+
*
|
|
58
|
+
* Matches the "Quick safety check" dialog displayed by Claude CLI v2.x
|
|
59
|
+
* when accessing a workspace for the first time.
|
|
60
|
+
*
|
|
61
|
+
* Intentionally uses partial matching (no line-start anchor ^):
|
|
62
|
+
* Other pattern constants (CLAUDE_PROMPT_PATTERN, CLAUDE_SEPARATOR_PATTERN, etc.)
|
|
63
|
+
* use line-start anchors (^), but this pattern needs to match at any position
|
|
64
|
+
* within the tmux output buffer because the dialog text may appear after
|
|
65
|
+
* tmux padding or other output. (SF-001)
|
|
66
|
+
*/
|
|
67
|
+
exports.CLAUDE_TRUST_DIALOG_PATTERN = /Yes, I trust this folder/m;
|
|
48
68
|
/**
|
|
49
69
|
* Codex prompt pattern
|
|
50
70
|
* T1.2: Improved to detect empty prompts as well
|
|
@@ -54,6 +74,44 @@ exports.CODEX_PROMPT_PATTERN = /^›\s*/m;
|
|
|
54
74
|
* Codex separator pattern
|
|
55
75
|
*/
|
|
56
76
|
exports.CODEX_SEPARATOR_PATTERN = /^─.*Worked for.*─+$/m;
|
|
77
|
+
/**
|
|
78
|
+
* Pasted text pattern
|
|
79
|
+
*
|
|
80
|
+
* Claude CLI displays this when it detects multi-line text paste in the
|
|
81
|
+
* ink-based TextInput. The pattern matches the folded display format.
|
|
82
|
+
*
|
|
83
|
+
* @example "[Pasted text #1 +46 lines]"
|
|
84
|
+
* @see Issue #212, #163
|
|
85
|
+
* @designNote PASTE-001: Pattern matches the start of the indicator only.
|
|
86
|
+
* The line count (+XX lines) is variable, so we match the fixed prefix
|
|
87
|
+
* to minimize false negatives. False positive risk is low because
|
|
88
|
+
* "[Pasted text #" is a unique format generated by Claude CLI's ink renderer.
|
|
89
|
+
* @designNote PASTE-001-FP (SF-S4-002): When used in skipPatterns,
|
|
90
|
+
* line-level matching could filter legitimate response lines if Claude's
|
|
91
|
+
* answer text happens to contain "[Pasted text #". This is unlikely and
|
|
92
|
+
* acceptable -- only the affected line would be lost.
|
|
93
|
+
*/
|
|
94
|
+
exports.PASTED_TEXT_PATTERN = /\[Pasted text #\d+/;
|
|
95
|
+
/**
|
|
96
|
+
* Pasted text detection delay (milliseconds)
|
|
97
|
+
*
|
|
98
|
+
* Wait time after sendKeys for tmux buffer to reflect [Pasted text] display.
|
|
99
|
+
*
|
|
100
|
+
* @see Issue #212
|
|
101
|
+
* @designNote PASTE-002: 500ms is the empirically measured time for
|
|
102
|
+
* Claude CLI's ink rendering to complete. capturePane({ startLine: -10 })
|
|
103
|
+
* reads only the last 10 lines since [Pasted text] appears in the most
|
|
104
|
+
* recent few lines.
|
|
105
|
+
*/
|
|
106
|
+
exports.PASTED_TEXT_DETECT_DELAY = 500;
|
|
107
|
+
/**
|
|
108
|
+
* Pasted text detection max retries
|
|
109
|
+
*
|
|
110
|
+
* @see Issue #212
|
|
111
|
+
* @designNote PASTE-003: 3 retries x 500ms = max 1500ms additional delay.
|
|
112
|
+
* Typically resolves on the first attempt (+500ms).
|
|
113
|
+
*/
|
|
114
|
+
exports.MAX_PASTED_TEXT_RETRIES = 3;
|
|
57
115
|
/**
|
|
58
116
|
* Gemini shell prompt pattern
|
|
59
117
|
*/
|
|
@@ -100,6 +158,7 @@ function getCliToolPatterns(cliToolId) {
|
|
|
100
158
|
/^\s*Tip:/, // Tip lines
|
|
101
159
|
/^\s*\?\s*for shortcuts/, // Shortcuts hint
|
|
102
160
|
/to interrupt\)/, // Part of "esc to interrupt" message
|
|
161
|
+
exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines] (Issue #212)
|
|
103
162
|
],
|
|
104
163
|
};
|
|
105
164
|
case 'codex':
|
|
@@ -121,6 +180,7 @@ function getCliToolPatterns(cliToolId) {
|
|
|
121
180
|
/^\s*└/, // Tree output (completion indicator)
|
|
122
181
|
/^\s*│/, // Continuation lines
|
|
123
182
|
/\(.*esc to interrupt\)/, // Interrupt hint
|
|
183
|
+
exports.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines] (Issue #212, defensive)
|
|
124
184
|
],
|
|
125
185
|
};
|
|
126
186
|
case 'gemini':
|
|
@@ -140,10 +200,46 @@ function getCliToolPatterns(cliToolId) {
|
|
|
140
200
|
}
|
|
141
201
|
}
|
|
142
202
|
/**
|
|
143
|
-
* Strip ANSI escape codes from a string
|
|
144
|
-
* Optimized version at module level for performance
|
|
203
|
+
* Strip ANSI escape codes from a string.
|
|
204
|
+
* Optimized version at module level for performance.
|
|
205
|
+
*
|
|
206
|
+
* Covers:
|
|
207
|
+
* - SGR sequences: ESC[Nm (colors, bold, underline, etc.)
|
|
208
|
+
* - OSC sequences: ESC]...BEL (window title, hyperlinks, etc.)
|
|
209
|
+
* - CSI sequences: ESC[...letter (cursor movement, erase, etc.)
|
|
210
|
+
*
|
|
211
|
+
* Known limitations (SEC-002):
|
|
212
|
+
* - 8-bit CSI (0x9B): C1 control code form of CSI is not covered
|
|
213
|
+
* - DEC private modes: ESC[?25h and similar are not covered
|
|
214
|
+
* - Character set switching: ESC(0, ESC(B are not covered
|
|
215
|
+
* - Some RGB color forms: ESC[38;2;r;g;bm may not be fully matched
|
|
216
|
+
*
|
|
217
|
+
* In practice, tmux capture-pane output rarely contains these sequences,
|
|
218
|
+
* so the risk is low. Future consideration: adopt the `strip-ansi` npm package
|
|
219
|
+
* for more comprehensive coverage.
|
|
145
220
|
*/
|
|
146
221
|
const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\[[0-9;]*m/g;
|
|
147
222
|
function stripAnsi(str) {
|
|
148
223
|
return str.replace(ANSI_PATTERN, '');
|
|
149
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Build DetectPromptOptions for a given CLI tool.
|
|
227
|
+
* Centralizes cliToolId-to-options mapping logic (DRY - MF-001).
|
|
228
|
+
*
|
|
229
|
+
* prompt-detector.ts remains CLI tool independent (Issue #161 principle);
|
|
230
|
+
* this function lives in cli-patterns.ts which already depends on CLIToolType.
|
|
231
|
+
*
|
|
232
|
+
* [Future extension memo (C-002)]
|
|
233
|
+
* If CLI tool count grows significantly (currently 3), consider migrating
|
|
234
|
+
* to a CLIToolConfig registry pattern where tool-specific settings
|
|
235
|
+
* (including promptDetectionOptions) are managed in a Record<CLIToolType, CLIToolConfig>.
|
|
236
|
+
*
|
|
237
|
+
* @param cliToolId - CLI tool identifier
|
|
238
|
+
* @returns DetectPromptOptions for the tool, or undefined for default behavior
|
|
239
|
+
*/
|
|
240
|
+
function buildDetectPromptOptions(cliToolId) {
|
|
241
|
+
if (cliToolId === 'claude') {
|
|
242
|
+
return { requireDefaultIndicator: false };
|
|
243
|
+
}
|
|
244
|
+
return undefined; // Default behavior (requireDefaultIndicator = true)
|
|
245
|
+
}
|
|
@@ -9,7 +9,15 @@ const base_1 = require("./base");
|
|
|
9
9
|
const tmux_1 = require("../tmux");
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const util_1 = require("util");
|
|
12
|
+
const pasted_text_helper_1 = require("../pasted-text-helper");
|
|
12
13
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
14
|
+
/**
|
|
15
|
+
* Extract error message from unknown error type (DRY)
|
|
16
|
+
* Same pattern as claude-session.ts getErrorMessage()
|
|
17
|
+
*/
|
|
18
|
+
function getErrorMessage(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
13
21
|
/**
|
|
14
22
|
* Codex initialization timing constants
|
|
15
23
|
* T2.6: Extracted as constants for maintainability
|
|
@@ -79,7 +87,7 @@ class CodexTool extends base_1.BaseCLITool {
|
|
|
79
87
|
console.log(`✓ Started Codex session: ${sessionName}`);
|
|
80
88
|
}
|
|
81
89
|
catch (error) {
|
|
82
|
-
const errorMessage =
|
|
90
|
+
const errorMessage = getErrorMessage(error);
|
|
83
91
|
throw new Error(`Failed to start Codex session: ${errorMessage}`);
|
|
84
92
|
}
|
|
85
93
|
}
|
|
@@ -105,10 +113,15 @@ class CodexTool extends base_1.BaseCLITool {
|
|
|
105
113
|
await execAsync(`tmux send-keys -t "${sessionName}" C-m`);
|
|
106
114
|
// Wait a moment for the message to be processed
|
|
107
115
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
116
|
+
// Issue #212: Detect [Pasted text] and resend Enter for multi-line messages
|
|
117
|
+
// MF-001: Single-line messages skip detection (+0ms overhead)
|
|
118
|
+
if (message.includes('\n')) {
|
|
119
|
+
await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
|
|
120
|
+
}
|
|
108
121
|
console.log(`✓ Sent message to Codex session: ${sessionName}`);
|
|
109
122
|
}
|
|
110
123
|
catch (error) {
|
|
111
|
-
const errorMessage =
|
|
124
|
+
const errorMessage = getErrorMessage(error);
|
|
112
125
|
throw new Error(`Failed to send message to Codex: ${errorMessage}`);
|
|
113
126
|
}
|
|
114
127
|
}
|
|
@@ -135,7 +148,7 @@ class CodexTool extends base_1.BaseCLITool {
|
|
|
135
148
|
}
|
|
136
149
|
}
|
|
137
150
|
catch (error) {
|
|
138
|
-
const errorMessage =
|
|
151
|
+
const errorMessage = getErrorMessage(error);
|
|
139
152
|
console.error(`Error stopping Codex session: ${errorMessage}`);
|
|
140
153
|
throw error;
|
|
141
154
|
}
|