@masslessai/push-todo 4.1.7 → 4.1.8

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.
Files changed (2) hide show
  1. package/lib/daemon.js +68 -2
  2. package/package.json +1 -1
package/lib/daemon.js CHANGED
@@ -1482,11 +1482,38 @@ function maybesSendStreamProgress(displayNumber, activity) {
1482
1482
  filesRead: activity.filesRead.size
1483
1483
  }
1484
1484
  })
1485
+ }).then(async (response) => {
1486
+ // Check for cancellation signal in response
1487
+ const result = await response.json().catch(() => null);
1488
+ if (result?.status === 'cancelling') {
1489
+ handleCancellation(displayNumber, 'stream_progress');
1490
+ }
1485
1491
  }).catch(err => {
1486
1492
  log(`Task #${displayNumber}: stream progress failed (non-fatal): ${err.message}`);
1487
1493
  });
1488
1494
  }
1489
1495
 
1496
+ // ==================== Cancellation (Phase 2) ====================
1497
+
1498
+ function handleCancellation(displayNumber, source) {
1499
+ const taskInfo = runningTasks.get(displayNumber);
1500
+ if (!taskInfo) return;
1501
+
1502
+ log(`Task #${displayNumber}: cancellation detected via ${source}, sending SIGTERM`);
1503
+
1504
+ try {
1505
+ taskInfo.process.kill('SIGTERM');
1506
+ } catch (err) {
1507
+ log(`Task #${displayNumber}: SIGTERM failed: ${err.message}`);
1508
+ }
1509
+
1510
+ // Mark as cancelled so handleTaskCompletion reports the right reason
1511
+ updateTaskDetail(displayNumber, {
1512
+ phase: 'cancelled',
1513
+ detail: 'Cancelled by user'
1514
+ });
1515
+ }
1516
+
1490
1517
  function monitorTaskStdout(displayNumber, proc) {
1491
1518
  if (!proc.stdout) return;
1492
1519
 
@@ -1623,6 +1650,12 @@ async function sendProgressHeartbeats() {
1623
1650
  }
1624
1651
  // No status field — event-only update, won't change execution_status
1625
1652
  })
1653
+ }).then(async (response) => {
1654
+ // Check for cancellation signal in heartbeat response
1655
+ const result = await response.json().catch(() => null);
1656
+ if (result?.status === 'cancelling') {
1657
+ handleCancellation(displayNumber, 'heartbeat');
1658
+ }
1626
1659
  }).catch(err => {
1627
1660
  log(`Task #${displayNumber}: heartbeat failed (non-fatal): ${err.message}`);
1628
1661
  });
@@ -2162,12 +2195,45 @@ async function handleTaskCompletion(displayNumber, exitCode) {
2162
2195
  const summary = info.summary || 'Unknown task';
2163
2196
  const projectPath = taskProjectPaths.get(displayNumber);
2164
2197
 
2165
- log(`Task #${displayNumber} completed with code ${exitCode} (${duration}s)`);
2198
+ const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
2199
+ const wasCancelled = info.phase === 'cancelled';
2200
+ log(`Task #${displayNumber} ${wasCancelled ? 'cancelled' : 'completed'} with code ${exitCode} (${duration}s)`);
2201
+
2202
+ // Handle user cancellation — report as failed with clear reason, skip PR/merge/summary
2203
+ if (wasCancelled) {
2204
+ const cancelMsg = `Cancelled by user after ${durationStr}.`;
2205
+ await updateTaskStatus(displayNumber, 'failed', {
2206
+ error: cancelMsg,
2207
+ sessionId: taskInfo.sessionId
2208
+ }, info.taskId);
2209
+
2210
+ cleanupWorktree(displayNumber, projectPath);
2211
+
2212
+ trackCompleted({
2213
+ displayNumber,
2214
+ summary,
2215
+ completedAt: new Date().toISOString(),
2216
+ duration,
2217
+ status: 'cancelled'
2218
+ });
2219
+
2220
+ // Cleanup internal tracking
2221
+ taskDetails.delete(displayNumber);
2222
+ taskLastOutput.delete(displayNumber);
2223
+ taskStdoutBuffer.delete(displayNumber);
2224
+ taskStderrBuffer.delete(displayNumber);
2225
+ taskProjectPaths.delete(displayNumber);
2226
+ taskLastHeartbeat.delete(displayNumber);
2227
+ taskStreamLineBuffer.delete(displayNumber);
2228
+ taskActivityState.delete(displayNumber);
2229
+ taskLastStreamProgress.delete(displayNumber);
2230
+ updateStatusFile();
2231
+ return;
2232
+ }
2166
2233
 
2167
2234
  // Session ID: prefer pre-assigned ID (reliable), fall back to stdout extraction
2168
2235
  const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || [], displayNumber);
2169
2236
  const worktreePath = getWorktreePath(displayNumber, projectPath);
2170
- const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
2171
2237
  const machineName = getMachineName() || 'Mac';
2172
2238
 
2173
2239
  if (sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.1.7",
3
+ "version": "4.1.8",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {