commandmate 0.1.12 → 0.2.1
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/.env.example +4 -9
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +24 -24
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +7 -7
- 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/0.pack +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/react-loadable-manifest.json +7 -7
- 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 +2 -2
- package/.next/server/app/api/hooks/claude-done/route.js +1 -19
- package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js +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 +1 -1
- package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/excluded/route.js +36 -0
- package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -0
- package/.next/server/app/api/repositories/excluded.body +1 -0
- package/.next/server/app/api/repositories/excluded.meta +1 -0
- package/.next/server/app/api/repositories/restore/route.js +36 -0
- package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -0
- package/.next/server/app/api/repositories/route.js +36 -1
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/scan/route.js +1 -1
- package/.next/server/app/api/repositories/sync/route.js +36 -1
- package/.next/server/app/api/slash-commands/route.js +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]/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]/interrupt/route.js +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 +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 +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route.js +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 +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 +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js +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 +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js +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/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 +3 -3
- package/.next/server/app/page.js +7 -7
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.js +2 -2
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +4 -4
- package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
- 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 +10 -8
- package/.next/server/chunks/5488.js +36 -0
- package/.next/server/chunks/6550.js +1 -1
- package/.next/server/chunks/7425.js +53 -50
- package/.next/server/chunks/7536.js +1 -0
- package/.next/server/chunks/8174.js +23 -0
- package/.next/server/chunks/9367.js +19 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +2 -28
- package/.next/server/middleware-react-loadable-manifest.js +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/4327.740cc7fe2d0b5049.js +60 -0
- package/.next/static/chunks/4343-ebe884a2a80eb033.js +1 -0
- package/.next/static/chunks/6568-38a33aa67d82e12b.js +1 -0
- package/.next/static/chunks/816-c254f4e2406e696a.js +1 -0
- package/.next/static/chunks/app/layout-4804cfba519283cf.js +1 -0
- package/.next/static/chunks/app/page-3926224c4cdf315b.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-8bd88bdc29607413.js +1 -0
- package/.next/static/chunks/main-b6d727aa9248d4f2.js +1 -0
- package/.next/static/chunks/{webpack-3fc79fab9bb738d7.js → webpack-4f85dcef6279c6ee.js} +1 -1
- package/.next/static/css/28be35e4727ae7ef.css +3 -0
- package/.next/trace +5 -5
- package/.next/types/app/api/repositories/excluded/route.ts +343 -0
- package/.next/types/app/api/repositories/restore/route.ts +343 -0
- package/README.md +2 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -13
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +3 -7
- package/dist/cli/config/security-messages.d.ts +11 -0
- package/dist/cli/config/security-messages.d.ts.map +1 -0
- package/dist/cli/config/security-messages.js +29 -0
- package/dist/cli/types/index.d.ts +0 -1
- package/dist/cli/types/index.d.ts.map +1 -1
- package/dist/cli/utils/daemon.d.ts.map +1 -1
- package/dist/cli/utils/daemon.js +3 -7
- package/dist/cli/utils/env-setup.d.ts +0 -4
- package/dist/cli/utils/env-setup.d.ts.map +1 -1
- package/dist/cli/utils/env-setup.js +0 -14
- package/dist/cli/utils/security-logger.d.ts.map +1 -1
- package/dist/cli/utils/security-logger.js +1 -2
- package/dist/server/server.js +25 -2
- package/dist/server/src/lib/auto-yes-manager.js +100 -11
- package/dist/server/src/lib/claude-poller.js +341 -0
- package/dist/server/src/lib/claude-session.js +48 -19
- package/dist/server/src/lib/cli-patterns.js +69 -6
- package/dist/server/src/lib/cli-tools/base.js +7 -1
- package/dist/server/src/lib/cli-tools/codex.js +14 -2
- package/dist/server/src/lib/cli-tools/manager.js +27 -0
- package/dist/server/src/lib/cli-tools/types.js +7 -0
- package/dist/server/src/lib/cli-tools/validation.js +41 -0
- package/dist/server/src/lib/db-repository.js +482 -0
- package/dist/server/src/lib/db.js +23 -0
- package/dist/server/src/lib/env.js +0 -17
- package/dist/server/src/lib/logger.js +0 -4
- package/dist/server/src/lib/prompt-detector.js +297 -109
- package/dist/server/src/lib/response-poller.js +73 -27
- package/dist/server/src/lib/tmux.js +48 -0
- package/dist/server/src/lib/ws-server.js +12 -1
- package/dist/server/src/types/sidebar.js +16 -31
- package/dist/server/src/types/slash-commands.js +2 -0
- package/package.json +1 -1
- package/.next/server/chunks/1318.js +0 -29
- package/.next/server/chunks/2597.js +0 -1
- package/.next/server/chunks/2648.js +0 -1
- package/.next/server/chunks/9703.js +0 -31
- package/.next/server/chunks/9723.js +0 -19
- package/.next/server/edge-runtime-webpack.js +0 -2
- package/.next/server/edge-runtime-webpack.js.map +0 -1
- package/.next/server/src/middleware.js +0 -14
- package/.next/server/src/middleware.js.map +0 -1
- package/.next/static/chunks/2853-d11a80b03c9a1640.js +0 -1
- package/.next/static/chunks/4327.3b84aa049900fdeb.js +0 -60
- package/.next/static/chunks/816-7e340dad784be28c.js +0 -1
- package/.next/static/chunks/9365-733d8c05712d2888.js +0 -1
- package/.next/static/chunks/app/layout-37e55f11dcc8b1bf.js +0 -1
- package/.next/static/chunks/app/page-fe35d61f14b90a51.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-58fcf2e63c056743.js +0 -1
- package/.next/static/chunks/main-a960f4a5e1a2f598.js +0 -1
- package/.next/static/css/376b339640084689.css +0 -3
- /package/.next/static/{564GHwluX5xIv9qpqLJV2 → oUD-A998xeBoez6zsrTH3}/_buildManifest.js +0 -0
- /package/.next/static/{564GHwluX5xIv9qpqLJV2 → oUD-A998xeBoez6zsrTH3}/_ssgManifest.js +0 -0
|
@@ -8,6 +8,25 @@ exports.detectPrompt = detectPrompt;
|
|
|
8
8
|
exports.getAnswerInput = getAnswerInput;
|
|
9
9
|
const logger_1 = require("./logger");
|
|
10
10
|
const logger = (0, logger_1.createLogger)('prompt-detector');
|
|
11
|
+
/**
|
|
12
|
+
* Yes/no pattern definitions for data-driven matching.
|
|
13
|
+
* Each entry defines a regex pattern and its associated default option.
|
|
14
|
+
* Patterns are evaluated in order; the first match wins.
|
|
15
|
+
*
|
|
16
|
+
* Pattern format:
|
|
17
|
+
* - regex: Must have a capture group (1) for the question text
|
|
18
|
+
* - defaultOption: 'yes', 'no', or undefined (no default)
|
|
19
|
+
*/
|
|
20
|
+
const YES_NO_PATTERNS = [
|
|
21
|
+
// (y/n) - no default
|
|
22
|
+
{ regex: /^(.+)\s+\(y\/n\)\s*$/m },
|
|
23
|
+
// [y/N] - default no
|
|
24
|
+
{ regex: /^(.+)\s+\[y\/N\]\s*$/m, defaultOption: 'no' },
|
|
25
|
+
// [Y/n] - default yes
|
|
26
|
+
{ regex: /^(.+)\s+\[Y\/n\]\s*$/m, defaultOption: 'yes' },
|
|
27
|
+
// (yes/no) - no default
|
|
28
|
+
{ regex: /^(.+)\s+\(yes\/no\)\s*$/m },
|
|
29
|
+
];
|
|
11
30
|
/**
|
|
12
31
|
* Detect if output contains an interactive prompt
|
|
13
32
|
*
|
|
@@ -29,7 +48,7 @@ const logger = (0, logger_1.createLogger)('prompt-detector');
|
|
|
29
48
|
* // result.promptData.question === 'Do you want to proceed?'
|
|
30
49
|
* ```
|
|
31
50
|
*/
|
|
32
|
-
function detectPrompt(output) {
|
|
51
|
+
function detectPrompt(output, options) {
|
|
33
52
|
logger.debug('detectPrompt:start', { outputLength: output.length });
|
|
34
53
|
const lines = output.split('\n');
|
|
35
54
|
const lastLines = lines.slice(-10).join('\n');
|
|
@@ -39,7 +58,7 @@ function detectPrompt(output) {
|
|
|
39
58
|
// ❯ 1. Yes
|
|
40
59
|
// 2. No
|
|
41
60
|
// 3. Cancel
|
|
42
|
-
const multipleChoiceResult = detectMultipleChoicePrompt(output);
|
|
61
|
+
const multipleChoiceResult = detectMultipleChoicePrompt(output, options);
|
|
43
62
|
if (multipleChoiceResult.isPrompt) {
|
|
44
63
|
logger.info('detectPrompt:multipleChoice', {
|
|
45
64
|
isPrompt: true,
|
|
@@ -48,74 +67,30 @@ function detectPrompt(output) {
|
|
|
48
67
|
});
|
|
49
68
|
return multipleChoiceResult;
|
|
50
69
|
}
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const match2 = lastLines.match(yesNoDefaultPattern);
|
|
69
|
-
if (match2) {
|
|
70
|
-
return {
|
|
71
|
-
isPrompt: true,
|
|
72
|
-
promptData: {
|
|
73
|
-
type: 'yes_no',
|
|
74
|
-
question: match2[1].trim(),
|
|
75
|
-
options: ['yes', 'no'],
|
|
76
|
-
status: 'pending',
|
|
77
|
-
defaultOption: 'no',
|
|
78
|
-
},
|
|
79
|
-
cleanContent: match2[1].trim(),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
// Pattern 3: [Y/n] (Y is default)
|
|
83
|
-
const yesDefaultPattern = /^(.+)\s+\[Y\/n\]\s*$/m;
|
|
84
|
-
const match3 = lastLines.match(yesDefaultPattern);
|
|
85
|
-
if (match3) {
|
|
86
|
-
return {
|
|
87
|
-
isPrompt: true,
|
|
88
|
-
promptData: {
|
|
89
|
-
type: 'yes_no',
|
|
90
|
-
question: match3[1].trim(),
|
|
91
|
-
options: ['yes', 'no'],
|
|
92
|
-
status: 'pending',
|
|
93
|
-
defaultOption: 'yes',
|
|
94
|
-
},
|
|
95
|
-
cleanContent: match3[1].trim(),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
// Pattern 4: (yes/no)
|
|
99
|
-
const yesNoFullPattern = /^(.+)\s+\(yes\/no\)\s*$/m;
|
|
100
|
-
const match4 = lastLines.match(yesNoFullPattern);
|
|
101
|
-
if (match4) {
|
|
102
|
-
return {
|
|
103
|
-
isPrompt: true,
|
|
104
|
-
promptData: {
|
|
105
|
-
type: 'yes_no',
|
|
106
|
-
question: match4[1].trim(),
|
|
107
|
-
options: ['yes', 'no'],
|
|
108
|
-
status: 'pending',
|
|
109
|
-
},
|
|
110
|
-
cleanContent: match4[1].trim(),
|
|
111
|
-
};
|
|
70
|
+
// Patterns 1-4: Yes/no patterns (data-driven matching)
|
|
71
|
+
for (const pattern of YES_NO_PATTERNS) {
|
|
72
|
+
const match = lastLines.match(pattern.regex);
|
|
73
|
+
if (match) {
|
|
74
|
+
const question = match[1].trim();
|
|
75
|
+
return {
|
|
76
|
+
isPrompt: true,
|
|
77
|
+
promptData: {
|
|
78
|
+
type: 'yes_no',
|
|
79
|
+
question,
|
|
80
|
+
options: ['yes', 'no'],
|
|
81
|
+
status: 'pending',
|
|
82
|
+
...(pattern.defaultOption !== undefined && { defaultOption: pattern.defaultOption }),
|
|
83
|
+
},
|
|
84
|
+
cleanContent: question,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
112
87
|
}
|
|
113
88
|
// Pattern 5: Approve?
|
|
114
89
|
// Matches "Approve?" on its own line or at the end of a line
|
|
115
90
|
const approvePattern = /^(.*?)Approve\?\s*$/m;
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
const content =
|
|
91
|
+
const approveMatch = lastLines.match(approvePattern);
|
|
92
|
+
if (approveMatch) {
|
|
93
|
+
const content = approveMatch[1].trim();
|
|
119
94
|
// If there's content before "Approve?", include it in the question
|
|
120
95
|
const question = content ? `${content} Approve?` : 'Approve?';
|
|
121
96
|
return {
|
|
@@ -147,14 +122,175 @@ const TEXT_INPUT_PATTERNS = [
|
|
|
147
122
|
/custom/i,
|
|
148
123
|
/differently/i,
|
|
149
124
|
];
|
|
125
|
+
/**
|
|
126
|
+
* Pattern for ❯ (U+276F) indicator lines used by Claude CLI to mark the default selection.
|
|
127
|
+
* Used in Pass 1 (existence check) and Pass 2 (option collection) of the 2-pass detection.
|
|
128
|
+
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
129
|
+
*/
|
|
130
|
+
const DEFAULT_OPTION_PATTERN = /^\s*\u276F\s*(\d+)\.\s*(.+)$/;
|
|
131
|
+
/**
|
|
132
|
+
* Pattern for normal option lines (no ❯ indicator, just leading whitespace + number).
|
|
133
|
+
* Only applied in Pass 2 when ❯ indicator existence is confirmed by Pass 1.
|
|
134
|
+
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
135
|
+
*/
|
|
136
|
+
const NORMAL_OPTION_PATTERN = /^\s*(\d+)\.\s*(.+)$/;
|
|
137
|
+
/**
|
|
138
|
+
* Pattern for separator lines (horizontal rules).
|
|
139
|
+
* Matches lines consisting only of dash (-) or em-dash (─) characters.
|
|
140
|
+
* Used to skip separator lines in question extraction and non-option line handling.
|
|
141
|
+
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
142
|
+
*/
|
|
143
|
+
const SEPARATOR_LINE_PATTERN = /^[-─]+$/;
|
|
144
|
+
/**
|
|
145
|
+
* Creates a "no prompt detected" result.
|
|
146
|
+
* Centralizes the repeated pattern of returning isPrompt: false with trimmed content.
|
|
147
|
+
*
|
|
148
|
+
* @param output - The original output text
|
|
149
|
+
* @returns PromptDetectionResult with isPrompt: false
|
|
150
|
+
*/
|
|
151
|
+
function noPromptResult(output) {
|
|
152
|
+
return {
|
|
153
|
+
isPrompt: false,
|
|
154
|
+
cleanContent: output.trim(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Pattern for detecting question/selection keywords in question lines.
|
|
159
|
+
* CLI tools typically use these keywords in the line immediately before numbered choices.
|
|
160
|
+
*
|
|
161
|
+
* Keyword classification:
|
|
162
|
+
* [Observed] select, choose, pick, which, what, enter, confirm
|
|
163
|
+
* - Keywords confirmed in actual Claude Code / CLI tool prompts.
|
|
164
|
+
* [Defensive additions] how, where, type, specify, approve, accept, reject, decide, preference, option
|
|
165
|
+
* - Not yet observed in actual prompts, but commonly used in question sentences.
|
|
166
|
+
* Added defensively to reduce False Negative risk.
|
|
167
|
+
* - Slightly beyond YAGNI, but False Positive risk from these keywords is
|
|
168
|
+
* extremely low (they rarely appear in normal list headings).
|
|
169
|
+
* - Consider removing unused keywords if confirmed unnecessary in the future.
|
|
170
|
+
*
|
|
171
|
+
* No word boundaries (\b) used -- partial matches (e.g., "Selections:" matching "select")
|
|
172
|
+
* are acceptable because such headings followed by consecutive numbered lists are
|
|
173
|
+
* likely actual prompts. See design policy IC-004 for tradeoff analysis.
|
|
174
|
+
*
|
|
175
|
+
* Alternation-only pattern with no nested quantifiers -- ReDoS safe (SEC-S4-002).
|
|
176
|
+
* The pattern consists only of OR (alternation) within a non-capturing group,
|
|
177
|
+
* resulting in a linear-time structure (O(n)) with no backtracking risk.
|
|
178
|
+
* Follows the 'ReDoS safe (S4-001)' annotation convention of existing patterns.
|
|
179
|
+
*/
|
|
180
|
+
const QUESTION_KEYWORD_PATTERN = /(?:select|choose|pick|which|what|how|where|enter|type|specify|confirm|approve|accept|reject|decide|preference|option)/i;
|
|
181
|
+
/**
|
|
182
|
+
* Validates whether a question line actually asks a question or requests a selection.
|
|
183
|
+
* Distinguishes normal heading lines ("Recommendations:", "Steps:", etc.) from
|
|
184
|
+
* actual question lines ("Which option?", "Select a mode:", etc.).
|
|
185
|
+
*
|
|
186
|
+
* Control character resilience (SEC-S4-004): The line parameter is passed via
|
|
187
|
+
* lines[questionEndIndex]?.trim(), so residual control characters from tmux
|
|
188
|
+
* capture-pane output (8-bit CSI (0x9B), DEC private modes, etc. not fully
|
|
189
|
+
* removed by stripAnsi()) may be present. However, endsWith('?') / endsWith(':')
|
|
190
|
+
* inspect only the last character, and QUESTION_KEYWORD_PATTERN.test() matches
|
|
191
|
+
* only English letter keywords, so residual control characters will not match
|
|
192
|
+
* any pattern and the function returns false (false-safe).
|
|
193
|
+
*
|
|
194
|
+
* Full-width colon (U+FF1A) is intentionally not supported. Claude Code/CLI
|
|
195
|
+
* prompts use ASCII colon. See design policy IC-008.
|
|
196
|
+
*
|
|
197
|
+
* @param line - The line to validate (trimmed)
|
|
198
|
+
* @returns true if the line is a question/selection request, false otherwise
|
|
199
|
+
*/
|
|
200
|
+
function isQuestionLikeLine(line) {
|
|
201
|
+
// Empty lines are not questions
|
|
202
|
+
if (line.length === 0)
|
|
203
|
+
return false;
|
|
204
|
+
// Pattern 1: Lines ending with question mark (English or full-width Japanese)
|
|
205
|
+
// Full-width question mark (U+FF1F) support is a defensive measure: Claude Code/CLI
|
|
206
|
+
// displays questions in English, but this covers future multi-language support
|
|
207
|
+
// and third-party tool integration.
|
|
208
|
+
if (line.endsWith('?') || line.endsWith('\uff1f'))
|
|
209
|
+
return true;
|
|
210
|
+
// Pattern 2: Lines ending with colon that contain a selection/input keyword
|
|
211
|
+
// Examples: "Select an option:", "Choose a mode:", "Pick one:"
|
|
212
|
+
if (line.endsWith(':')) {
|
|
213
|
+
if (QUESTION_KEYWORD_PATTERN.test(line))
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Defensive check: protection against future unknown false positive patterns.
|
|
220
|
+
* Note: The actual false positive pattern in Issue #161 ("1. Create file\n2. Run tests")
|
|
221
|
+
* IS consecutive from 1, so this validation alone does not prevent it.
|
|
222
|
+
* The primary defense layers are: Layer 1 (thinking check in caller) + Layer 2 (2-pass
|
|
223
|
+
* cursor detection). This function provides Layer 3 defense against future unknown
|
|
224
|
+
* patterns with scattered/non-consecutive numbering.
|
|
225
|
+
*
|
|
226
|
+
* [S3-010] This validation assumes Claude CLI always uses consecutive numbering
|
|
227
|
+
* starting from 1. If in the future Claude CLI is observed to filter choices and
|
|
228
|
+
* output non-consecutive numbers (e.g., 1, 2, 4), consider relaxing this validation
|
|
229
|
+
* (e.g., only check starts-from-1, remove consecutive requirement).
|
|
230
|
+
*/
|
|
231
|
+
function isConsecutiveFromOne(numbers) {
|
|
232
|
+
if (numbers.length === 0)
|
|
233
|
+
return false;
|
|
234
|
+
if (numbers[0] !== 1)
|
|
235
|
+
return false;
|
|
236
|
+
for (let i = 1; i < numbers.length; i++) {
|
|
237
|
+
if (numbers[i] !== numbers[i - 1] + 1)
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Continuation line detection for multiline option text wrapping.
|
|
244
|
+
* Detects lines that are part of a previous option's text, wrapped due to terminal width.
|
|
245
|
+
*
|
|
246
|
+
* Called within detectMultipleChoicePrompt() Pass 2 reverse scan, only when
|
|
247
|
+
* options.length > 0 (at least one option already detected):
|
|
248
|
+
* const rawLine = lines[i]; // Original line with indentation preserved
|
|
249
|
+
* const line = lines[i].trim(); // Trimmed line
|
|
250
|
+
* if (options.length > 0 && line && !line.match(/^[-─]+$/)) {
|
|
251
|
+
* if (isContinuationLine(rawLine, line)) { continue; }
|
|
252
|
+
* }
|
|
253
|
+
*
|
|
254
|
+
* Each condition's responsibility:
|
|
255
|
+
* - hasLeadingSpaces: Indented non-option line (label text wrapping with indentation).
|
|
256
|
+
* Excludes lines ending with '?' to prevent question lines (e.g., " Do you want
|
|
257
|
+
* to proceed?") from being misclassified as continuation. Claude Bash tool outputs
|
|
258
|
+
* question and options with identical 2-space indentation, so this exclusion allows
|
|
259
|
+
* the question line to be recognized as questionEndIndex instead of being skipped.
|
|
260
|
+
* - isShortFragment: Short fragment (< 5 chars, e.g., filename tail)
|
|
261
|
+
* - isPathContinuation: Path string continuation (Issue #181)
|
|
262
|
+
*
|
|
263
|
+
* @param rawLine - Original line with indentation preserved (lines[i])
|
|
264
|
+
* @param line - Trimmed line (lines[i].trim())
|
|
265
|
+
* @returns true if the line should be treated as a continuation of a previous option
|
|
266
|
+
*/
|
|
267
|
+
function isContinuationLine(rawLine, line) {
|
|
268
|
+
// Indented non-option line.
|
|
269
|
+
// Excludes lines ending with '?' or '?' (U+FF1F) because those are typically question lines
|
|
270
|
+
// (e.g., " Do you want to proceed?", " コピーしたい対象はどれですか?") from CLI tool output
|
|
271
|
+
// where both the question and options are 2-space indented. Without this exclusion,
|
|
272
|
+
// the question line would be misclassified as a continuation line, causing
|
|
273
|
+
// questionEndIndex to remain -1 and Layer 5 SEC-001 to block detection.
|
|
274
|
+
const endsWithQuestion = line.endsWith('?') || line.endsWith('\uff1f');
|
|
275
|
+
const hasLeadingSpaces = rawLine.match(/^\s{2,}[^\d]/) && !rawLine.match(/^\s*\d+\./) && !endsWithQuestion;
|
|
276
|
+
// Short fragment (< 5 chars, excluding question-ending lines)
|
|
277
|
+
const isShortFragment = line.length < 5 && !endsWithQuestion;
|
|
278
|
+
// Path string continuation: lines starting with / or ~, or alphanumeric-only fragments (2+ chars)
|
|
279
|
+
const isPathContinuation = /^[\/~]/.test(line) || (line.length >= 2 && /^[a-zA-Z0-9_-]+$/.test(line));
|
|
280
|
+
return !!hasLeadingSpaces || isShortFragment || isPathContinuation;
|
|
281
|
+
}
|
|
150
282
|
/**
|
|
151
283
|
* Detect multiple choice prompts (numbered list with ❯ indicator)
|
|
152
284
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
285
|
+
* Uses a 2-pass detection approach (Issue #161):
|
|
286
|
+
* - Pass 1: Scan 50-line window for ❯ indicator lines (defaultOptionPattern).
|
|
287
|
+
* If no ❯ lines found, immediately return isPrompt: false.
|
|
288
|
+
* - Pass 2: Only if ❯ was found, re-scan collecting options using both
|
|
289
|
+
* defaultOptionPattern (isDefault=true) and normalOptionPattern (isDefault=false).
|
|
290
|
+
*
|
|
291
|
+
* This prevents normal numbered lists from being accumulated in the options array.
|
|
156
292
|
*
|
|
157
|
-
* Example:
|
|
293
|
+
* Example of valid prompt:
|
|
158
294
|
* Do you want to proceed?
|
|
159
295
|
* ❯ 1. Yes
|
|
160
296
|
* 2. No
|
|
@@ -163,38 +299,67 @@ const TEXT_INPUT_PATTERNS = [
|
|
|
163
299
|
* @param output - The tmux output to analyze (typically captured from tmux pane)
|
|
164
300
|
* @returns Detection result with prompt data if a valid multiple choice prompt is found
|
|
165
301
|
*/
|
|
166
|
-
function detectMultipleChoicePrompt(output) {
|
|
302
|
+
function detectMultipleChoicePrompt(output, options) {
|
|
303
|
+
// C-003: Use ?? true for readability instead of !== false double negation
|
|
304
|
+
const requireDefault = options?.requireDefaultIndicator ?? true;
|
|
167
305
|
const lines = output.split('\n');
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
306
|
+
// Strip trailing empty lines (tmux terminal padding) before computing scan window.
|
|
307
|
+
// tmux buffers often end with many empty padding lines that would shift the
|
|
308
|
+
// scan window away from the actual prompt content.
|
|
309
|
+
let effectiveEnd = lines.length;
|
|
310
|
+
while (effectiveEnd > 0 && lines[effectiveEnd - 1].trim() === '') {
|
|
311
|
+
effectiveEnd--;
|
|
312
|
+
}
|
|
313
|
+
// Calculate scan window: last 50 non-trailing-empty lines
|
|
314
|
+
const scanStart = Math.max(0, effectiveEnd - 50);
|
|
315
|
+
// ==========================================================================
|
|
316
|
+
// Pass 1: Check for ❯ indicator existence in scan window
|
|
317
|
+
// If no ❯ lines found and requireDefault is true, there is no multiple_choice prompt.
|
|
318
|
+
// When requireDefault is false, skip this gate entirely to allow ❯-less detection.
|
|
319
|
+
// ==========================================================================
|
|
320
|
+
if (requireDefault) {
|
|
321
|
+
let hasDefaultLine = false;
|
|
322
|
+
for (let i = scanStart; i < effectiveEnd; i++) {
|
|
323
|
+
const line = lines[i].trim();
|
|
324
|
+
if (DEFAULT_OPTION_PATTERN.test(line)) {
|
|
325
|
+
hasDefaultLine = true;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!hasDefaultLine) {
|
|
330
|
+
return noPromptResult(output);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ==========================================================================
|
|
334
|
+
// Pass 2: Collect options (executed when Pass 1 passes or is skipped)
|
|
335
|
+
// Scan from end to find options, using both patterns.
|
|
336
|
+
// ==========================================================================
|
|
337
|
+
const collectedOptions = [];
|
|
172
338
|
let questionEndIndex = -1;
|
|
173
|
-
let
|
|
174
|
-
// Scan from the end to find options
|
|
175
|
-
// Increased from 20 to 50 to handle multi-line wrapped options
|
|
176
|
-
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 50; i--) {
|
|
339
|
+
for (let i = effectiveEnd - 1; i >= scanStart; i--) {
|
|
177
340
|
const line = lines[i].trim();
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
341
|
+
// Try DEFAULT_OPTION_PATTERN first (❯ indicator)
|
|
342
|
+
const defaultMatch = line.match(DEFAULT_OPTION_PATTERN);
|
|
343
|
+
if (defaultMatch) {
|
|
344
|
+
const number = parseInt(defaultMatch[1], 10);
|
|
345
|
+
const label = defaultMatch[2].trim();
|
|
346
|
+
collectedOptions.unshift({ number, label, isDefault: true });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Try NORMAL_OPTION_PATTERN (no ❯ indicator)
|
|
350
|
+
const normalMatch = line.match(NORMAL_OPTION_PATTERN);
|
|
351
|
+
if (normalMatch) {
|
|
352
|
+
const number = parseInt(normalMatch[1], 10);
|
|
353
|
+
const label = normalMatch[2].trim();
|
|
354
|
+
collectedOptions.unshift({ number, label, isDefault: false });
|
|
355
|
+
continue;
|
|
189
356
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const isContinuationLine = hasLeadingSpaces || isShortFragment;
|
|
197
|
-
if (isContinuationLine) {
|
|
357
|
+
// Non-option line handling
|
|
358
|
+
if (collectedOptions.length > 0 && line && !SEPARATOR_LINE_PATTERN.test(line)) {
|
|
359
|
+
// Check if this is a continuation line (indented line between options,
|
|
360
|
+
// or path/filename fragments from terminal width wrapping - Issue #181)
|
|
361
|
+
const rawLine = lines[i]; // Original line with indentation preserved
|
|
362
|
+
if (isContinuationLine(rawLine, line)) {
|
|
198
363
|
// Skip continuation lines and continue scanning for more options
|
|
199
364
|
continue;
|
|
200
365
|
}
|
|
@@ -203,13 +368,34 @@ function detectMultipleChoicePrompt(output) {
|
|
|
203
368
|
break;
|
|
204
369
|
}
|
|
205
370
|
}
|
|
206
|
-
//
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
371
|
+
// Layer 3: Consecutive number validation (defensive measure)
|
|
372
|
+
const optionNumbers = collectedOptions.map(opt => opt.number);
|
|
373
|
+
if (!isConsecutiveFromOne(optionNumbers)) {
|
|
374
|
+
return noPromptResult(output);
|
|
375
|
+
}
|
|
376
|
+
// Layer 4: Must have at least 2 options. When requireDefault is true,
|
|
377
|
+
// also require at least one option with ❯ indicator.
|
|
378
|
+
const hasDefaultIndicator = collectedOptions.some(opt => opt.isDefault);
|
|
379
|
+
if (collectedOptions.length < 2 || (requireDefault && !hasDefaultIndicator)) {
|
|
380
|
+
return noPromptResult(output);
|
|
381
|
+
}
|
|
382
|
+
// Layer 5 [SEC-001]: Enhanced question line validation for requireDefaultIndicator=false.
|
|
383
|
+
// When requireDefault is false, apply stricter validation to prevent false positives
|
|
384
|
+
// from normal numbered lists (e.g., "Recommendations:\n1. Add tests\n2. Update docs").
|
|
385
|
+
if (!requireDefault) {
|
|
386
|
+
// SEC-001a: No question line found (questionEndIndex === -1) - reject.
|
|
387
|
+
// Prevents generic question fallback from triggering Auto-Yes
|
|
388
|
+
// on plain numbered lists that happen to be consecutive from 1.
|
|
389
|
+
if (questionEndIndex === -1) {
|
|
390
|
+
return noPromptResult(output);
|
|
391
|
+
}
|
|
392
|
+
// SEC-001b: Question line exists but is not actually a question/selection request.
|
|
393
|
+
// Validates that the question line contains a question mark or a selection keyword
|
|
394
|
+
// with colon, distinguishing "Select an option:" from "Recommendations:".
|
|
395
|
+
const questionLine = lines[questionEndIndex]?.trim() ?? '';
|
|
396
|
+
if (!isQuestionLikeLine(questionLine)) {
|
|
397
|
+
return noPromptResult(output);
|
|
398
|
+
}
|
|
213
399
|
}
|
|
214
400
|
// Extract question text
|
|
215
401
|
let question = '';
|
|
@@ -218,7 +404,7 @@ function detectMultipleChoicePrompt(output) {
|
|
|
218
404
|
const questionLines = [];
|
|
219
405
|
for (let i = Math.max(0, questionEndIndex - 5); i <= questionEndIndex; i++) {
|
|
220
406
|
const line = lines[i].trim();
|
|
221
|
-
if (line && !
|
|
407
|
+
if (line && !SEPARATOR_LINE_PATTERN.test(line)) {
|
|
222
408
|
questionLines.push(line);
|
|
223
409
|
}
|
|
224
410
|
}
|
|
@@ -233,7 +419,7 @@ function detectMultipleChoicePrompt(output) {
|
|
|
233
419
|
promptData: {
|
|
234
420
|
type: 'multiple_choice',
|
|
235
421
|
question: question.trim(),
|
|
236
|
-
options:
|
|
422
|
+
options: collectedOptions.map(opt => {
|
|
237
423
|
// Check if this option requires text input using module-level patterns
|
|
238
424
|
const requiresTextInput = TEXT_INPUT_PATTERNS.some(pattern => pattern.test(opt.label));
|
|
239
425
|
return {
|
|
@@ -272,7 +458,8 @@ function getAnswerInput(answer, promptType = 'yes_no') {
|
|
|
272
458
|
if (/^\d+$/.test(normalized)) {
|
|
273
459
|
return normalized;
|
|
274
460
|
}
|
|
275
|
-
|
|
461
|
+
// SEC-003: Fixed error message without user input to prevent log injection
|
|
462
|
+
throw new Error('Invalid answer for multiple choice prompt. Expected a number.');
|
|
276
463
|
}
|
|
277
464
|
// Handle yes/no prompts
|
|
278
465
|
if (normalized === 'yes' || normalized === 'y') {
|
|
@@ -281,5 +468,6 @@ function getAnswerInput(answer, promptType = 'yes_no') {
|
|
|
281
468
|
if (normalized === 'no' || normalized === 'n') {
|
|
282
469
|
return 'n';
|
|
283
470
|
}
|
|
284
|
-
|
|
471
|
+
// SEC-003: Fixed error message without user input to prevent log injection
|
|
472
|
+
throw new Error("Invalid answer for yes/no prompt. Expected 'yes', 'no', 'y', or 'n'.");
|
|
285
473
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* CLI Tool response polling
|
|
4
|
-
* Periodically checks tmux sessions for CLI tool responses (Claude, Codex, Gemini)
|
|
3
|
+
* CLI Tool response polling.
|
|
4
|
+
* Periodically checks tmux sessions for CLI tool responses (Claude, Codex, Gemini).
|
|
5
|
+
*
|
|
6
|
+
* Key responsibilities:
|
|
7
|
+
* - Extract completed responses from tmux output (extractResponse)
|
|
8
|
+
* - Detect interactive prompts and save as prompt messages
|
|
9
|
+
* - Clean tool-specific artifacts from response content
|
|
10
|
+
* - Manage polling lifecycle (start/stop/timeout)
|
|
11
|
+
*
|
|
12
|
+
* Issue #188 improvements:
|
|
13
|
+
* - DR-004: Tail-line windowing for thinking detection in extractResponse
|
|
14
|
+
* - MF-001 fix: Same windowing applied to checkForResponse thinking check
|
|
15
|
+
* - SF-003: RESPONSE_THINKING_TAIL_LINE_COUNT constant tracks STATUS_THINKING_LINE_COUNT
|
|
5
16
|
*/
|
|
6
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
18
|
exports.cleanClaudeResponse = cleanClaudeResponse;
|
|
@@ -26,6 +37,22 @@ const POLLING_INTERVAL = 2000;
|
|
|
26
37
|
* Maximum polling duration in milliseconds (default: 5 minutes)
|
|
27
38
|
*/
|
|
28
39
|
const MAX_POLLING_DURATION = 5 * 60 * 1000;
|
|
40
|
+
/**
|
|
41
|
+
* Number of tail lines to check for active thinking indicators in response extraction.
|
|
42
|
+
*
|
|
43
|
+
* SF-003 coupling note: This value must track STATUS_THINKING_LINE_COUNT (5) in
|
|
44
|
+
* status-detector.ts. Both constants exist because they serve separate modules:
|
|
45
|
+
* - RESPONSE_THINKING_TAIL_LINE_COUNT: response-poller.ts (response extraction)
|
|
46
|
+
* - STATUS_THINKING_LINE_COUNT: status-detector.ts (UI status display)
|
|
47
|
+
* If STATUS_THINKING_LINE_COUNT changes, update this value accordingly.
|
|
48
|
+
*
|
|
49
|
+
* Why not a shared constant? status-detector.ts has no dependency on response-poller.ts
|
|
50
|
+
* and vice versa. Introducing a shared module would create a coupling that does not
|
|
51
|
+
* currently exist. The test suite validates consistency (SF-003 test).
|
|
52
|
+
*
|
|
53
|
+
* @constant
|
|
54
|
+
*/
|
|
55
|
+
const RESPONSE_THINKING_TAIL_LINE_COUNT = 5;
|
|
29
56
|
/**
|
|
30
57
|
* Active pollers map: "worktreeId:cliToolId" -> NodeJS.Timeout
|
|
31
58
|
*/
|
|
@@ -40,6 +67,26 @@ const pollingStartTimes = new Map();
|
|
|
40
67
|
function getPollerKey(worktreeId, cliToolId) {
|
|
41
68
|
return `${worktreeId}:${cliToolId}`;
|
|
42
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Internal helper: detect prompt with CLI-tool-specific options.
|
|
72
|
+
*
|
|
73
|
+
* Centralizes the stripAnsi() + buildDetectPromptOptions() + detectPrompt() pipeline
|
|
74
|
+
* to avoid repeating this 3-step sequence across extractResponse() and checkForResponse().
|
|
75
|
+
*
|
|
76
|
+
* Design notes:
|
|
77
|
+
* - IA-001: stripAnsi() is applied uniformly inside this helper. It is idempotent,
|
|
78
|
+
* so double-application on already-stripped input is safe.
|
|
79
|
+
* - SF-003: Uses buildDetectPromptOptions() from cli-patterns.ts for tool-specific
|
|
80
|
+
* configuration (e.g., Claude's requireDefaultIndicator=false for Issue #193).
|
|
81
|
+
*
|
|
82
|
+
* @param output - Raw or pre-stripped tmux output
|
|
83
|
+
* @param cliToolId - CLI tool identifier for building detection options
|
|
84
|
+
* @returns PromptDetectionResult with isPrompt, promptData, and cleanContent
|
|
85
|
+
*/
|
|
86
|
+
function detectPromptWithOptions(output, cliToolId) {
|
|
87
|
+
const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
|
|
88
|
+
return (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripAnsi)(output), promptOptions);
|
|
89
|
+
}
|
|
43
90
|
/**
|
|
44
91
|
* Clean up Claude response by removing shell setup commands, environment exports, ANSI codes, and banner
|
|
45
92
|
* Also extracts only the LATEST response to avoid including conversation history
|
|
@@ -209,14 +256,12 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
209
256
|
// Permission prompts appear after normal responses and need special handling
|
|
210
257
|
if (cliToolId === 'claude') {
|
|
211
258
|
const fullOutput = lines.join('\n');
|
|
212
|
-
|
|
213
|
-
const cleanFullOutput = (0, cli_patterns_1.stripAnsi)(fullOutput);
|
|
214
|
-
const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanFullOutput);
|
|
259
|
+
const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
|
|
215
260
|
if (promptDetection.isPrompt) {
|
|
216
261
|
// Return the full output as a complete interactive prompt
|
|
217
262
|
// Use the cleaned output without ANSI codes
|
|
218
263
|
return {
|
|
219
|
-
response:
|
|
264
|
+
response: (0, cli_patterns_1.stripAnsi)(fullOutput),
|
|
220
265
|
isComplete: true,
|
|
221
266
|
lineCount: totalLines,
|
|
222
267
|
};
|
|
@@ -285,9 +330,10 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
285
330
|
responseLines.push(line);
|
|
286
331
|
}
|
|
287
332
|
const response = responseLines.join('\n').trim();
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
333
|
+
// DR-004: Check only the tail of the response for thinking indicators.
|
|
334
|
+
// Prevents false blocking when completed thinking summaries appear in the response body.
|
|
335
|
+
const responseTailLines = response.split('\n').slice(-RESPONSE_THINKING_TAIL_LINE_COUNT).join('\n');
|
|
336
|
+
if (thinkingPattern.test(responseTailLines)) {
|
|
291
337
|
return {
|
|
292
338
|
response: '',
|
|
293
339
|
isComplete: false,
|
|
@@ -347,18 +393,13 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
347
393
|
lineCount: totalLines,
|
|
348
394
|
};
|
|
349
395
|
}
|
|
350
|
-
// Check for auth/loading states that should not be treated as complete responses
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
response.includes('⠴') ||
|
|
358
|
-
response.includes('⠦') ||
|
|
359
|
-
response.includes('⠧') ||
|
|
360
|
-
response.includes('⠇') ||
|
|
361
|
-
response.includes('⠏')) {
|
|
396
|
+
// Check for auth/loading states that should not be treated as complete responses.
|
|
397
|
+
// Braille spinner characters are shared with CLAUDE_SPINNER_CHARS in cli-patterns.ts.
|
|
398
|
+
const LOADING_INDICATORS = [
|
|
399
|
+
'Waiting for auth',
|
|
400
|
+
'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏',
|
|
401
|
+
];
|
|
402
|
+
if (LOADING_INDICATORS.some(indicator => response.includes(indicator))) {
|
|
362
403
|
return {
|
|
363
404
|
response: '',
|
|
364
405
|
isComplete: false,
|
|
@@ -382,7 +423,7 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
382
423
|
// Check if this is an interactive prompt (yes/no or multiple choice)
|
|
383
424
|
// Interactive prompts don't have the ">" prompt and separator, so we need to detect them separately
|
|
384
425
|
const fullOutput = lines.join('\n');
|
|
385
|
-
const promptDetection = (
|
|
426
|
+
const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
|
|
386
427
|
if (promptDetection.isPrompt) {
|
|
387
428
|
// This is an interactive prompt - consider it complete
|
|
388
429
|
return {
|
|
@@ -454,12 +495,17 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
454
495
|
// Extract response
|
|
455
496
|
const result = extractResponse(output, lastCapturedLine, cliToolId);
|
|
456
497
|
if (!result || !result.isComplete) {
|
|
457
|
-
// No new output or response not yet complete
|
|
458
|
-
//
|
|
459
|
-
// This handles cases where user responded to prompts directly via terminal
|
|
498
|
+
// No new output or response not yet complete.
|
|
499
|
+
// If CLI tool is actively thinking, mark any pending prompts as answered.
|
|
500
|
+
// This handles cases where user responded to prompts directly via terminal.
|
|
501
|
+
//
|
|
502
|
+
// DR-004 windowing: Only check tail lines (same as extractResponse thinking check)
|
|
503
|
+
// to avoid false matches on completed thinking summaries in scrollback.
|
|
504
|
+
// Previously (MF-001), full-text check caused false positives.
|
|
460
505
|
const { thinkingPattern } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
|
|
461
506
|
const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
|
|
462
|
-
|
|
507
|
+
const tailLines = cleanOutput.split('\n').slice(-RESPONSE_THINKING_TAIL_LINE_COUNT).join('\n');
|
|
508
|
+
if (thinkingPattern.test(tailLines)) {
|
|
463
509
|
const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId);
|
|
464
510
|
if (answeredCount > 0) {
|
|
465
511
|
console.log(`Marked ${answeredCount} pending prompt(s) as answered (thinking detected) for ${worktreeId}`);
|
|
@@ -479,7 +525,7 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
479
525
|
return false;
|
|
480
526
|
}
|
|
481
527
|
// Response is complete! Check if it's a prompt
|
|
482
|
-
const promptDetection = (
|
|
528
|
+
const promptDetection = detectPromptWithOptions(result.response, cliToolId);
|
|
483
529
|
if (promptDetection.isPrompt) {
|
|
484
530
|
// This is a prompt - save as prompt message
|
|
485
531
|
(0, db_1.clearInProgressMessageId)(db, worktreeId, cliToolId);
|