@bbearai/core 0.7.2 → 0.8.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/dist/index.d.mts CHANGED
@@ -377,6 +377,8 @@ interface TestAssignment {
377
377
  startedAt?: string;
378
378
  /** Duration in seconds (calculated when completed) */
379
379
  durationSeconds?: number;
380
+ /** Active time in seconds, computed from presence heartbeats */
381
+ activeSeconds?: number;
380
382
  /** Whether this is a verification assignment for a fixed bug */
381
383
  isVerification?: boolean;
382
384
  /** Original report ID if this is a verification assignment */
@@ -479,6 +481,7 @@ interface TesterThread {
479
481
  createdAt: string;
480
482
  unreadCount: number;
481
483
  lastMessage?: TesterMessage;
484
+ reporterName?: string;
482
485
  }
483
486
  interface TesterMessage {
484
487
  id: string;
@@ -788,6 +791,14 @@ interface TesterIssue {
788
791
  originalBugId?: string;
789
792
  /** Original bug title (for reopened/test_fail issues) */
790
793
  originalBugTitle?: string;
794
+ /** Resolution notes from the developer (for ready_to_test issues) */
795
+ resolutionNotes?: string;
796
+ /** Fix commit SHA (for ready_to_test issues) */
797
+ fixCommitSha?: string;
798
+ /** Fix commit message */
799
+ fixCommitMessage?: string;
800
+ /** Files changed in the fix */
801
+ fixFilesChanged?: string[];
791
802
  }
792
803
  /** Delivery status for captured emails */
793
804
  type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
@@ -1102,6 +1113,12 @@ declare class BugBearClient {
1102
1113
  private initialized;
1103
1114
  /** Initialization error, if any. */
1104
1115
  private initError;
1116
+ /** Active presence session ID (passive time tracking). */
1117
+ private _presenceSessionId;
1118
+ /** Heartbeat interval for presence tracking. */
1119
+ private _presenceInterval;
1120
+ /** Whether presence is paused (tab hidden / app backgrounded). */
1121
+ private _presencePaused;
1105
1122
  constructor(config: BugBearConfig);
1106
1123
  /** Whether the client is ready for requests. */
1107
1124
  get isReady(): boolean;
@@ -1561,6 +1578,22 @@ declare class BugBearClient {
1561
1578
  * Transform database finding to QAFinding type
1562
1579
  */
1563
1580
  private transformFinding;
1581
+ /** Current presence session ID (null if not tracking). */
1582
+ get presenceSessionId(): string | null;
1583
+ /**
1584
+ * Start passive presence tracking for this tester.
1585
+ * Idempotent — reuses an existing active session if one exists.
1586
+ */
1587
+ startPresence(platform: 'web' | 'ios' | 'android'): Promise<string | null>;
1588
+ /** Gracefully end the current presence session. */
1589
+ endPresence(): Promise<void>;
1590
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
1591
+ pausePresence(): void;
1592
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
1593
+ resumePresence(): Promise<void>;
1594
+ private heartbeatPresence;
1595
+ private startPresenceHeartbeat;
1596
+ private stopPresenceHeartbeat;
1564
1597
  }
1565
1598
  /**
1566
1599
  * Create a BugBear client instance
package/dist/index.d.ts CHANGED
@@ -377,6 +377,8 @@ interface TestAssignment {
377
377
  startedAt?: string;
378
378
  /** Duration in seconds (calculated when completed) */
379
379
  durationSeconds?: number;
380
+ /** Active time in seconds, computed from presence heartbeats */
381
+ activeSeconds?: number;
380
382
  /** Whether this is a verification assignment for a fixed bug */
381
383
  isVerification?: boolean;
382
384
  /** Original report ID if this is a verification assignment */
@@ -479,6 +481,7 @@ interface TesterThread {
479
481
  createdAt: string;
480
482
  unreadCount: number;
481
483
  lastMessage?: TesterMessage;
484
+ reporterName?: string;
482
485
  }
483
486
  interface TesterMessage {
484
487
  id: string;
@@ -788,6 +791,14 @@ interface TesterIssue {
788
791
  originalBugId?: string;
789
792
  /** Original bug title (for reopened/test_fail issues) */
790
793
  originalBugTitle?: string;
794
+ /** Resolution notes from the developer (for ready_to_test issues) */
795
+ resolutionNotes?: string;
796
+ /** Fix commit SHA (for ready_to_test issues) */
797
+ fixCommitSha?: string;
798
+ /** Fix commit message */
799
+ fixCommitMessage?: string;
800
+ /** Files changed in the fix */
801
+ fixFilesChanged?: string[];
791
802
  }
792
803
  /** Delivery status for captured emails */
793
804
  type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
@@ -1102,6 +1113,12 @@ declare class BugBearClient {
1102
1113
  private initialized;
1103
1114
  /** Initialization error, if any. */
1104
1115
  private initError;
1116
+ /** Active presence session ID (passive time tracking). */
1117
+ private _presenceSessionId;
1118
+ /** Heartbeat interval for presence tracking. */
1119
+ private _presenceInterval;
1120
+ /** Whether presence is paused (tab hidden / app backgrounded). */
1121
+ private _presencePaused;
1105
1122
  constructor(config: BugBearConfig);
1106
1123
  /** Whether the client is ready for requests. */
1107
1124
  get isReady(): boolean;
@@ -1561,6 +1578,22 @@ declare class BugBearClient {
1561
1578
  * Transform database finding to QAFinding type
1562
1579
  */
1563
1580
  private transformFinding;
1581
+ /** Current presence session ID (null if not tracking). */
1582
+ get presenceSessionId(): string | null;
1583
+ /**
1584
+ * Start passive presence tracking for this tester.
1585
+ * Idempotent — reuses an existing active session if one exists.
1586
+ */
1587
+ startPresence(platform: 'web' | 'ios' | 'android'): Promise<string | null>;
1588
+ /** Gracefully end the current presence session. */
1589
+ endPresence(): Promise<void>;
1590
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
1591
+ pausePresence(): void;
1592
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
1593
+ resumePresence(): Promise<void>;
1594
+ private heartbeatPresence;
1595
+ private startPresenceHeartbeat;
1596
+ private stopPresenceHeartbeat;
1564
1597
  }
1565
1598
  /**
1566
1599
  * Create a BugBear client instance
package/dist/index.js CHANGED
@@ -914,6 +914,12 @@ var BugBearClient = class {
914
914
  this.initialized = false;
915
915
  /** Initialization error, if any. */
916
916
  this.initError = null;
917
+ /** Active presence session ID (passive time tracking). */
918
+ this._presenceSessionId = null;
919
+ /** Heartbeat interval for presence tracking. */
920
+ this._presenceInterval = null;
921
+ /** Whether presence is paused (tab hidden / app backgrounded). */
922
+ this._presencePaused = false;
917
923
  this.config = config;
918
924
  if (config.apiKey) {
919
925
  this.pendingInit = this.resolveFromApiKey(config.apiKey);
@@ -1469,55 +1475,66 @@ var BugBearClient = class {
1469
1475
  ...pendingResult.data || [],
1470
1476
  ...completedResult.data || []
1471
1477
  ];
1472
- const mapItem = (item) => ({
1473
- id: item.id,
1474
- status: item.status,
1475
- startedAt: item.started_at,
1476
- skipReason: item.skip_reason,
1477
- isVerification: item.is_verification || false,
1478
- originalReportId: item.original_report_id,
1479
- testCase: {
1480
- id: item.test_case.id,
1481
- title: item.test_case.title,
1482
- testKey: item.test_case.test_key,
1483
- description: item.test_case.description,
1484
- steps: item.test_case.steps,
1485
- expectedResult: item.test_case.expected_result,
1486
- priority: item.test_case.priority,
1487
- targetRoute: item.test_case.target_route,
1488
- track: item.test_case.track ? {
1489
- id: item.test_case.track.id,
1490
- name: item.test_case.track.name,
1491
- icon: item.test_case.track.icon,
1492
- color: item.test_case.track.color,
1493
- testTemplate: item.test_case.track.test_template,
1494
- rubricMode: item.test_case.track.rubric_mode || "pass_fail",
1495
- description: item.test_case.track.description
1496
- } : void 0,
1497
- group: item.test_case.group ? {
1498
- id: item.test_case.group.id,
1499
- name: item.test_case.group.name,
1500
- description: item.test_case.group.description,
1501
- sortOrder: item.test_case.group.sort_order
1502
- } : void 0,
1503
- role: item.test_case.role ? {
1504
- id: item.test_case.role.id,
1505
- name: item.test_case.role.name,
1506
- slug: item.test_case.role.slug,
1507
- color: item.test_case.role.color,
1508
- description: item.test_case.role.description,
1509
- loginHint: item.test_case.role.login_hint
1510
- } : void 0,
1511
- platforms: item.test_case.platforms || void 0
1512
- }
1513
- });
1514
- const mapped = allData.filter((item) => {
1515
- if (!item.test_case) {
1516
- console.warn("BugBear: Assignment returned without test_case", { id: item.id });
1517
- return false;
1518
- }
1519
- return true;
1520
- }).map(mapItem);
1478
+ const mapItem = (item) => {
1479
+ const tc = item.test_case;
1480
+ return {
1481
+ id: item.id,
1482
+ status: item.status,
1483
+ startedAt: item.started_at,
1484
+ skipReason: item.skip_reason,
1485
+ isVerification: item.is_verification || false,
1486
+ originalReportId: item.original_report_id,
1487
+ testCase: tc ? {
1488
+ id: tc.id,
1489
+ title: tc.title,
1490
+ testKey: tc.test_key,
1491
+ description: tc.description,
1492
+ steps: tc.steps,
1493
+ expectedResult: tc.expected_result,
1494
+ priority: tc.priority,
1495
+ targetRoute: tc.target_route,
1496
+ track: tc.track ? {
1497
+ id: tc.track.id,
1498
+ name: tc.track.name,
1499
+ icon: tc.track.icon,
1500
+ color: tc.track.color,
1501
+ testTemplate: tc.track.test_template,
1502
+ rubricMode: tc.track.rubric_mode || "pass_fail",
1503
+ description: tc.track.description
1504
+ } : void 0,
1505
+ group: tc.group ? {
1506
+ id: tc.group.id,
1507
+ name: tc.group.name,
1508
+ description: tc.group.description,
1509
+ sortOrder: tc.group.sort_order
1510
+ } : void 0,
1511
+ role: tc.role ? {
1512
+ id: tc.role.id,
1513
+ name: tc.role.name,
1514
+ slug: tc.role.slug,
1515
+ color: tc.role.color,
1516
+ description: tc.role.description,
1517
+ loginHint: tc.role.login_hint
1518
+ } : void 0,
1519
+ platforms: tc.platforms || void 0
1520
+ } : {
1521
+ // Standalone verification assignment (bug reported without a test case)
1522
+ id: item.original_report_id || item.id,
1523
+ title: item.notes || "Bug Verification",
1524
+ testKey: "VERIFY",
1525
+ description: "Verify that the reported bug has been fixed",
1526
+ steps: [],
1527
+ expectedResult: "The bug should no longer be reproducible",
1528
+ priority: "P1",
1529
+ targetRoute: void 0,
1530
+ track: void 0,
1531
+ group: void 0,
1532
+ role: void 0,
1533
+ platforms: void 0
1534
+ }
1535
+ };
1536
+ };
1537
+ const mapped = allData.map(mapItem);
1521
1538
  mapped.sort((a, b) => {
1522
1539
  if (a.isVerification && !b.isVerification) return -1;
1523
1540
  if (!a.isVerification && b.isVerification) return 1;
@@ -1611,7 +1628,7 @@ var BugBearClient = class {
1611
1628
  async updateAssignmentStatus(assignmentId, status, options) {
1612
1629
  try {
1613
1630
  await this.ensureReady();
1614
- const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
1631
+ const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at, tester_id, project_id").eq("id", assignmentId).single();
1615
1632
  if (fetchError || !currentAssignment) {
1616
1633
  console.error("BugBear: Assignment not found", {
1617
1634
  message: fetchError?.message,
@@ -1632,6 +1649,19 @@ var BugBearClient = class {
1632
1649
  const completedAt = /* @__PURE__ */ new Date();
1633
1650
  durationSeconds = Math.round((completedAt.getTime() - startedAt.getTime()) / 1e3);
1634
1651
  updateData.duration_seconds = durationSeconds;
1652
+ if (currentAssignment.tester_id && currentAssignment.project_id) {
1653
+ try {
1654
+ const { data: activeTime } = await this.supabase.rpc("compute_assignment_active_time", {
1655
+ p_tester_id: currentAssignment.tester_id,
1656
+ p_project_id: currentAssignment.project_id,
1657
+ p_started_at: currentAssignment.started_at,
1658
+ p_completed_at: updateData.completed_at
1659
+ });
1660
+ updateData.active_seconds = typeof activeTime === "number" ? activeTime : Math.min(durationSeconds, 1800);
1661
+ } catch {
1662
+ updateData.active_seconds = Math.min(durationSeconds, 1800);
1663
+ }
1664
+ }
1635
1665
  }
1636
1666
  }
1637
1667
  if (options?.notes) {
@@ -1710,6 +1740,7 @@ var BugBearClient = class {
1710
1740
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
1711
1741
  completed_at: null,
1712
1742
  duration_seconds: null,
1743
+ active_seconds: null,
1713
1744
  skip_reason: null
1714
1745
  }).eq("id", assignmentId).eq("status", current.status);
1715
1746
  if (error) {
@@ -2017,7 +2048,11 @@ var BugBearClient = class {
2017
2048
  verifiedByName: row.verified_by_name || void 0,
2018
2049
  verifiedAt: row.verified_at || void 0,
2019
2050
  originalBugId: row.original_bug_id || void 0,
2020
- originalBugTitle: row.original_bug_title || void 0
2051
+ originalBugTitle: row.original_bug_title || void 0,
2052
+ resolutionNotes: row.resolution_notes || void 0,
2053
+ fixCommitSha: row.code_context?.fix?.commit_sha || void 0,
2054
+ fixCommitMessage: row.code_context?.fix?.commit_message || void 0,
2055
+ fixFilesChanged: row.code_context?.fix?.files_changed || void 0
2021
2056
  }));
2022
2057
  } catch (err) {
2023
2058
  console.error("BugBear: Error fetching issues", err);
@@ -2559,6 +2594,7 @@ var BugBearClient = class {
2559
2594
  lastMessageAt: row.last_message_at,
2560
2595
  createdAt: row.created_at,
2561
2596
  unreadCount: Number(row.unread_count) || 0,
2597
+ reporterName: row.reporter_name || void 0,
2562
2598
  lastMessage: row.last_message_preview ? {
2563
2599
  id: "",
2564
2600
  threadId: row.thread_id,
@@ -2998,6 +3034,93 @@ var BugBearClient = class {
2998
3034
  updatedAt: data.updated_at
2999
3035
  };
3000
3036
  }
3037
+ // ─── Passive Presence Tracking ──────────────────────────────
3038
+ /** Current presence session ID (null if not tracking). */
3039
+ get presenceSessionId() {
3040
+ return this._presenceSessionId;
3041
+ }
3042
+ /**
3043
+ * Start passive presence tracking for this tester.
3044
+ * Idempotent — reuses an existing active session if one exists.
3045
+ */
3046
+ async startPresence(platform) {
3047
+ try {
3048
+ await this.ensureReady();
3049
+ const testerInfo = await this.getTesterInfo();
3050
+ if (!testerInfo) return null;
3051
+ const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
3052
+ p_project_id: this.config.projectId,
3053
+ p_tester_id: testerInfo.id,
3054
+ p_platform: platform
3055
+ });
3056
+ if (error) {
3057
+ console.error("BugBear: Failed to start presence", formatPgError(error));
3058
+ return null;
3059
+ }
3060
+ this._presenceSessionId = data;
3061
+ this._presencePaused = false;
3062
+ this.startPresenceHeartbeat();
3063
+ return data;
3064
+ } catch (err) {
3065
+ console.error("BugBear: Error starting presence", err);
3066
+ return null;
3067
+ }
3068
+ }
3069
+ /** Gracefully end the current presence session. */
3070
+ async endPresence() {
3071
+ this.stopPresenceHeartbeat();
3072
+ if (!this._presenceSessionId) return;
3073
+ try {
3074
+ await this.supabase.rpc("end_tester_presence", {
3075
+ p_session_id: this._presenceSessionId
3076
+ });
3077
+ } catch {
3078
+ }
3079
+ this._presenceSessionId = null;
3080
+ }
3081
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
3082
+ pausePresence() {
3083
+ this._presencePaused = true;
3084
+ this.heartbeatPresence();
3085
+ }
3086
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
3087
+ async resumePresence() {
3088
+ if (!this._presenceSessionId) return;
3089
+ this._presencePaused = false;
3090
+ try {
3091
+ const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
3092
+ p_session_id: this._presenceSessionId
3093
+ });
3094
+ if (!data) {
3095
+ this._presenceSessionId = null;
3096
+ }
3097
+ } catch {
3098
+ this._presenceSessionId = null;
3099
+ }
3100
+ }
3101
+ async heartbeatPresence() {
3102
+ if (!this._presenceSessionId || this._presencePaused) return;
3103
+ try {
3104
+ const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
3105
+ p_session_id: this._presenceSessionId
3106
+ });
3107
+ if (error || data === false) {
3108
+ this.stopPresenceHeartbeat();
3109
+ this._presenceSessionId = null;
3110
+ }
3111
+ } catch {
3112
+ }
3113
+ }
3114
+ startPresenceHeartbeat() {
3115
+ this.stopPresenceHeartbeat();
3116
+ this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
3117
+ }
3118
+ stopPresenceHeartbeat() {
3119
+ if (this._presenceInterval) {
3120
+ clearInterval(this._presenceInterval);
3121
+ this._presenceInterval = null;
3122
+ }
3123
+ }
3001
3124
  };
3002
3125
  function createBugBear(config) {
3003
3126
  return new BugBearClient(config);
package/dist/index.mjs CHANGED
@@ -868,6 +868,12 @@ var BugBearClient = class {
868
868
  this.initialized = false;
869
869
  /** Initialization error, if any. */
870
870
  this.initError = null;
871
+ /** Active presence session ID (passive time tracking). */
872
+ this._presenceSessionId = null;
873
+ /** Heartbeat interval for presence tracking. */
874
+ this._presenceInterval = null;
875
+ /** Whether presence is paused (tab hidden / app backgrounded). */
876
+ this._presencePaused = false;
871
877
  this.config = config;
872
878
  if (config.apiKey) {
873
879
  this.pendingInit = this.resolveFromApiKey(config.apiKey);
@@ -1423,55 +1429,66 @@ var BugBearClient = class {
1423
1429
  ...pendingResult.data || [],
1424
1430
  ...completedResult.data || []
1425
1431
  ];
1426
- const mapItem = (item) => ({
1427
- id: item.id,
1428
- status: item.status,
1429
- startedAt: item.started_at,
1430
- skipReason: item.skip_reason,
1431
- isVerification: item.is_verification || false,
1432
- originalReportId: item.original_report_id,
1433
- testCase: {
1434
- id: item.test_case.id,
1435
- title: item.test_case.title,
1436
- testKey: item.test_case.test_key,
1437
- description: item.test_case.description,
1438
- steps: item.test_case.steps,
1439
- expectedResult: item.test_case.expected_result,
1440
- priority: item.test_case.priority,
1441
- targetRoute: item.test_case.target_route,
1442
- track: item.test_case.track ? {
1443
- id: item.test_case.track.id,
1444
- name: item.test_case.track.name,
1445
- icon: item.test_case.track.icon,
1446
- color: item.test_case.track.color,
1447
- testTemplate: item.test_case.track.test_template,
1448
- rubricMode: item.test_case.track.rubric_mode || "pass_fail",
1449
- description: item.test_case.track.description
1450
- } : void 0,
1451
- group: item.test_case.group ? {
1452
- id: item.test_case.group.id,
1453
- name: item.test_case.group.name,
1454
- description: item.test_case.group.description,
1455
- sortOrder: item.test_case.group.sort_order
1456
- } : void 0,
1457
- role: item.test_case.role ? {
1458
- id: item.test_case.role.id,
1459
- name: item.test_case.role.name,
1460
- slug: item.test_case.role.slug,
1461
- color: item.test_case.role.color,
1462
- description: item.test_case.role.description,
1463
- loginHint: item.test_case.role.login_hint
1464
- } : void 0,
1465
- platforms: item.test_case.platforms || void 0
1466
- }
1467
- });
1468
- const mapped = allData.filter((item) => {
1469
- if (!item.test_case) {
1470
- console.warn("BugBear: Assignment returned without test_case", { id: item.id });
1471
- return false;
1472
- }
1473
- return true;
1474
- }).map(mapItem);
1432
+ const mapItem = (item) => {
1433
+ const tc = item.test_case;
1434
+ return {
1435
+ id: item.id,
1436
+ status: item.status,
1437
+ startedAt: item.started_at,
1438
+ skipReason: item.skip_reason,
1439
+ isVerification: item.is_verification || false,
1440
+ originalReportId: item.original_report_id,
1441
+ testCase: tc ? {
1442
+ id: tc.id,
1443
+ title: tc.title,
1444
+ testKey: tc.test_key,
1445
+ description: tc.description,
1446
+ steps: tc.steps,
1447
+ expectedResult: tc.expected_result,
1448
+ priority: tc.priority,
1449
+ targetRoute: tc.target_route,
1450
+ track: tc.track ? {
1451
+ id: tc.track.id,
1452
+ name: tc.track.name,
1453
+ icon: tc.track.icon,
1454
+ color: tc.track.color,
1455
+ testTemplate: tc.track.test_template,
1456
+ rubricMode: tc.track.rubric_mode || "pass_fail",
1457
+ description: tc.track.description
1458
+ } : void 0,
1459
+ group: tc.group ? {
1460
+ id: tc.group.id,
1461
+ name: tc.group.name,
1462
+ description: tc.group.description,
1463
+ sortOrder: tc.group.sort_order
1464
+ } : void 0,
1465
+ role: tc.role ? {
1466
+ id: tc.role.id,
1467
+ name: tc.role.name,
1468
+ slug: tc.role.slug,
1469
+ color: tc.role.color,
1470
+ description: tc.role.description,
1471
+ loginHint: tc.role.login_hint
1472
+ } : void 0,
1473
+ platforms: tc.platforms || void 0
1474
+ } : {
1475
+ // Standalone verification assignment (bug reported without a test case)
1476
+ id: item.original_report_id || item.id,
1477
+ title: item.notes || "Bug Verification",
1478
+ testKey: "VERIFY",
1479
+ description: "Verify that the reported bug has been fixed",
1480
+ steps: [],
1481
+ expectedResult: "The bug should no longer be reproducible",
1482
+ priority: "P1",
1483
+ targetRoute: void 0,
1484
+ track: void 0,
1485
+ group: void 0,
1486
+ role: void 0,
1487
+ platforms: void 0
1488
+ }
1489
+ };
1490
+ };
1491
+ const mapped = allData.map(mapItem);
1475
1492
  mapped.sort((a, b) => {
1476
1493
  if (a.isVerification && !b.isVerification) return -1;
1477
1494
  if (!a.isVerification && b.isVerification) return 1;
@@ -1565,7 +1582,7 @@ var BugBearClient = class {
1565
1582
  async updateAssignmentStatus(assignmentId, status, options) {
1566
1583
  try {
1567
1584
  await this.ensureReady();
1568
- const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
1585
+ const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at, tester_id, project_id").eq("id", assignmentId).single();
1569
1586
  if (fetchError || !currentAssignment) {
1570
1587
  console.error("BugBear: Assignment not found", {
1571
1588
  message: fetchError?.message,
@@ -1586,6 +1603,19 @@ var BugBearClient = class {
1586
1603
  const completedAt = /* @__PURE__ */ new Date();
1587
1604
  durationSeconds = Math.round((completedAt.getTime() - startedAt.getTime()) / 1e3);
1588
1605
  updateData.duration_seconds = durationSeconds;
1606
+ if (currentAssignment.tester_id && currentAssignment.project_id) {
1607
+ try {
1608
+ const { data: activeTime } = await this.supabase.rpc("compute_assignment_active_time", {
1609
+ p_tester_id: currentAssignment.tester_id,
1610
+ p_project_id: currentAssignment.project_id,
1611
+ p_started_at: currentAssignment.started_at,
1612
+ p_completed_at: updateData.completed_at
1613
+ });
1614
+ updateData.active_seconds = typeof activeTime === "number" ? activeTime : Math.min(durationSeconds, 1800);
1615
+ } catch {
1616
+ updateData.active_seconds = Math.min(durationSeconds, 1800);
1617
+ }
1618
+ }
1589
1619
  }
1590
1620
  }
1591
1621
  if (options?.notes) {
@@ -1664,6 +1694,7 @@ var BugBearClient = class {
1664
1694
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
1665
1695
  completed_at: null,
1666
1696
  duration_seconds: null,
1697
+ active_seconds: null,
1667
1698
  skip_reason: null
1668
1699
  }).eq("id", assignmentId).eq("status", current.status);
1669
1700
  if (error) {
@@ -1971,7 +2002,11 @@ var BugBearClient = class {
1971
2002
  verifiedByName: row.verified_by_name || void 0,
1972
2003
  verifiedAt: row.verified_at || void 0,
1973
2004
  originalBugId: row.original_bug_id || void 0,
1974
- originalBugTitle: row.original_bug_title || void 0
2005
+ originalBugTitle: row.original_bug_title || void 0,
2006
+ resolutionNotes: row.resolution_notes || void 0,
2007
+ fixCommitSha: row.code_context?.fix?.commit_sha || void 0,
2008
+ fixCommitMessage: row.code_context?.fix?.commit_message || void 0,
2009
+ fixFilesChanged: row.code_context?.fix?.files_changed || void 0
1975
2010
  }));
1976
2011
  } catch (err) {
1977
2012
  console.error("BugBear: Error fetching issues", err);
@@ -2513,6 +2548,7 @@ var BugBearClient = class {
2513
2548
  lastMessageAt: row.last_message_at,
2514
2549
  createdAt: row.created_at,
2515
2550
  unreadCount: Number(row.unread_count) || 0,
2551
+ reporterName: row.reporter_name || void 0,
2516
2552
  lastMessage: row.last_message_preview ? {
2517
2553
  id: "",
2518
2554
  threadId: row.thread_id,
@@ -2952,6 +2988,93 @@ var BugBearClient = class {
2952
2988
  updatedAt: data.updated_at
2953
2989
  };
2954
2990
  }
2991
+ // ─── Passive Presence Tracking ──────────────────────────────
2992
+ /** Current presence session ID (null if not tracking). */
2993
+ get presenceSessionId() {
2994
+ return this._presenceSessionId;
2995
+ }
2996
+ /**
2997
+ * Start passive presence tracking for this tester.
2998
+ * Idempotent — reuses an existing active session if one exists.
2999
+ */
3000
+ async startPresence(platform) {
3001
+ try {
3002
+ await this.ensureReady();
3003
+ const testerInfo = await this.getTesterInfo();
3004
+ if (!testerInfo) return null;
3005
+ const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
3006
+ p_project_id: this.config.projectId,
3007
+ p_tester_id: testerInfo.id,
3008
+ p_platform: platform
3009
+ });
3010
+ if (error) {
3011
+ console.error("BugBear: Failed to start presence", formatPgError(error));
3012
+ return null;
3013
+ }
3014
+ this._presenceSessionId = data;
3015
+ this._presencePaused = false;
3016
+ this.startPresenceHeartbeat();
3017
+ return data;
3018
+ } catch (err) {
3019
+ console.error("BugBear: Error starting presence", err);
3020
+ return null;
3021
+ }
3022
+ }
3023
+ /** Gracefully end the current presence session. */
3024
+ async endPresence() {
3025
+ this.stopPresenceHeartbeat();
3026
+ if (!this._presenceSessionId) return;
3027
+ try {
3028
+ await this.supabase.rpc("end_tester_presence", {
3029
+ p_session_id: this._presenceSessionId
3030
+ });
3031
+ } catch {
3032
+ }
3033
+ this._presenceSessionId = null;
3034
+ }
3035
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
3036
+ pausePresence() {
3037
+ this._presencePaused = true;
3038
+ this.heartbeatPresence();
3039
+ }
3040
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
3041
+ async resumePresence() {
3042
+ if (!this._presenceSessionId) return;
3043
+ this._presencePaused = false;
3044
+ try {
3045
+ const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
3046
+ p_session_id: this._presenceSessionId
3047
+ });
3048
+ if (!data) {
3049
+ this._presenceSessionId = null;
3050
+ }
3051
+ } catch {
3052
+ this._presenceSessionId = null;
3053
+ }
3054
+ }
3055
+ async heartbeatPresence() {
3056
+ if (!this._presenceSessionId || this._presencePaused) return;
3057
+ try {
3058
+ const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
3059
+ p_session_id: this._presenceSessionId
3060
+ });
3061
+ if (error || data === false) {
3062
+ this.stopPresenceHeartbeat();
3063
+ this._presenceSessionId = null;
3064
+ }
3065
+ } catch {
3066
+ }
3067
+ }
3068
+ startPresenceHeartbeat() {
3069
+ this.stopPresenceHeartbeat();
3070
+ this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
3071
+ }
3072
+ stopPresenceHeartbeat() {
3073
+ if (this._presenceInterval) {
3074
+ clearInterval(this._presenceInterval);
3075
+ this._presenceInterval = null;
3076
+ }
3077
+ }
2955
3078
  };
2956
3079
  function createBugBear(config) {
2957
3080
  return new BugBearClient(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",