commandmate 0.3.4 → 0.3.5
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 +12 -12
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +5 -5
- 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 +13 -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/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/repositories/route.js +2 -2
- package/.next/server/app/api/repositories/route.js.nft.json +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]/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]/execution-logs/[logId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/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]/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]/schedules/[scheduleId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +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/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page_client-reference-manifest.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 +4 -4
- package/.next/server/app/worktrees/[id]/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 +7 -7
- package/.next/server/chunks/3074.js +1 -1
- package/.next/server/chunks/4952.js +1 -0
- package/.next/server/chunks/539.js +3 -3
- package/.next/server/chunks/5795.js +1 -1
- package/.next/server/chunks/7425.js +28 -25
- package/.next/server/chunks/7566.js +1 -1
- package/.next/server/chunks/8693.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{4327.740cc7fe2d0b5049.js → 4327.157a4c226d919531.js} +14 -14
- package/.next/static/chunks/5970.0df906ad5a9c9147.js +1 -0
- package/.next/static/chunks/{8091-c0e955616dd86f82.js → 8091-d65d2ab6daed23c6.js} +1 -1
- package/.next/static/chunks/app/login/page-010f02fd4b0dbc48.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-8fb4dc30b58a5681.js +1 -0
- package/.next/static/chunks/webpack-81c97591dd5567ac.js +1 -0
- package/.next/static/css/45b3a41370668314.css +3 -0
- package/.next/trace +5 -5
- package/dist/server/src/lib/claude-executor.js +14 -3
- package/dist/server/src/lib/cli-patterns.js +99 -20
- package/dist/server/src/lib/cli-tools/manager.js +5 -3
- package/dist/server/src/lib/cli-tools/opencode-config.js +236 -0
- package/dist/server/src/lib/cli-tools/opencode.js +188 -0
- package/dist/server/src/lib/cli-tools/types.js +12 -5
- package/dist/server/src/lib/db.js +18 -0
- package/dist/server/src/lib/response-poller.js +367 -19
- package/package.json +2 -1
- package/.next/server/chunks/9446.js +0 -1
- package/.next/static/chunks/app/login/page-2d42204ba87cd136.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-9c0c64488c17db3c.js +0 -1
- package/.next/static/chunks/webpack-3c0ee3ce5b546818.js +0 -1
- package/.next/static/css/fa3df0e6f437f2ba.css +0 -3
- /package/.next/static/{BiyH3zkbySg7ZWTeZuXqj → p3hosTZoJ22r35fWwUoLr}/_buildManifest.js +0 -0
- /package/.next/static/{BiyH3zkbySg7ZWTeZuXqj → p3hosTZoJ22r35fWwUoLr}/_ssgManifest.js +0 -0
|
@@ -13,7 +13,7 @@ exports.isValidVibeLocalContextWindow = isValidVibeLocalContextWindow;
|
|
|
13
13
|
* T2.1: Single source of truth for CLI tool IDs
|
|
14
14
|
* CLIToolType is derived from this constant (DRY principle)
|
|
15
15
|
*/
|
|
16
|
-
exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini', 'vibe-local'];
|
|
16
|
+
exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini', 'vibe-local', 'opencode'];
|
|
17
17
|
/**
|
|
18
18
|
* CLI tool display names for UI rendering
|
|
19
19
|
* Issue #368: Centralized display name mapping
|
|
@@ -26,6 +26,7 @@ exports.CLI_TOOL_DISPLAY_NAMES = {
|
|
|
26
26
|
codex: 'Codex',
|
|
27
27
|
gemini: 'Gemini',
|
|
28
28
|
'vibe-local': 'Vibe Local',
|
|
29
|
+
opencode: 'OpenCode',
|
|
29
30
|
};
|
|
30
31
|
/**
|
|
31
32
|
* Check if a string is a valid CLIToolType
|
|
@@ -99,10 +100,16 @@ function isValidVibeLocalContextWindow(value) {
|
|
|
99
100
|
value <= exports.VIBE_LOCAL_CONTEXT_WINDOW_MAX);
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
|
-
* Ollama model name validation pattern.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* Ollama model name validation pattern (API/DB layer).
|
|
104
|
+
* Requires alphanumeric first character, followed by alphanumeric, dots, underscores,
|
|
105
|
+
* colons, slashes, hyphens. No explicit length limit (DB schema handles storage limits).
|
|
105
106
|
*
|
|
106
|
-
* [SEC-001] Shared between API route validation and CLI command construction
|
|
107
|
+
* [SEC-001] Shared between API route validation and CLI command construction.
|
|
108
|
+
*
|
|
109
|
+
* Note: opencode-config.ts has a separate OLLAMA_MODEL_PATTERN with a 100-character
|
|
110
|
+
* length limit (`{1,100}`) for DoS protection when parsing Ollama API responses.
|
|
111
|
+
* The patterns are intentionally different: this one enforces first-character constraints
|
|
112
|
+
* for user-facing validation, while the opencode-config version adds length limits
|
|
113
|
+
* for untrusted external API data.
|
|
107
114
|
*/
|
|
108
115
|
exports.OLLAMA_MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/;
|
|
@@ -19,6 +19,7 @@ exports.getMessages = getMessages;
|
|
|
19
19
|
exports.getLastUserMessage = getLastUserMessage;
|
|
20
20
|
exports.getLastMessage = getLastMessage;
|
|
21
21
|
exports.deleteAllMessages = deleteAllMessages;
|
|
22
|
+
exports.deleteMessageById = deleteMessageById;
|
|
22
23
|
exports.deleteMessagesByCliTool = deleteMessagesByCliTool;
|
|
23
24
|
exports.getSessionState = getSessionState;
|
|
24
25
|
exports.updateSessionState = updateSessionState;
|
|
@@ -480,6 +481,23 @@ function deleteAllMessages(db, worktreeId) {
|
|
|
480
481
|
stmt.run(worktreeId);
|
|
481
482
|
console.log(`[deleteAllMessages] Deleted all messages for worktree: ${worktreeId}`);
|
|
482
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Delete a single message by its ID
|
|
486
|
+
* Used to clean up orphaned user messages (e.g., when a user re-sends a message
|
|
487
|
+
* after the previous one received no response).
|
|
488
|
+
*
|
|
489
|
+
* @param db - Database instance
|
|
490
|
+
* @param messageId - ID of the message to delete
|
|
491
|
+
* @returns True if a message was deleted, false otherwise
|
|
492
|
+
*/
|
|
493
|
+
function deleteMessageById(db, messageId) {
|
|
494
|
+
const stmt = db.prepare(`
|
|
495
|
+
DELETE FROM chat_messages
|
|
496
|
+
WHERE id = ?
|
|
497
|
+
`);
|
|
498
|
+
const result = stmt.run(messageId);
|
|
499
|
+
return result.changes > 0;
|
|
500
|
+
}
|
|
483
501
|
/**
|
|
484
502
|
* Delete messages for a specific CLI tool in a worktree
|
|
485
503
|
* Issue #4: T4.2 - Individual CLI tool session termination (MF3-001)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* CLI Tool response polling.
|
|
4
|
-
* Periodically checks tmux sessions for CLI tool responses (Claude, Codex, Gemini).
|
|
4
|
+
* Periodically checks tmux sessions for CLI tool responses (Claude, Codex, Gemini, Vibe Local, OpenCode).
|
|
5
5
|
*
|
|
6
6
|
* Key responsibilities:
|
|
7
7
|
* - Extract completed responses from tmux output (extractResponse)
|
|
@@ -13,10 +13,23 @@
|
|
|
13
13
|
* - DR-004: Tail-line windowing for thinking detection in extractResponse
|
|
14
14
|
* - MF-001 fix: Same windowing applied to checkForResponse thinking check
|
|
15
15
|
* - SF-003: RESPONSE_THINKING_TAIL_LINE_COUNT constant tracks STATUS_THINKING_LINE_COUNT
|
|
16
|
+
*
|
|
17
|
+
* Issue #379 additions:
|
|
18
|
+
* - OpenCode completion detection via Build summary line (isOpenCodeComplete)
|
|
19
|
+
* - OpenCode response cleaning (cleanOpenCodeResponse)
|
|
20
|
+
* - OpenCode extraction stop conditions (OPENCODE_PROMPT_PATTERN, OPENCODE_PROMPT_AFTER_RESPONSE)
|
|
16
21
|
*/
|
|
17
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.extractTuiContentLines = extractTuiContentLines;
|
|
24
|
+
exports.findOverlapIndex = findOverlapIndex;
|
|
25
|
+
exports.initTuiAccumulator = initTuiAccumulator;
|
|
26
|
+
exports.accumulateTuiContent = accumulateTuiContent;
|
|
27
|
+
exports.getAccumulatedContent = getAccumulatedContent;
|
|
28
|
+
exports.clearTuiAccumulator = clearTuiAccumulator;
|
|
18
29
|
exports.cleanClaudeResponse = cleanClaudeResponse;
|
|
19
30
|
exports.cleanGeminiResponse = cleanGeminiResponse;
|
|
31
|
+
exports.isOpenCodeComplete = isOpenCodeComplete;
|
|
32
|
+
exports.cleanOpenCodeResponse = cleanOpenCodeResponse;
|
|
20
33
|
exports.resolveExtractionStartIndex = resolveExtractionStartIndex;
|
|
21
34
|
exports.startPolling = startPolling;
|
|
22
35
|
exports.stopPolling = stopPolling;
|
|
@@ -101,6 +114,163 @@ function buildPromptExtractionResult(lines, lastCapturedLine, totalLines, buffer
|
|
|
101
114
|
bufferReset,
|
|
102
115
|
};
|
|
103
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Number of lines from the end of accumulated content to use as
|
|
119
|
+
* fingerprint for overlap detection with the next capture.
|
|
120
|
+
*/
|
|
121
|
+
const OVERLAP_FINGERPRINT_SIZE = 10;
|
|
122
|
+
/**
|
|
123
|
+
* Per-session TUI response accumulator storage.
|
|
124
|
+
* Key: pollerKey ("worktreeId:cliToolId")
|
|
125
|
+
*/
|
|
126
|
+
const tuiResponseAccumulator = new Map();
|
|
127
|
+
/**
|
|
128
|
+
* Normalize a single OpenCode TUI line by removing ANSI codes and border glyphs.
|
|
129
|
+
* Returns an empty string when the line has no meaningful content after cleanup.
|
|
130
|
+
*/
|
|
131
|
+
function normalizeOpenCodeLine(line) {
|
|
132
|
+
return (0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(line))
|
|
133
|
+
.replace(/^\u2503\s?/, '')
|
|
134
|
+
.replace(/\s*\u2503$/, '')
|
|
135
|
+
.trim();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extract meaningful content lines from raw TUI output.
|
|
139
|
+
* Strips ANSI codes, box-drawing characters, and lines matching OPENCODE_SKIP_PATTERNS.
|
|
140
|
+
*
|
|
141
|
+
* @param rawOutput - Raw tmux capture-pane output
|
|
142
|
+
* @returns Array of cleaned, non-empty content lines
|
|
143
|
+
*
|
|
144
|
+
* @internal Exported for unit testing
|
|
145
|
+
*/
|
|
146
|
+
function extractTuiContentLines(rawOutput) {
|
|
147
|
+
const lines = rawOutput.split('\n');
|
|
148
|
+
const contentLines = [];
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const cleaned = normalizeOpenCodeLine(line);
|
|
151
|
+
if (!cleaned)
|
|
152
|
+
continue;
|
|
153
|
+
const shouldSkip = cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(pattern => pattern.test(cleaned));
|
|
154
|
+
if (shouldSkip)
|
|
155
|
+
continue;
|
|
156
|
+
// Also skip the Build summary line (completion indicator)
|
|
157
|
+
if (cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleaned))
|
|
158
|
+
continue;
|
|
159
|
+
contentLines.push(cleaned);
|
|
160
|
+
}
|
|
161
|
+
return contentLines;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Find the overlap index between previously accumulated lines and newly captured lines.
|
|
165
|
+
* Searches for the longest suffix of `previous` that matches a prefix of `current`.
|
|
166
|
+
*
|
|
167
|
+
* @param previous - Previously accumulated lines (fingerprint subset)
|
|
168
|
+
* @param current - Newly captured content lines
|
|
169
|
+
* @returns Number of overlapping lines (0 if no overlap found)
|
|
170
|
+
*
|
|
171
|
+
* @internal Exported for unit testing
|
|
172
|
+
*/
|
|
173
|
+
function findOverlapIndex(previous, current) {
|
|
174
|
+
if (previous.length === 0 || current.length === 0)
|
|
175
|
+
return 0;
|
|
176
|
+
// Try decreasing overlap sizes: full fingerprint down to 1 line
|
|
177
|
+
const maxOverlap = Math.min(previous.length, current.length);
|
|
178
|
+
for (let overlapSize = maxOverlap; overlapSize >= 1; overlapSize--) {
|
|
179
|
+
// Check if the last `overlapSize` lines of previous match
|
|
180
|
+
// the first `overlapSize` lines of current
|
|
181
|
+
const prevSlice = previous.slice(-overlapSize);
|
|
182
|
+
const currSlice = current.slice(0, overlapSize);
|
|
183
|
+
let matches = true;
|
|
184
|
+
for (let i = 0; i < overlapSize; i++) {
|
|
185
|
+
if (prevSlice[i] !== currSlice[i]) {
|
|
186
|
+
matches = false;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (matches) {
|
|
191
|
+
return overlapSize;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Initialize the TUI accumulator for a polling session.
|
|
198
|
+
*
|
|
199
|
+
* @param pollerKey - Poller key ("worktreeId:cliToolId")
|
|
200
|
+
*
|
|
201
|
+
* @internal Exported for unit testing
|
|
202
|
+
*/
|
|
203
|
+
function initTuiAccumulator(pollerKey) {
|
|
204
|
+
tuiResponseAccumulator.set(pollerKey, {
|
|
205
|
+
lines: [],
|
|
206
|
+
lastFingerprint: [],
|
|
207
|
+
pollCount: 0,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Accumulate TUI content from a new capture into the session buffer.
|
|
212
|
+
* Detects overlap with previous capture and appends only new lines.
|
|
213
|
+
*
|
|
214
|
+
* @param pollerKey - Poller key ("worktreeId:cliToolId")
|
|
215
|
+
* @param rawOutput - Raw tmux capture-pane output
|
|
216
|
+
*
|
|
217
|
+
* @internal Exported for unit testing
|
|
218
|
+
*/
|
|
219
|
+
function accumulateTuiContent(pollerKey, rawOutput) {
|
|
220
|
+
const state = tuiResponseAccumulator.get(pollerKey);
|
|
221
|
+
if (!state)
|
|
222
|
+
return;
|
|
223
|
+
const contentLines = extractTuiContentLines(rawOutput);
|
|
224
|
+
if (contentLines.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
state.pollCount++;
|
|
227
|
+
if (state.lines.length === 0) {
|
|
228
|
+
// First capture: seed with all content
|
|
229
|
+
state.lines = [...contentLines];
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Subsequent captures: find overlap and append new lines
|
|
233
|
+
const overlapCount = findOverlapIndex(state.lastFingerprint, contentLines);
|
|
234
|
+
if (overlapCount > 0) {
|
|
235
|
+
// Append only lines after the overlap
|
|
236
|
+
const newLines = contentLines.slice(overlapCount);
|
|
237
|
+
state.lines.push(...newLines);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// No overlap found: append all content (completeness over dedup)
|
|
241
|
+
state.lines.push(...contentLines);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Update fingerprint for next poll
|
|
245
|
+
state.lastFingerprint = contentLines.slice(-OVERLAP_FINGERPRINT_SIZE);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get the accumulated content as a single string.
|
|
249
|
+
*
|
|
250
|
+
* @param pollerKey - Poller key ("worktreeId:cliToolId")
|
|
251
|
+
* @returns Accumulated content joined by newlines, or empty string if no accumulator
|
|
252
|
+
*
|
|
253
|
+
* @internal Exported for unit testing
|
|
254
|
+
*/
|
|
255
|
+
function getAccumulatedContent(pollerKey) {
|
|
256
|
+
const state = tuiResponseAccumulator.get(pollerKey);
|
|
257
|
+
if (!state || state.lines.length === 0)
|
|
258
|
+
return '';
|
|
259
|
+
return state.lines.join('\n');
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Clear the TUI accumulator for a polling session.
|
|
263
|
+
*
|
|
264
|
+
* @param pollerKey - Poller key ("worktreeId:cliToolId")
|
|
265
|
+
*
|
|
266
|
+
* @internal Exported for unit testing
|
|
267
|
+
*/
|
|
268
|
+
function clearTuiAccumulator(pollerKey) {
|
|
269
|
+
tuiResponseAccumulator.delete(pollerKey);
|
|
270
|
+
}
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Poller State Management
|
|
273
|
+
// ============================================================================
|
|
104
274
|
/**
|
|
105
275
|
* Active pollers map: "worktreeId:cliToolId" -> NodeJS.Timeout
|
|
106
276
|
*/
|
|
@@ -252,13 +422,91 @@ function cleanGeminiResponse(response) {
|
|
|
252
422
|
}
|
|
253
423
|
return cleanedLines.join('\n').trim();
|
|
254
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if OpenCode has completed its response.
|
|
427
|
+
* Detects the Build summary line pattern (e.g., "▣ Build . model . 2.5s").
|
|
428
|
+
* [D2-002] Independent completion detection for OpenCode.
|
|
429
|
+
*
|
|
430
|
+
* Unlike Claude (prompt + separator) or Codex/Gemini (prompt + not thinking),
|
|
431
|
+
* OpenCode signals completion via the Build summary line, which includes
|
|
432
|
+
* the model name and generation timing.
|
|
433
|
+
*
|
|
434
|
+
* @param output - Cleaned tmux output to check (ANSI-stripped)
|
|
435
|
+
* @returns True if OpenCode response is complete
|
|
436
|
+
*
|
|
437
|
+
* @internal Exported for unit testing (response-poller-opencode.test.ts)
|
|
438
|
+
*/
|
|
439
|
+
function isOpenCodeComplete(output) {
|
|
440
|
+
// Must have a Build completion marker AND must NOT be actively processing.
|
|
441
|
+
// The "esc interrupt" indicator appears in the TUI footer during model processing.
|
|
442
|
+
// Without this check, old Build markers from previous Q&As cause false completions.
|
|
443
|
+
return cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(output) && !cli_patterns_1.OPENCODE_PROCESSING_INDICATOR.test(output);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Clean OpenCode TUI response by removing decoration characters and status lines,
|
|
447
|
+
* and trimming to only the latest response.
|
|
448
|
+
* [D2-009] Removes box-drawing characters, Build summary, loading indicators,
|
|
449
|
+
* prompt patterns, and processing indicators.
|
|
450
|
+
*
|
|
451
|
+
* Cleaning pipeline:
|
|
452
|
+
* 1. Split response into lines
|
|
453
|
+
* 2. Trim to latest response: find Build markers (▣ Build · model · time)
|
|
454
|
+
* and discard all content before the second-to-last marker.
|
|
455
|
+
* OpenCode TUI accumulates conversation history; each Q&A exchange ends
|
|
456
|
+
* with a Build marker. Without this trimming, savePendingAssistantResponse
|
|
457
|
+
* and Layer 2 accumulator would include previous Q&As in the response.
|
|
458
|
+
* 3. Skip empty lines
|
|
459
|
+
* 4. Skip lines matching any OPENCODE_SKIP_PATTERNS (TUI artifacts)
|
|
460
|
+
* 5. Skip Build summary line (OPENCODE_RESPONSE_COMPLETE, the completion indicator)
|
|
461
|
+
* 6. Join remaining lines
|
|
462
|
+
*
|
|
463
|
+
* @param response - Raw OpenCode response (may contain TUI decoration)
|
|
464
|
+
* @returns Cleaned response with TUI artifacts removed
|
|
465
|
+
*
|
|
466
|
+
* @internal Exported for unit testing (response-poller-opencode.test.ts)
|
|
467
|
+
*/
|
|
468
|
+
function cleanOpenCodeResponse(response) {
|
|
469
|
+
const lines = response.split('\n');
|
|
470
|
+
// Step 2: Trim to latest response by finding Build markers.
|
|
471
|
+
// Each Q&A exchange ends with "▣ Build · model · time".
|
|
472
|
+
// If 2+ markers exist, only include content after the second-to-last marker.
|
|
473
|
+
const buildIndices = [];
|
|
474
|
+
for (let i = 0; i < lines.length; i++) {
|
|
475
|
+
const cleanLine = normalizeOpenCodeLine(lines[i]);
|
|
476
|
+
if (cleanLine && cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanLine)) {
|
|
477
|
+
buildIndices.push(i);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
let startLine = 0;
|
|
481
|
+
if (buildIndices.length >= 2) {
|
|
482
|
+
startLine = buildIndices[buildIndices.length - 2] + 1;
|
|
483
|
+
}
|
|
484
|
+
const cleanedLines = [];
|
|
485
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
486
|
+
// Strip ANSI escape codes and TUI border characters before pattern matching.
|
|
487
|
+
// Without this, embedded ANSI codes and heavy borders can break regex matches.
|
|
488
|
+
const cleanLine = normalizeOpenCodeLine(lines[i]);
|
|
489
|
+
if (!cleanLine)
|
|
490
|
+
continue;
|
|
491
|
+
// Skip lines matching any OpenCode skip pattern
|
|
492
|
+
const shouldSkip = cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(pattern => pattern.test(cleanLine));
|
|
493
|
+
if (shouldSkip)
|
|
494
|
+
continue;
|
|
495
|
+
// Skip the Build summary line (completion indicator)
|
|
496
|
+
if (cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanLine))
|
|
497
|
+
continue;
|
|
498
|
+
cleanedLines.push(cleanLine);
|
|
499
|
+
}
|
|
500
|
+
return cleanedLines.join('\n').trim();
|
|
501
|
+
}
|
|
255
502
|
/**
|
|
256
503
|
* Determine the start index for response extraction based on buffer state.
|
|
257
504
|
* Shared between normal response extraction and prompt detection paths.
|
|
258
505
|
*
|
|
259
|
-
* Implements a
|
|
506
|
+
* Implements a 5-branch decision tree for startIndex determination:
|
|
260
507
|
* 1. bufferWasReset -> findRecentUserPromptIndex(40) + 1, or 0 if not found
|
|
261
|
-
*
|
|
508
|
+
* 2a. cliToolId === 'opencode' -> findRecentUserPromptIndex(totalLines) + 1, or 0
|
|
509
|
+
* 2b. cliToolId === 'codex' -> Math.max(0, lastCapturedLine)
|
|
262
510
|
* 3. lastCapturedLine >= totalLines - 5 (scroll boundary) ->
|
|
263
511
|
* findRecentUserPromptIndex(50) + 1, or totalLines - 40 if not found
|
|
264
512
|
* 4. Normal case -> Math.max(0, lastCapturedLine)
|
|
@@ -291,6 +539,15 @@ function cleanGeminiResponse(response) {
|
|
|
291
539
|
function resolveExtractionStartIndex(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex) {
|
|
292
540
|
// Defensive validation: clamp negative values to 0 (Stage 4 SF-001)
|
|
293
541
|
lastCapturedLine = Math.max(0, lastCapturedLine);
|
|
542
|
+
// Branch 2a (highest priority for OpenCode): OpenCode runs in alternate screen mode
|
|
543
|
+
// (fixed-size buffer, no scrollback). lastCapturedLine is meaningless because the buffer
|
|
544
|
+
// doesn't grow — it's always ~PANE_HEIGHT lines. bufferWasReset is often true because
|
|
545
|
+
// lastCapturedLine ≈ totalLines. Must execute BEFORE Branch 1 to avoid Branch 1's small
|
|
546
|
+
// window (40 lines) which fails to find the second-to-last Build marker in a 200-line pane.
|
|
547
|
+
if (cliToolId === 'opencode') {
|
|
548
|
+
const foundUserPrompt = findRecentUserPromptIndex(totalLines);
|
|
549
|
+
return foundUserPrompt >= 0 ? foundUserPrompt + 1 : 0;
|
|
550
|
+
}
|
|
294
551
|
// Compute bufferWasReset internally (MF-001: responsibility boundary)
|
|
295
552
|
const bufferWasReset = lastCapturedLine >= totalLines || bufferReset;
|
|
296
553
|
// Branch 1: Buffer was reset - find the most recent user prompt as anchor
|
|
@@ -298,7 +555,7 @@ function resolveExtractionStartIndex(lastCapturedLine, totalLines, bufferReset,
|
|
|
298
555
|
const foundUserPrompt = findRecentUserPromptIndex(40);
|
|
299
556
|
return foundUserPrompt >= 0 ? foundUserPrompt + 1 : 0;
|
|
300
557
|
}
|
|
301
|
-
// Branch
|
|
558
|
+
// Branch 2b: Codex uses lastCapturedLine directly (Codex-specific TUI behavior)
|
|
302
559
|
if (cliToolId === 'codex') {
|
|
303
560
|
return Math.max(0, lastCapturedLine);
|
|
304
561
|
}
|
|
@@ -339,18 +596,47 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
339
596
|
if (!bufferReset && totalLines < lastCapturedLine - 5) {
|
|
340
597
|
return null;
|
|
341
598
|
}
|
|
342
|
-
//
|
|
599
|
+
// Check recent lines for completion pattern.
|
|
600
|
+
// OpenCode TUI: content area + many empty padding lines + status bar at bottom.
|
|
601
|
+
// The trailing-empty-line trim above removes trailing newlines but NOT the internal
|
|
602
|
+
// padding between content and status bar. For OpenCode, the Build completion marker
|
|
603
|
+
// can be far above the last 20 lines, so we must check the full buffer.
|
|
343
604
|
const checkLineCount = 20;
|
|
344
605
|
const startLine = Math.max(0, totalLines - checkLineCount);
|
|
345
606
|
const linesToCheck = lines.slice(startLine);
|
|
346
|
-
const outputToCheck =
|
|
607
|
+
const outputToCheck = cliToolId === 'opencode'
|
|
608
|
+
? (0, cli_patterns_1.stripAnsi)(lines.join('\n'))
|
|
609
|
+
: linesToCheck.join('\n');
|
|
347
610
|
// Get tool-specific patterns from shared module
|
|
348
611
|
const { promptPattern, separatorPattern, thinkingPattern, skipPatterns } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
|
|
349
612
|
const findRecentUserPromptIndex = (windowSize = 60) => {
|
|
350
613
|
// User prompt pattern: supports legacy '>' and new '❯' for Claude
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
614
|
+
let userPromptPattern;
|
|
615
|
+
if (cliToolId === 'codex') {
|
|
616
|
+
userPromptPattern = /^›\s+(?!Implement|Find and fix|Type|Summarize)/;
|
|
617
|
+
}
|
|
618
|
+
else if (cliToolId === 'opencode') {
|
|
619
|
+
// OpenCode TUI accumulates conversation history in a single screen.
|
|
620
|
+
// Each Q&A exchange ends with a "▣ Build · model · time" marker.
|
|
621
|
+
// "Ask anything..." does NOT appear in tmux capture content area.
|
|
622
|
+
// Instead, find the SECOND-TO-LAST Build marker (= end of previous exchange)
|
|
623
|
+
// to use as the extraction boundary. The first Build marker found (searching
|
|
624
|
+
// backwards) is the current response's completion; the second is the boundary.
|
|
625
|
+
let buildCount = 0;
|
|
626
|
+
for (let i = totalLines - 1; i >= Math.max(0, totalLines - windowSize); i--) {
|
|
627
|
+
const cleanLine = (0, cli_patterns_1.stripAnsi)(lines[i]);
|
|
628
|
+
if (cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanLine)) {
|
|
629
|
+
buildCount++;
|
|
630
|
+
if (buildCount === 2) {
|
|
631
|
+
return i;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return -1;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
userPromptPattern = /^[>❯]\s+\S/;
|
|
639
|
+
}
|
|
354
640
|
for (let i = totalLines - 1; i >= Math.max(0, totalLines - windowSize); i--) {
|
|
355
641
|
const cleanLine = (0, cli_patterns_1.stripAnsi)(lines[i]);
|
|
356
642
|
if (userPromptPattern.test(cleanLine)) {
|
|
@@ -387,7 +673,9 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
387
673
|
// Claude: require both prompt and separator
|
|
388
674
|
const isCodexOrGeminiComplete = (cliToolId === 'codex' || cliToolId === 'gemini' || cliToolId === 'vibe-local') && hasPrompt && !isThinking;
|
|
389
675
|
const isClaudeComplete = cliToolId === 'claude' && hasPrompt && hasSeparator && !isThinking;
|
|
390
|
-
|
|
676
|
+
// [D2-002] OpenCode completion: detected via Build summary line pattern (independent of prompt/separator)
|
|
677
|
+
const isOpenCodeDone = cliToolId === 'opencode' && isOpenCodeComplete(cleanOutputToCheck);
|
|
678
|
+
if (isCodexOrGeminiComplete || isClaudeComplete || isOpenCodeDone) {
|
|
391
679
|
// CLI tool has completed response
|
|
392
680
|
// Extract the response content from lastCapturedLine to the separator (not just last 20 lines)
|
|
393
681
|
const responseLines = [];
|
|
@@ -408,6 +696,13 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
408
696
|
endIndex = i;
|
|
409
697
|
break;
|
|
410
698
|
}
|
|
699
|
+
// [D2-003] For OpenCode: stop at prompt or status bar patterns
|
|
700
|
+
if (cliToolId === 'opencode') {
|
|
701
|
+
if (cli_patterns_1.OPENCODE_PROMPT_PATTERN.test(cleanLine) || cli_patterns_1.OPENCODE_PROMPT_AFTER_RESPONSE.test(cleanLine)) {
|
|
702
|
+
endIndex = i;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
411
706
|
// Skip lines matching any skip pattern (check against clean line)
|
|
412
707
|
const shouldSkip = skipPatterns.some(pattern => pattern.test(cleanLine));
|
|
413
708
|
if (shouldSkip) {
|
|
@@ -470,6 +765,21 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
470
765
|
return incompleteResult(totalLines);
|
|
471
766
|
}
|
|
472
767
|
}
|
|
768
|
+
// OpenCode banner defense: initial startup screen should not be treated as a response
|
|
769
|
+
if (cliToolId === 'opencode') {
|
|
770
|
+
const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
|
|
771
|
+
// If the output is very short and contains only TUI elements, treat as startup banner
|
|
772
|
+
if (cleanResponse.length < 50 || !cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanOutputToCheck)) {
|
|
773
|
+
// Check if there's actual content (not just TUI decoration)
|
|
774
|
+
const contentLines = cleanResponse.split('\n').filter(line => {
|
|
775
|
+
const trimmed = line.trim();
|
|
776
|
+
return trimmed && !cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(p => p.test(trimmed));
|
|
777
|
+
});
|
|
778
|
+
if (contentLines.length === 0) {
|
|
779
|
+
return incompleteResult(totalLines);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
473
783
|
return {
|
|
474
784
|
response,
|
|
475
785
|
isComplete: true,
|
|
@@ -479,12 +789,18 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
479
789
|
}
|
|
480
790
|
// Check if this is an interactive prompt (yes/no or multiple choice)
|
|
481
791
|
// Interactive prompts don't have the ">" prompt and separator, so we need to detect them separately
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
792
|
+
// [Issue #379] Skip general prompt detection for OpenCode when response is incomplete.
|
|
793
|
+
// OpenCode's "Ask anything..." prompt pattern can cause false positive prompt detection
|
|
794
|
+
// when combined with user input text visible in the TUI buffer, leading to duplicate
|
|
795
|
+
// message creation. OpenCode prompts are only relevant when completion is detected above.
|
|
796
|
+
if (cliToolId !== 'opencode') {
|
|
797
|
+
const fullOutput = lines.join('\n');
|
|
798
|
+
const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
|
|
799
|
+
if (promptDetection.isPrompt) {
|
|
800
|
+
// Prompt detection uses full buffer for accuracy, but return only lastCapturedLine onwards
|
|
801
|
+
// stripAnsi is applied inside buildPromptExtractionResult (Stage 4 MF-001: XSS risk mitigation)
|
|
802
|
+
return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection);
|
|
803
|
+
}
|
|
488
804
|
}
|
|
489
805
|
// Not a prompt, but we may have a partial response in progress (even if Claude shows a spinner)
|
|
490
806
|
const responseLines = [];
|
|
@@ -543,6 +859,11 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
543
859
|
const lastCapturedLine = sessionState?.lastCapturedLine || 0;
|
|
544
860
|
// Capture current output
|
|
545
861
|
const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, 10000);
|
|
862
|
+
// Layer 2: Accumulate TUI content for OpenCode (for overlap tracking only).
|
|
863
|
+
if (cliToolId === 'opencode') {
|
|
864
|
+
const pollerKey = getPollerKey(worktreeId, cliToolId);
|
|
865
|
+
accumulateTuiContent(pollerKey, output);
|
|
866
|
+
}
|
|
546
867
|
// Extract response
|
|
547
868
|
const result = extractResponse(output, lastCapturedLine, cliToolId);
|
|
548
869
|
if (!result || !result.isComplete) {
|
|
@@ -564,17 +885,22 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
564
885
|
}
|
|
565
886
|
return false;
|
|
566
887
|
}
|
|
888
|
+
// Issue #379: OpenCode uses a full-screen TUI with fixed buffer size (~200 lines).
|
|
889
|
+
// The tmux pane doesn't grow (no scrollback); each response overwrites the same pane,
|
|
890
|
+
// so lineCount is always approximately equal to lastCapturedLine. Skip line-based
|
|
891
|
+
// duplicate detection entirely for full-screen TUIs.
|
|
892
|
+
const isFullScreenTui = cliToolId === 'opencode';
|
|
567
893
|
// CRITICAL FIX: If lineCount == lastCapturedLine AND there's no in-progress message,
|
|
568
894
|
// this response has already been saved. Skip to prevent duplicates.
|
|
569
895
|
// Issue #372: Skip when buffer reset detected (TUI redraw may coincidentally match lineCount).
|
|
570
|
-
if (!result.bufferReset && result.lineCount === lastCapturedLine && !sessionState?.inProgressMessageId) {
|
|
896
|
+
if (!isFullScreenTui && !result.bufferReset && result.lineCount === lastCapturedLine && !sessionState?.inProgressMessageId) {
|
|
571
897
|
return false;
|
|
572
898
|
}
|
|
573
899
|
// Additional duplicate prevention: check if savePendingAssistantResponse
|
|
574
900
|
// already saved this content by comparing line counts.
|
|
575
901
|
// Issue #372: Skip this check when buffer reset is detected (TUI redraw, screen clear).
|
|
576
902
|
// Codex TUI redraws cause totalLines to shrink, making lineCount < lastCapturedLine.
|
|
577
|
-
if (!result.bufferReset && result.lineCount <= lastCapturedLine) {
|
|
903
|
+
if (!result.bufferReset && !isFullScreenTui && result.lineCount <= lastCapturedLine) {
|
|
578
904
|
console.log(`[checkForResponse] Already saved up to line ${lastCapturedLine}, skipping (result: ${result.lineCount})`);
|
|
579
905
|
return false;
|
|
580
906
|
}
|
|
@@ -611,6 +937,7 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
611
937
|
? (0, claude_output_1.parseClaudeOutput)(result.response)
|
|
612
938
|
: undefined;
|
|
613
939
|
// Clean up responses (remove shell prompts, setup commands, and errors)
|
|
940
|
+
// [D2-009] Each tool has its own clean function for tool-specific artifacts
|
|
614
941
|
let cleanedResponse = result.response;
|
|
615
942
|
if (cliToolId === 'gemini') {
|
|
616
943
|
cleanedResponse = cleanGeminiResponse(result.response);
|
|
@@ -618,6 +945,14 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
618
945
|
else if (cliToolId === 'claude') {
|
|
619
946
|
cleanedResponse = cleanClaudeResponse(result.response);
|
|
620
947
|
}
|
|
948
|
+
else if (cliToolId === 'opencode') {
|
|
949
|
+
cleanedResponse = cleanOpenCodeResponse(result.response);
|
|
950
|
+
// Clear accumulator for next response cycle (Layer 2 data not used for final content;
|
|
951
|
+
// accumulatedContent includes all past Q&A history from the fixed-size TUI, causing
|
|
952
|
+
// old responses to leak into the saved message even after cleanOpenCodeResponse trimming).
|
|
953
|
+
const pollerKey = getPollerKey(worktreeId, cliToolId);
|
|
954
|
+
clearTuiAccumulator(pollerKey);
|
|
955
|
+
}
|
|
621
956
|
// If cleaned response is empty or just "[No content]", skip saving
|
|
622
957
|
// This prevents creating messages for shell setup commands that get filtered out
|
|
623
958
|
if (!cleanedResponse || cleanedResponse.trim() === '' || cleanedResponse === '[No content]') {
|
|
@@ -637,8 +972,9 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
637
972
|
}
|
|
638
973
|
// Race condition prevention: re-check session state before saving
|
|
639
974
|
// savePendingAssistantResponse may have already saved this content concurrently
|
|
975
|
+
// Issue #379: Skip for OpenCode full-screen TUI (fixed buffer size, lineCount never advances)
|
|
640
976
|
const currentSessionState = (0, db_1.getSessionState)(db, worktreeId, cliToolId);
|
|
641
|
-
if (currentSessionState && result.lineCount <= currentSessionState.lastCapturedLine) {
|
|
977
|
+
if (!isFullScreenTui && currentSessionState && result.lineCount <= currentSessionState.lastCapturedLine) {
|
|
642
978
|
console.log(`[checkForResponse] Race condition detected, skipping save (result: ${result.lineCount}, current: ${currentSessionState.lastCapturedLine})`);
|
|
643
979
|
return false;
|
|
644
980
|
}
|
|
@@ -659,6 +995,12 @@ async function checkForResponse(worktreeId, cliToolId) {
|
|
|
659
995
|
(0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
|
|
660
996
|
// Update session state
|
|
661
997
|
(0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
|
|
998
|
+
// For full-screen TUIs (OpenCode), stop polling after saving the response.
|
|
999
|
+
// Line-count based duplicate prevention doesn't work because the pane size is fixed,
|
|
1000
|
+
// so lineCount never advances. Polling restarts when the user sends the next message.
|
|
1001
|
+
if (isFullScreenTui) {
|
|
1002
|
+
stopPolling(worktreeId, cliToolId);
|
|
1003
|
+
}
|
|
662
1004
|
return true;
|
|
663
1005
|
}
|
|
664
1006
|
catch (error) {
|
|
@@ -684,6 +1026,10 @@ function startPolling(worktreeId, cliToolId) {
|
|
|
684
1026
|
stopPolling(worktreeId, cliToolId);
|
|
685
1027
|
// Record start time
|
|
686
1028
|
pollingStartTimes.set(pollerKey, Date.now());
|
|
1029
|
+
// Initialize TUI accumulator for OpenCode (Layer 2 safety net)
|
|
1030
|
+
if (cliToolId === 'opencode') {
|
|
1031
|
+
initTuiAccumulator(pollerKey);
|
|
1032
|
+
}
|
|
687
1033
|
// Start polling with setTimeout chain to prevent race conditions
|
|
688
1034
|
scheduleNextResponsePoll(worktreeId, cliToolId);
|
|
689
1035
|
}
|
|
@@ -731,6 +1077,8 @@ function stopPolling(worktreeId, cliToolId) {
|
|
|
731
1077
|
activePollers.delete(pollerKey);
|
|
732
1078
|
pollingStartTimes.delete(pollerKey);
|
|
733
1079
|
}
|
|
1080
|
+
// Clean up TUI accumulator if present
|
|
1081
|
+
clearTuiAccumulator(pollerKey);
|
|
734
1082
|
}
|
|
735
1083
|
/**
|
|
736
1084
|
* Stop all active pollers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commandmate",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Git worktree management with Claude CLI and tmux sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"react": "^18.3.0",
|
|
66
66
|
"react-dom": "^18.3.0",
|
|
67
67
|
"react-markdown": "^10.1.0",
|
|
68
|
+
"react-qr-code": "^2.0.18",
|
|
68
69
|
"rehype-highlight": "^7.0.2",
|
|
69
70
|
"rehype-sanitize": "^6.0.0",
|
|
70
71
|
"remark-gfm": "^4.0.1",
|