crewly 1.2.0 → 1.2.3
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/config/constants.ts +5 -0
- package/config/roles/architect/prompt.md +7 -5
- package/config/roles/backend-developer/prompt.md +8 -4
- package/config/roles/content-strategist/prompt.md +12 -4
- package/config/roles/designer/prompt.md +8 -4
- package/config/roles/developer/prompt.md +7 -4
- package/config/roles/frontend-developer/prompt.md +8 -4
- package/config/roles/fullstack-dev/prompt.md +8 -4
- package/config/roles/generalist/prompt.md +7 -4
- package/config/roles/ops/prompt.md +7 -4
- package/config/roles/orchestrator/prompt.md +22 -1
- package/config/roles/product-manager/prompt.md +8 -4
- package/config/roles/qa/prompt.md +8 -4
- package/config/roles/qa-engineer/prompt.md +8 -4
- package/config/roles/sales/prompt.md +8 -4
- package/config/roles/support/prompt.md +8 -4
- package/config/roles/tpm/prompt.md +8 -4
- package/config/skills/orchestrator/delegate-task/execute.sh +7 -5
- package/config/skills/orchestrator/delegate-task/instructions.md +22 -17
- package/config/templates/agent-claude-md.md +10 -5
- package/dist/backend/backend/src/constants.d.ts +18 -63
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +17 -68
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +39 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +1 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +2 -13
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +105 -130
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +8 -8
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts +30 -27
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js +128 -53
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +47 -10
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts +35 -3
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +123 -25
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts +6 -0
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +25 -3
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js +29 -2
- package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/backend/backend/src/utils/terminal-output.utils.d.ts +2 -1
- package/dist/backend/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/terminal-output.utils.js +2 -28
- package/dist/backend/backend/src/utils/terminal-output.utils.js.map +1 -1
- package/dist/backend/backend/src/utils/terminal-string-ops.d.ts +183 -0
- package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/terminal-string-ops.js +717 -0
- package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -0
- package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.js +22 -27
- package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
- package/dist/backend/config/constants.d.ts +5 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +5 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +18 -63
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +17 -68
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/utils/terminal-output.utils.d.ts +2 -1
- package/dist/cli/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
- package/dist/cli/backend/src/utils/terminal-output.utils.js +2 -28
- package/dist/cli/backend/src/utils/terminal-output.utils.js.map +1 -1
- package/dist/cli/backend/src/utils/terminal-string-ops.d.ts +183 -0
- package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/terminal-string-ops.js +717 -0
- package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -0
- package/dist/cli/config/constants.d.ts +5 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +5 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/package.json +2 -1
|
@@ -14,6 +14,8 @@ import { ContextWindowMonitorService } from './context-window-monitor.service.js
|
|
|
14
14
|
import { SubAgentMessageQueue } from '../messaging/sub-agent-message-queue.service.js';
|
|
15
15
|
import { AgentSuspendService } from './agent-suspend.service.js';
|
|
16
16
|
import { stripAnsiCodes } from '../../utils/terminal-output.utils.js';
|
|
17
|
+
import { isPromptLine, containsSpinnerOrWorkingIndicator, containsProcessingIndicator, containsBusyStatusBar, containsRewindMode, containsGeminiProcessingKeywords, extractChatPrefix, stripTuiLineBorders, matchTuiPromptLine, } from '../../utils/terminal-string-ops.js';
|
|
18
|
+
import { PtyActivityTrackerService } from './pty-activity-tracker.service.js';
|
|
17
19
|
/**
|
|
18
20
|
* Service responsible for the complex, multi-step process of agent initialization and registration.
|
|
19
21
|
* Isolates the complex state management of agent startup with progressive escalation.
|
|
@@ -47,16 +49,10 @@ export class AgentRegistrationService {
|
|
|
47
49
|
// which causes multiple Ctrl+C presses that can crash the runtime.
|
|
48
50
|
sessionDeliveryMutex = new Map();
|
|
49
51
|
// Terminal patterns are now centralized in TERMINAL_PATTERNS constant
|
|
50
|
-
// Keeping
|
|
52
|
+
// Keeping prompt chars as static getter for backwards compatibility within the class
|
|
51
53
|
static get CLAUDE_PROMPT_INDICATORS() {
|
|
52
54
|
return TERMINAL_PATTERNS.PROMPT_CHARS;
|
|
53
55
|
}
|
|
54
|
-
static get CLAUDE_PROMPT_STREAM_PATTERN() {
|
|
55
|
-
return TERMINAL_PATTERNS.PROMPT_STREAM;
|
|
56
|
-
}
|
|
57
|
-
static get CLAUDE_PROCESSING_INDICATORS() {
|
|
58
|
-
return TERMINAL_PATTERNS.PROCESSING_INDICATORS;
|
|
59
|
-
}
|
|
60
56
|
constructor(_legacyTmuxService, // Legacy parameter for backwards compatibility
|
|
61
57
|
projectRoot, storageService) {
|
|
62
58
|
this.logger = LoggerService.getInstance().createComponentLogger('AgentRegistrationService');
|
|
@@ -1508,6 +1504,14 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
1508
1504
|
await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_SESSION_NAME, sessionName);
|
|
1509
1505
|
await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_ROLE, role);
|
|
1510
1506
|
await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_API_URL, `http://localhost:${WEB_CONSTANTS.PORTS.BACKEND}`);
|
|
1507
|
+
// Pass Gemini API key to gemini-cli agents so they authenticate
|
|
1508
|
+
// with the paid API key instead of the free-tier Google login.
|
|
1509
|
+
if (runtimeType === RUNTIME_TYPES.GEMINI_CLI) {
|
|
1510
|
+
const geminiApiKey = process.env[ENV_CONSTANTS.GEMINI_API_KEY];
|
|
1511
|
+
if (geminiApiKey) {
|
|
1512
|
+
await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.GEMINI_API_KEY, geminiApiKey);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1511
1515
|
this.logger.info('Agent session created and environment variables set, initializing with registration', {
|
|
1512
1516
|
sessionName,
|
|
1513
1517
|
role,
|
|
@@ -1699,9 +1703,16 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
1699
1703
|
// Use robust message delivery with proper waiting mechanism
|
|
1700
1704
|
const delivered = await this.sendMessageWithRetry(sessionName, message, 3, runtimeType);
|
|
1701
1705
|
if (!delivered) {
|
|
1706
|
+
// Check if the agent is actively processing (busy) — the queue
|
|
1707
|
+
// processor can re-queue instead of permanently failing the message.
|
|
1708
|
+
// Use PTY idle time instead of regex patterns for robust cross-runtime detection.
|
|
1709
|
+
const idleMs = PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName);
|
|
1710
|
+
const isBusy = idleMs < SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
|
|
1702
1711
|
return {
|
|
1703
1712
|
success: false,
|
|
1704
|
-
error:
|
|
1713
|
+
error: isBusy
|
|
1714
|
+
? '[AGENT_BUSY] Failed to deliver message — agent is actively processing'
|
|
1715
|
+
: 'Failed to deliver message after multiple attempts',
|
|
1705
1716
|
};
|
|
1706
1717
|
}
|
|
1707
1718
|
this.logger.info('Message sent to agent successfully', {
|
|
@@ -1761,9 +1772,6 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
1761
1772
|
if (!session) {
|
|
1762
1773
|
return false;
|
|
1763
1774
|
}
|
|
1764
|
-
// Use runtime-specific pattern for stream detection to avoid false positives
|
|
1765
|
-
// (e.g. Gemini's `> ` pattern matching markdown blockquotes in Claude Code output)
|
|
1766
|
-
const streamPattern = this.getPromptPatternForRuntime(runtimeType);
|
|
1767
1775
|
return new Promise((resolve) => {
|
|
1768
1776
|
let resolved = false;
|
|
1769
1777
|
const pollInterval = EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL;
|
|
@@ -1810,9 +1818,11 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
1810
1818
|
if (resolved)
|
|
1811
1819
|
return;
|
|
1812
1820
|
// Strip ANSI escape sequences before testing — raw PTY data contains
|
|
1813
|
-
// cursor positioning, color codes, etc. that break
|
|
1821
|
+
// cursor positioning, color codes, etc. that break pattern matching (#106)
|
|
1814
1822
|
const cleanData = stripAnsiCodes(data);
|
|
1815
|
-
|
|
1823
|
+
// Check each line for prompt pattern (string-based, no regex)
|
|
1824
|
+
const hasPromptInStream = cleanData.split('\n').some(line => line.trim().length > 0 && isPromptLine(line, runtimeType));
|
|
1825
|
+
if (hasPromptInStream) {
|
|
1816
1826
|
// Double-check with capturePane to avoid false positives from partial data
|
|
1817
1827
|
const output = sessionHelper.capturePane(sessionName);
|
|
1818
1828
|
if (this.isClaudeAtPrompt(output, runtimeType)) {
|
|
@@ -2045,7 +2055,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2045
2055
|
}
|
|
2046
2056
|
// Phase 1: Wait for Claude to be at prompt before sending
|
|
2047
2057
|
if (!messageSent) {
|
|
2048
|
-
const isAtPrompt =
|
|
2058
|
+
const isAtPrompt = buffer.split('\n').some(line => line.trim().length > 0 && isPromptLine(line));
|
|
2049
2059
|
if (isAtPrompt) {
|
|
2050
2060
|
sendMessageNow();
|
|
2051
2061
|
}
|
|
@@ -2056,9 +2066,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2056
2066
|
return; // Wait for Enter to be sent
|
|
2057
2067
|
}
|
|
2058
2068
|
// Look for processing indicators confirming delivery
|
|
2059
|
-
const hasProcessingIndicator =
|
|
2069
|
+
const hasProcessingIndicator = containsProcessingIndicator(buffer);
|
|
2060
2070
|
// Also check if prompt disappeared (Claude is working)
|
|
2061
|
-
const promptStillVisible =
|
|
2071
|
+
const promptStillVisible = buffer.split('\n').some(line => line.trim().length > 0 && isPromptLine(line));
|
|
2062
2072
|
// Use constant for minimum buffer check (P3.2 fix)
|
|
2063
2073
|
if (hasProcessingIndicator || (!promptStillVisible && buffer.length > EVENT_DELIVERY_CONSTANTS.MIN_BUFFER_FOR_PROCESSING_DETECTION)) {
|
|
2064
2074
|
this.logger.debug('Message delivery confirmed (event-driven)', {
|
|
@@ -2107,12 +2117,10 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2107
2117
|
if (!this.isClaudeAtPrompt(output, runtimeType)) {
|
|
2108
2118
|
if (attempt === maxAttempts) {
|
|
2109
2119
|
// On the final attempt, check if the agent is DEFINITELY busy
|
|
2110
|
-
// before force-delivering.
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
const
|
|
2114
|
-
const isBusy = TERMINAL_PATTERNS.BUSY_STATUS_BAR.test(tailForBusyCheck) ||
|
|
2115
|
-
TERMINAL_PATTERNS.PROCESSING_WITH_TEXT.test(tailForBusyCheck);
|
|
2120
|
+
// before force-delivering. Use PTY idle time for robust
|
|
2121
|
+
// cross-runtime detection instead of fragile regex patterns.
|
|
2122
|
+
const idleMs = PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName);
|
|
2123
|
+
const isBusy = idleMs < SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
|
|
2116
2124
|
if (isBusy) {
|
|
2117
2125
|
this.logger.warn('Agent is busy (processing indicators detected), skipping force delivery', {
|
|
2118
2126
|
sessionName,
|
|
@@ -2175,8 +2183,30 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2175
2183
|
// for focus cycling and overlay dismissal.
|
|
2176
2184
|
if (isClaudeCode) {
|
|
2177
2185
|
if (attempt > 1) {
|
|
2186
|
+
// On retry: Ctrl+C to cancel stale input, then PTY resize to force
|
|
2187
|
+
// TUI re-render (SIGWINCH), then Tab to cycle Ink focus.
|
|
2178
2188
|
await sessionHelper.sendCtrlC(sessionName);
|
|
2179
2189
|
await delay(300);
|
|
2190
|
+
try {
|
|
2191
|
+
const session = sessionHelper.getSession(sessionName);
|
|
2192
|
+
if (session) {
|
|
2193
|
+
session.resize(81, 25);
|
|
2194
|
+
await delay(200);
|
|
2195
|
+
session.resize(80, 24);
|
|
2196
|
+
await delay(300);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
catch { /* non-fatal */ }
|
|
2200
|
+
await sessionHelper.sendKey(sessionName, 'Tab');
|
|
2201
|
+
await delay(300);
|
|
2202
|
+
}
|
|
2203
|
+
else {
|
|
2204
|
+
// First attempt: lightweight focus nudge — Tab key cycles Ink's
|
|
2205
|
+
// focusNext() to ensure the InputPrompt is active. This prevents
|
|
2206
|
+
// the "write succeeds but TUI ignores it" failure mode without
|
|
2207
|
+
// the overhead of Ctrl+C or PTY resize.
|
|
2208
|
+
await sessionHelper.sendKey(sessionName, 'Tab');
|
|
2209
|
+
await delay(200);
|
|
2180
2210
|
}
|
|
2181
2211
|
}
|
|
2182
2212
|
else {
|
|
@@ -2311,7 +2341,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2311
2341
|
// definitive proof the agent accepted and is working.
|
|
2312
2342
|
// Only check spinner/⏺ chars — NOT text words like
|
|
2313
2343
|
// "thinking" which appear in historical response text.
|
|
2314
|
-
if (
|
|
2344
|
+
if (containsSpinnerOrWorkingIndicator(currentOutput)) {
|
|
2315
2345
|
this.logger.debug('Processing indicators detected — message accepted', {
|
|
2316
2346
|
sessionName,
|
|
2317
2347
|
attempt,
|
|
@@ -2346,7 +2376,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2346
2376
|
await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
|
|
2347
2377
|
// Verify recovery
|
|
2348
2378
|
const postEnterOutput = sessionHelper.capturePane(sessionName);
|
|
2349
|
-
if (
|
|
2379
|
+
if (containsSpinnerOrWorkingIndicator(postEnterOutput) ||
|
|
2350
2380
|
!this.isClaudeAtPrompt(postEnterOutput, runtimeType)) {
|
|
2351
2381
|
this.logger.info('Enter recovery from prompt successful', {
|
|
2352
2382
|
sessionName,
|
|
@@ -2393,7 +2423,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2393
2423
|
await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
|
|
2394
2424
|
// Verify recovery: check if processing started
|
|
2395
2425
|
const recoveryOutput = sessionHelper.capturePane(sessionName);
|
|
2396
|
-
if (
|
|
2426
|
+
if (containsSpinnerOrWorkingIndicator(recoveryOutput)) {
|
|
2397
2427
|
this.logger.info('Enter recovery successful — processing started', {
|
|
2398
2428
|
sessionName,
|
|
2399
2429
|
attempt,
|
|
@@ -2480,9 +2510,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2480
2510
|
.filter((line) => !beforeLines.has(line))
|
|
2481
2511
|
.join('\n');
|
|
2482
2512
|
}
|
|
2483
|
-
const hasProcessingIndicators =
|
|
2513
|
+
const hasProcessingIndicators = containsProcessingIndicator(newContent || afterOutput.slice(-500));
|
|
2484
2514
|
const hasGeminiIndicators = newContent.length > 0
|
|
2485
|
-
&&
|
|
2515
|
+
&& containsGeminiProcessingKeywords(newContent);
|
|
2486
2516
|
const significantLengthChange = Math.abs(lengthDiff) > 10;
|
|
2487
2517
|
// For Gemini CLI, contentChanged alone is sufficient evidence of
|
|
2488
2518
|
// delivery. The TUI redraws minimally (lengthDiff can be as low as
|
|
@@ -2561,19 +2591,11 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2561
2591
|
}
|
|
2562
2592
|
}
|
|
2563
2593
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
if (childAlive !== false) {
|
|
2570
|
-
this.logger.warn('Message delivery verification inconclusive but session alive — assuming success', {
|
|
2571
|
-
sessionName,
|
|
2572
|
-
maxAttempts,
|
|
2573
|
-
messageLength: message.length,
|
|
2574
|
-
});
|
|
2575
|
-
return true;
|
|
2576
|
-
}
|
|
2594
|
+
this.logger.warn('Message delivery verification failed — all attempts exhausted', {
|
|
2595
|
+
sessionName,
|
|
2596
|
+
maxAttempts,
|
|
2597
|
+
messageLength: message.length,
|
|
2598
|
+
});
|
|
2577
2599
|
this.logger.error('Message delivery failed after all retry attempts', {
|
|
2578
2600
|
sessionName,
|
|
2579
2601
|
maxAttempts,
|
|
@@ -2601,13 +2623,13 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2601
2623
|
}
|
|
2602
2624
|
// Extract a search token from the message:
|
|
2603
2625
|
// Strip [CHAT:uuid] prefix if present, then take the first 40 chars
|
|
2604
|
-
const
|
|
2605
|
-
const contentAfterPrefix =
|
|
2606
|
-
? message.slice(
|
|
2626
|
+
const { prefixLength } = extractChatPrefix(message);
|
|
2627
|
+
const contentAfterPrefix = prefixLength > 0
|
|
2628
|
+
? message.slice(prefixLength)
|
|
2607
2629
|
: message;
|
|
2608
2630
|
const searchToken = contentAfterPrefix.slice(0, 40).trim();
|
|
2609
2631
|
// Also use [CHAT: as a secondary token if message has a CHAT prefix
|
|
2610
|
-
const chatToken =
|
|
2632
|
+
const chatToken = prefixLength > 0 ? '[CHAT:' : null;
|
|
2611
2633
|
// Check last 20 non-empty lines for either token.
|
|
2612
2634
|
// Gemini CLI TUI has status bars at the bottom (branch, sandbox, model info)
|
|
2613
2635
|
// that push input content further up. 5 lines was insufficient.
|
|
@@ -2615,7 +2637,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2615
2637
|
const linesToCheck = lines.slice(-20);
|
|
2616
2638
|
const isStuck = linesToCheck.some((line) => {
|
|
2617
2639
|
// Strip TUI box-drawing borders before checking (Gemini CLI wraps content in │...│)
|
|
2618
|
-
const stripped = line
|
|
2640
|
+
const stripped = stripTuiLineBorders(line);
|
|
2619
2641
|
if (searchToken && (line.includes(searchToken) || stripped.includes(searchToken)))
|
|
2620
2642
|
return true;
|
|
2621
2643
|
if (chatToken && (line.includes(chatToken) || stripped.includes(chatToken)))
|
|
@@ -2665,9 +2687,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2665
2687
|
return false;
|
|
2666
2688
|
// Extract search token: first 30 chars of the message content
|
|
2667
2689
|
// (after stripping any [CHAT:uuid] prefix)
|
|
2668
|
-
const
|
|
2669
|
-
const contentAfterPrefix =
|
|
2670
|
-
? message.slice(
|
|
2690
|
+
const { prefixLength } = extractChatPrefix(message);
|
|
2691
|
+
const contentAfterPrefix = prefixLength > 0
|
|
2692
|
+
? message.slice(prefixLength)
|
|
2671
2693
|
: message;
|
|
2672
2694
|
const searchToken = contentAfterPrefix.slice(0, 30).trim();
|
|
2673
2695
|
if (!searchToken)
|
|
@@ -2677,14 +2699,13 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2677
2699
|
// Or without borders: > text
|
|
2678
2700
|
// The prompt line is the line with `> ` followed by actual content.
|
|
2679
2701
|
const lines = output.split('\n');
|
|
2680
|
-
const promptLineRegex = /^[│┃║|\s]*>\s+(.+)/;
|
|
2681
2702
|
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
|
|
2682
2703
|
const line = lines[i];
|
|
2683
|
-
const
|
|
2684
|
-
if (
|
|
2685
|
-
const
|
|
2704
|
+
const promptContent = matchTuiPromptLine(line);
|
|
2705
|
+
if (promptContent !== null) {
|
|
2706
|
+
const trimmedContent = stripTuiLineBorders(promptContent).trim();
|
|
2686
2707
|
// Check if the prompt line content contains our message text
|
|
2687
|
-
if (
|
|
2708
|
+
if (trimmedContent.length > 5 && trimmedContent.includes(searchToken)) {
|
|
2688
2709
|
this.logger.warn('Text stuck at TUI prompt — Enter was not pressed', {
|
|
2689
2710
|
sessionName,
|
|
2690
2711
|
searchToken: searchToken.slice(0, 20),
|
|
@@ -2784,6 +2805,22 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2784
2805
|
const sessionHelper = this._sessionHelper;
|
|
2785
2806
|
if (!sessionHelper)
|
|
2786
2807
|
return;
|
|
2808
|
+
// --- Part 0: Rewind mode detection (all sessions) ---
|
|
2809
|
+
// If Claude Code is stuck in Rewind mode (triggered by ESC during
|
|
2810
|
+
// processing), send 'q' to exit before any other recovery attempts.
|
|
2811
|
+
for (const sessionName of sessionHelper.listSessions()) {
|
|
2812
|
+
try {
|
|
2813
|
+
const output = sessionHelper.capturePane(sessionName);
|
|
2814
|
+
if (containsRewindMode(output)) {
|
|
2815
|
+
this.logger.warn('Rewind mode detected, sending q to exit', { sessionName });
|
|
2816
|
+
sessionHelper.writeRaw(sessionName, 'q');
|
|
2817
|
+
await delay(500);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
catch {
|
|
2821
|
+
// Non-fatal: session may have been destroyed
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2787
2824
|
// --- Part 1: Existing TUI prompt-line scanning (unchanged) ---
|
|
2788
2825
|
for (const [sessionName] of this.tuiSessionRegistry) {
|
|
2789
2826
|
try {
|
|
@@ -2797,19 +2834,19 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2797
2834
|
continue;
|
|
2798
2835
|
// Look for any text sitting on the prompt line
|
|
2799
2836
|
const lines = output.split('\n');
|
|
2800
|
-
const promptLineRegex = /^[│┃║|\s]*>\s+(.+)/;
|
|
2801
2837
|
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
|
|
2802
|
-
const
|
|
2803
|
-
if (
|
|
2804
|
-
const
|
|
2838
|
+
const promptContent = matchTuiPromptLine(lines[i]);
|
|
2839
|
+
if (promptContent !== null) {
|
|
2840
|
+
const trimmedContent = stripTuiLineBorders(promptContent).trim();
|
|
2805
2841
|
// Only act on substantial text (> 10 chars) to avoid false positives
|
|
2806
2842
|
// from TUI rendering artifacts or short status text
|
|
2807
|
-
if (
|
|
2843
|
+
if (trimmedContent.length > 10) {
|
|
2808
2844
|
// Skip known Gemini CLI idle placeholder text that sits at
|
|
2809
2845
|
// the `> ` prompt when no user input is present. These are
|
|
2810
2846
|
// NOT stuck messages — they are TUI decoration.
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2847
|
+
const lowerContent = trimmedContent.toLowerCase();
|
|
2848
|
+
const isPlaceholder = lowerContent.startsWith('type your message') ||
|
|
2849
|
+
trimmedContent.startsWith('@'); // e.g., "@path/to/file"
|
|
2813
2850
|
if (isPlaceholder) {
|
|
2814
2851
|
break;
|
|
2815
2852
|
}
|
|
@@ -2904,8 +2941,8 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2904
2941
|
*/
|
|
2905
2942
|
trackSentMessage(sessionName, message) {
|
|
2906
2943
|
// Extract a search snippet: skip [CHAT:uuid] prefix, take first 80 chars
|
|
2907
|
-
const
|
|
2908
|
-
const contentStart =
|
|
2944
|
+
const { prefixLength } = extractChatPrefix(message);
|
|
2945
|
+
const contentStart = prefixLength;
|
|
2909
2946
|
// Normalize whitespace: messages may contain \n from enhanced templates.
|
|
2910
2947
|
// Terminal bottom text is join(' '), so \n in snippet would never match.
|
|
2911
2948
|
const snippet = message.slice(contentStart, contentStart + 80).replace(/\s+/g, ' ').trim();
|
|
@@ -2919,27 +2956,10 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2919
2956
|
// Ensure the background scanner is running
|
|
2920
2957
|
this.startStuckMessageDetector();
|
|
2921
2958
|
}
|
|
2922
|
-
/**
|
|
2923
|
-
* Get the runtime-specific prompt regex pattern.
|
|
2924
|
-
* Avoids false positives by using narrow patterns when runtime is known.
|
|
2925
|
-
*
|
|
2926
|
-
* @param runtimeType - The runtime type (claude-code, gemini-cli, etc.)
|
|
2927
|
-
* @returns The appropriate prompt detection regex
|
|
2928
|
-
*/
|
|
2929
|
-
getPromptPatternForRuntime(runtimeType) {
|
|
2930
|
-
if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE)
|
|
2931
|
-
return TERMINAL_PATTERNS.CLAUDE_CODE_PROMPT;
|
|
2932
|
-
if (runtimeType === RUNTIME_TYPES.GEMINI_CLI)
|
|
2933
|
-
return TERMINAL_PATTERNS.GEMINI_CLI_PROMPT;
|
|
2934
|
-
if (runtimeType === RUNTIME_TYPES.CODEX_CLI)
|
|
2935
|
-
return TERMINAL_PATTERNS.CODEX_CLI_PROMPT;
|
|
2936
|
-
return TERMINAL_PATTERNS.PROMPT_STREAM;
|
|
2937
|
-
}
|
|
2938
2959
|
/**
|
|
2939
2960
|
* Check if the agent appears to be at an input prompt.
|
|
2940
|
-
*
|
|
2941
|
-
*
|
|
2942
|
-
* to avoid false negatives when the agent is processing.
|
|
2961
|
+
* Delegates to the regex-free isAgentAtPrompt() from terminal-string-ops,
|
|
2962
|
+
* with additional logging and per-line prompt detection using isPromptLine().
|
|
2943
2963
|
*
|
|
2944
2964
|
* @param terminalOutput - The terminal output to check
|
|
2945
2965
|
* @param runtimeType - The runtime type for pattern selection
|
|
@@ -2956,57 +2976,12 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
2956
2976
|
// Use 5000 chars to accommodate large tool outputs that push the prompt
|
|
2957
2977
|
// further back in the buffer (#106).
|
|
2958
2978
|
const tailSection = terminalOutput.slice(-5000);
|
|
2959
|
-
const isGemini = runtimeType === RUNTIME_TYPES.GEMINI_CLI;
|
|
2960
|
-
const isClaudeCode = runtimeType === RUNTIME_TYPES.CLAUDE_CODE;
|
|
2961
|
-
const isCodex = runtimeType === RUNTIME_TYPES.CODEX_CLI;
|
|
2962
|
-
const streamPattern = this.getPromptPatternForRuntime(runtimeType);
|
|
2963
2979
|
// Check for prompt FIRST. Processing indicators like "thinking" or "analyzing"
|
|
2964
2980
|
// can appear in the agent's previous response text and persist in the terminal
|
|
2965
2981
|
// scroll buffer, causing false negatives if checked before the prompt.
|
|
2966
|
-
if (streamPattern.test(tailSection)) {
|
|
2967
|
-
return true;
|
|
2968
|
-
}
|
|
2969
|
-
// Fallback: check last several lines for prompt indicators.
|
|
2970
|
-
// The prompt may not be on the very last line due to status bars,
|
|
2971
|
-
// notifications, or terminal wrapping below the prompt.
|
|
2972
2982
|
const lines = tailSection.split('\n').filter((line) => line.trim().length > 0);
|
|
2973
2983
|
const linesToCheck = lines.slice(-10);
|
|
2974
|
-
const hasPrompt = linesToCheck.some(
|
|
2975
|
-
const trimmed = line.trim();
|
|
2976
|
-
// Strip TUI box-drawing borders that Gemini CLI and other TUI frameworks
|
|
2977
|
-
// wrap around prompts. Covers full Unicode box-drawing range (#106).
|
|
2978
|
-
const stripped = trimmed
|
|
2979
|
-
.replace(/^[\u2500-\u257F|+\-═║╭╮╰╯]+\s*/, '')
|
|
2980
|
-
.replace(/\s*[\u2500-\u257F|+\-═║╭╮╰╯]+$/, '');
|
|
2981
|
-
// Claude Code prompts: ❯, ⏵, $ alone on a line
|
|
2982
|
-
if (!isGemini && !isCodex) {
|
|
2983
|
-
if (['❯', '⏵', '$'].some(ch => trimmed === ch || stripped === ch)) {
|
|
2984
|
-
return true;
|
|
2985
|
-
}
|
|
2986
|
-
// ❯❯ = bypass permissions prompt (idle).
|
|
2987
|
-
// Matches "❯❯", "❯❯ ", and "❯❯ bypass permissions on (shift+tab to cycle)".
|
|
2988
|
-
// Note: ⏵⏵ appears in the status bar but is visible both when idle AND
|
|
2989
|
-
// busy, so it cannot be used as a reliable prompt indicator.
|
|
2990
|
-
if (trimmed.startsWith('❯❯')) {
|
|
2991
|
-
return true;
|
|
2992
|
-
}
|
|
2993
|
-
}
|
|
2994
|
-
// Gemini CLI prompts: > or ! followed by space
|
|
2995
|
-
if (!isClaudeCode) {
|
|
2996
|
-
if (isCodex) {
|
|
2997
|
-
// Codex prompt uses `›`; avoid plain `> ` to prevent false-positives
|
|
2998
|
-
// from markdown blockquotes in agent output.
|
|
2999
|
-
if (trimmed.startsWith('› ') || stripped.startsWith('› ')) {
|
|
3000
|
-
return true;
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
else if (trimmed.startsWith('> ') || trimmed.startsWith('! ') ||
|
|
3004
|
-
stripped.startsWith('> ') || stripped.startsWith('! ')) {
|
|
3005
|
-
return true;
|
|
3006
|
-
}
|
|
3007
|
-
}
|
|
3008
|
-
return false;
|
|
3009
|
-
});
|
|
2984
|
+
const hasPrompt = linesToCheck.some(line => isPromptLine(line, runtimeType));
|
|
3010
2985
|
if (hasPrompt) {
|
|
3011
2986
|
return true;
|
|
3012
2987
|
}
|
|
@@ -3014,14 +2989,14 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
3014
2989
|
// Check last 10 lines (not just 5) because tool output can push processing
|
|
3015
2990
|
// indicators further up while the status bar stays at the bottom.
|
|
3016
2991
|
const recentLines = linesToCheck.join('\n');
|
|
3017
|
-
if (
|
|
2992
|
+
if (containsProcessingIndicator(recentLines)) {
|
|
3018
2993
|
this.logger.debug('Processing indicators present near bottom of output');
|
|
3019
2994
|
return false;
|
|
3020
2995
|
}
|
|
3021
2996
|
// Check for "esc to interrupt" in the status bar — this is a definitive
|
|
3022
2997
|
// busy signal. Claude Code only shows this text while actively processing.
|
|
3023
2998
|
// It disappears when the agent returns to idle at the prompt.
|
|
3024
|
-
if (
|
|
2999
|
+
if (containsBusyStatusBar(recentLines)) {
|
|
3025
3000
|
this.logger.debug('Busy status bar detected (esc to interrupt)');
|
|
3026
3001
|
return false;
|
|
3027
3002
|
}
|
|
@@ -3279,7 +3254,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
|
|
|
3279
3254
|
return false;
|
|
3280
3255
|
const currentOutput = sessionHelper.capturePane(sessionName);
|
|
3281
3256
|
// Processing indicators (spinners) = definitive success
|
|
3282
|
-
if (
|
|
3257
|
+
if (containsSpinnerOrWorkingIndicator(currentOutput)) {
|
|
3283
3258
|
this.logger.debug('Kickoff delivered — processing indicators detected', {
|
|
3284
3259
|
sessionName, checkIndex: i,
|
|
3285
3260
|
});
|