@mobvibe/cli 0.1.4 → 0.1.7

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.js CHANGED
@@ -379,12 +379,13 @@ var getCliConfig = async () => {
379
379
 
380
380
  // src/daemon/daemon.ts
381
381
  import { spawn as spawn2 } from "child_process";
382
- import fs4 from "fs/promises";
383
- import path5 from "path";
382
+ import fs5 from "fs/promises";
383
+ import path6 from "path";
384
384
 
385
385
  // src/acp/session-manager.ts
386
386
  import { randomUUID as randomUUID2 } from "crypto";
387
387
  import { EventEmitter as EventEmitter2 } from "events";
388
+ import fs3 from "fs/promises";
388
389
 
389
390
  // ../../packages/shared/dist/types/errors.js
390
391
  var createErrorDetail = (input) => ({
@@ -559,8 +560,7 @@ var AcpConnection = class {
559
560
  getSessionCapabilities() {
560
561
  return {
561
562
  list: this.agentCapabilities?.sessionCapabilities?.list != null,
562
- load: this.agentCapabilities?.loadSession === true,
563
- resume: this.agentCapabilities?.sessionCapabilities?.resume != null
563
+ load: this.agentCapabilities?.loadSession === true
564
564
  };
565
565
  }
566
566
  /**
@@ -575,12 +575,6 @@ var AcpConnection = class {
575
575
  supportsSessionLoad() {
576
576
  return this.agentCapabilities?.loadSession === true;
577
577
  }
578
- /**
579
- * Check if the agent supports session/resume.
580
- */
581
- supportsSessionResume() {
582
- return this.agentCapabilities?.sessionCapabilities?.resume != null;
583
- }
584
578
  /**
585
579
  * List sessions from the agent (session/list).
586
580
  * @param params Optional filter parameters
@@ -588,14 +582,17 @@ var AcpConnection = class {
588
582
  */
589
583
  async listSessions(params) {
590
584
  if (!this.supportsSessionList()) {
591
- return [];
585
+ return { sessions: [] };
592
586
  }
593
587
  const connection = await this.ensureReady();
594
588
  const response = await connection.unstable_listSessions({
595
589
  cursor: params?.cursor ?? void 0,
596
590
  cwd: params?.cwd ?? void 0
597
591
  });
598
- return response.sessions;
592
+ return {
593
+ sessions: response.sessions,
594
+ nextCursor: response.nextCursor ?? void 0
595
+ };
599
596
  }
600
597
  /**
601
598
  * Load a historical session with message history replay (session/load).
@@ -616,25 +613,6 @@ var AcpConnection = class {
616
613
  this.sessionId = sessionId;
617
614
  return response;
618
615
  }
619
- /**
620
- * Resume an active session without message history replay (session/resume).
621
- * @param sessionId The session ID to resume
622
- * @param cwd The working directory
623
- * @returns Resume session response
624
- */
625
- async resumeSession(sessionId, cwd) {
626
- if (!this.supportsSessionResume()) {
627
- throw new Error("Agent does not support session/resume capability");
628
- }
629
- const connection = await this.ensureReady();
630
- const response = await connection.unstable_resumeSession({
631
- sessionId,
632
- cwd,
633
- mcpServers: []
634
- });
635
- this.sessionId = sessionId;
636
- return response;
637
- }
638
616
  setPermissionHandler(handler) {
639
617
  this.permissionHandler = handler;
640
618
  }
@@ -979,6 +957,14 @@ var createCapabilityNotSupportedError = (message) => new AppError(
979
957
  }),
980
958
  409
981
959
  );
960
+ var isValidWorkspacePath = async (cwd) => {
961
+ try {
962
+ const stats = await fs3.stat(cwd);
963
+ return stats.isDirectory();
964
+ } catch {
965
+ return false;
966
+ }
967
+ };
982
968
  var SessionManager = class {
983
969
  constructor(config) {
984
970
  this.config = config;
@@ -988,6 +974,7 @@ var SessionManager = class {
988
974
  this.defaultBackendId = config.defaultAcpBackendId;
989
975
  }
990
976
  sessions = /* @__PURE__ */ new Map();
977
+ discoveredSessions = /* @__PURE__ */ new Map();
991
978
  backendById;
992
979
  defaultBackendId;
993
980
  permissionRequests = /* @__PURE__ */ new Map();
@@ -997,6 +984,8 @@ var SessionManager = class {
997
984
  permissionResultEmitter = new EventEmitter2();
998
985
  terminalOutputEmitter = new EventEmitter2();
999
986
  sessionsChangedEmitter = new EventEmitter2();
987
+ sessionAttachedEmitter = new EventEmitter2();
988
+ sessionDetachedEmitter = new EventEmitter2();
1000
989
  listSessions() {
1001
990
  return Array.from(this.sessions.values()).map(
1002
991
  (record) => this.buildSummary(record)
@@ -1041,9 +1030,54 @@ var SessionManager = class {
1041
1030
  this.sessionsChangedEmitter.off("changed", listener);
1042
1031
  };
1043
1032
  }
1033
+ onSessionAttached(listener) {
1034
+ this.sessionAttachedEmitter.on("attached", listener);
1035
+ return () => {
1036
+ this.sessionAttachedEmitter.off("attached", listener);
1037
+ };
1038
+ }
1039
+ onSessionDetached(listener) {
1040
+ this.sessionDetachedEmitter.on("detached", listener);
1041
+ return () => {
1042
+ this.sessionDetachedEmitter.off("detached", listener);
1043
+ };
1044
+ }
1044
1045
  emitSessionsChanged(payload) {
1045
1046
  this.sessionsChangedEmitter.emit("changed", payload);
1046
1047
  }
1048
+ emitSessionAttached(sessionId, force = false) {
1049
+ const record = this.sessions.get(sessionId);
1050
+ if (!record) {
1051
+ return;
1052
+ }
1053
+ if (record.isAttached && !force) {
1054
+ return;
1055
+ }
1056
+ const attachedAt = /* @__PURE__ */ new Date();
1057
+ record.isAttached = true;
1058
+ record.attachedAt = attachedAt;
1059
+ this.sessionAttachedEmitter.emit("attached", {
1060
+ sessionId,
1061
+ machineId: this.config.machineId,
1062
+ attachedAt: attachedAt.toISOString()
1063
+ });
1064
+ }
1065
+ emitSessionDetached(sessionId, reason) {
1066
+ const record = this.sessions.get(sessionId);
1067
+ if (!record) {
1068
+ return;
1069
+ }
1070
+ if (!record.isAttached) {
1071
+ return;
1072
+ }
1073
+ record.isAttached = false;
1074
+ this.sessionDetachedEmitter.emit("detached", {
1075
+ sessionId,
1076
+ machineId: this.config.machineId,
1077
+ detachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1078
+ reason
1079
+ });
1080
+ }
1047
1081
  listPendingPermissions(sessionId) {
1048
1082
  return Array.from(this.permissionRequests.values()).filter((record) => record.sessionId === sessionId).map((record) => this.buildPermissionRequestPayload(record));
1049
1083
  }
@@ -1132,29 +1166,9 @@ var SessionManager = class {
1132
1166
  };
1133
1167
  record.unsubscribe = connection.onSessionUpdate(
1134
1168
  (notification) => {
1135
- this.touchSession(session.sessionId);
1169
+ record.updatedAt = /* @__PURE__ */ new Date();
1136
1170
  this.sessionUpdateEmitter.emit("update", notification);
1137
- const update = notification.update;
1138
- if (update.sessionUpdate === "current_mode_update") {
1139
- record.modeId = update.currentModeId;
1140
- record.modeName = record.availableModes?.find(
1141
- (mode) => mode.id === update.currentModeId
1142
- )?.name ?? record.modeName;
1143
- return;
1144
- }
1145
- if (update.sessionUpdate === "session_info_update") {
1146
- if (typeof update.title === "string") {
1147
- record.title = update.title;
1148
- }
1149
- if (typeof update.updatedAt === "string") {
1150
- record.updatedAt = new Date(update.updatedAt);
1151
- }
1152
- }
1153
- if (update.sessionUpdate === "available_commands_update") {
1154
- if (update.availableCommands) {
1155
- record.availableCommands = update.availableCommands;
1156
- }
1157
- }
1171
+ this.applySessionUpdateToRecord(record, notification);
1158
1172
  }
1159
1173
  );
1160
1174
  record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
@@ -1166,6 +1180,7 @@ var SessionManager = class {
1166
1180
  sessionId: session.sessionId,
1167
1181
  error: status.error
1168
1182
  });
1183
+ this.emitSessionDetached(session.sessionId, "agent_exit");
1169
1184
  }
1170
1185
  });
1171
1186
  this.sessions.set(session.sessionId, record);
@@ -1175,6 +1190,7 @@ var SessionManager = class {
1175
1190
  updated: [],
1176
1191
  removed: []
1177
1192
  });
1193
+ this.emitSessionAttached(session.sessionId);
1178
1194
  return summary;
1179
1195
  } catch (error) {
1180
1196
  const status = connection.getStatus();
@@ -1391,6 +1407,7 @@ var SessionManager = class {
1391
1407
  } catch (error) {
1392
1408
  logger.error({ err: error, sessionId }, "session_disconnect_failed");
1393
1409
  }
1410
+ this.emitSessionDetached(sessionId, "unknown");
1394
1411
  this.sessions.delete(sessionId);
1395
1412
  this.emitSessionsChanged({
1396
1413
  added: [],
@@ -1424,11 +1441,30 @@ var SessionManager = class {
1424
1441
  await connection.connect();
1425
1442
  const capabilities = connection.getSessionCapabilities();
1426
1443
  const sessions = [];
1444
+ let nextCursor;
1427
1445
  if (capabilities.list) {
1428
- const agentSessions = await connection.listSessions({
1429
- cwd: options?.cwd
1446
+ const response = await connection.listSessions({
1447
+ cwd: options?.cwd,
1448
+ cursor: options?.cursor
1430
1449
  });
1431
- for (const session of agentSessions) {
1450
+ nextCursor = response.nextCursor;
1451
+ const validity = await Promise.all(
1452
+ response.sessions.map(async (session) => ({
1453
+ session,
1454
+ isValid: session.cwd ? await isValidWorkspacePath(session.cwd) : false
1455
+ }))
1456
+ );
1457
+ for (const { session, isValid } of validity) {
1458
+ if (!isValid) {
1459
+ this.discoveredSessions.delete(session.sessionId);
1460
+ continue;
1461
+ }
1462
+ this.discoveredSessions.set(session.sessionId, {
1463
+ sessionId: session.sessionId,
1464
+ cwd: session.cwd,
1465
+ title: session.title ?? void 0,
1466
+ updatedAt: session.updatedAt ?? void 0
1467
+ });
1432
1468
  sessions.push({
1433
1469
  sessionId: session.sessionId,
1434
1470
  cwd: session.cwd,
@@ -1445,7 +1481,7 @@ var SessionManager = class {
1445
1481
  },
1446
1482
  "sessions_discovered"
1447
1483
  );
1448
- return { sessions, capabilities };
1484
+ return { sessions, capabilities, nextCursor };
1449
1485
  } finally {
1450
1486
  await connection.disconnect();
1451
1487
  }
@@ -1461,6 +1497,7 @@ var SessionManager = class {
1461
1497
  async loadSession(sessionId, cwd, backendId) {
1462
1498
  const existing = this.sessions.get(sessionId);
1463
1499
  if (existing) {
1500
+ this.emitSessionAttached(sessionId, true);
1464
1501
  return this.buildSummary(existing);
1465
1502
  }
1466
1503
  const backend = this.resolveBackend(backendId);
@@ -1478,6 +1515,19 @@ var SessionManager = class {
1478
1515
  "Agent does not support session loading"
1479
1516
  );
1480
1517
  }
1518
+ const bufferedUpdates = [];
1519
+ let recordRef;
1520
+ const unsubscribe = connection.onSessionUpdate(
1521
+ (notification) => {
1522
+ this.sessionUpdateEmitter.emit("update", notification);
1523
+ if (recordRef) {
1524
+ recordRef.updatedAt = /* @__PURE__ */ new Date();
1525
+ this.applySessionUpdateToRecord(recordRef, notification);
1526
+ } else {
1527
+ bufferedUpdates.push(notification);
1528
+ }
1529
+ }
1530
+ );
1481
1531
  const response = await connection.loadSession(sessionId, cwd);
1482
1532
  connection.setPermissionHandler(
1483
1533
  (params) => this.handlePermissionRequest(sessionId, params)
@@ -1490,9 +1540,10 @@ var SessionManager = class {
1490
1540
  const { modeId, modeName, availableModes } = resolveModeState(
1491
1541
  response.modes
1492
1542
  );
1543
+ const discovered = this.discoveredSessions.get(sessionId);
1493
1544
  const record = {
1494
1545
  sessionId,
1495
- title: `Loaded Session`,
1546
+ title: discovered?.title ?? sessionId,
1496
1547
  backendId: backend.id,
1497
1548
  backendLabel: backend.label,
1498
1549
  connection,
@@ -1508,7 +1559,12 @@ var SessionManager = class {
1508
1559
  availableModels,
1509
1560
  availableCommands: void 0
1510
1561
  };
1511
- this.setupSessionSubscriptions(record);
1562
+ recordRef = record;
1563
+ record.unsubscribe = unsubscribe;
1564
+ for (const notification of bufferedUpdates) {
1565
+ this.applySessionUpdateToRecord(record, notification);
1566
+ }
1567
+ this.setupSessionSubscriptions(record, { skipSessionUpdates: true });
1512
1568
  this.sessions.set(sessionId, record);
1513
1569
  const summary = this.buildSummary(record);
1514
1570
  this.emitSessionsChanged({
@@ -1516,6 +1572,7 @@ var SessionManager = class {
1516
1572
  updated: [],
1517
1573
  removed: []
1518
1574
  });
1575
+ this.emitSessionAttached(sessionId);
1519
1576
  logger.info({ sessionId, backendId: backend.id }, "session_loaded");
1520
1577
  return summary;
1521
1578
  } catch (error) {
@@ -1524,110 +1581,81 @@ var SessionManager = class {
1524
1581
  }
1525
1582
  }
1526
1583
  /**
1527
- * Resume an active session from the ACP agent.
1528
- * This does not replay message history.
1529
- * @param sessionId The session ID to resume
1530
- * @param cwd The working directory
1531
- * @param backendId Optional backend ID
1532
- * @returns The resumed session summary
1584
+ * Reload a historical session from the ACP agent.
1585
+ * Replays session history even if the session is already loaded.
1533
1586
  */
1534
- async resumeSession(sessionId, cwd, backendId) {
1587
+ async reloadSession(sessionId, cwd, backendId) {
1535
1588
  const existing = this.sessions.get(sessionId);
1536
- if (existing) {
1537
- return this.buildSummary(existing);
1589
+ if (!existing) {
1590
+ return this.loadSession(sessionId, cwd, backendId);
1538
1591
  }
1539
- const backend = this.resolveBackend(backendId);
1540
- const connection = new AcpConnection({
1541
- backend,
1542
- client: {
1543
- name: this.config.clientName,
1544
- version: this.config.clientVersion
1545
- }
1592
+ if (!existing.connection.supportsSessionLoad()) {
1593
+ throw createCapabilityNotSupportedError(
1594
+ "Agent does not support session loading"
1595
+ );
1596
+ }
1597
+ const response = await existing.connection.loadSession(sessionId, cwd);
1598
+ const { modelId, modelName, availableModels } = resolveModelState(
1599
+ response.models
1600
+ );
1601
+ const { modeId, modeName, availableModes } = resolveModeState(
1602
+ response.modes
1603
+ );
1604
+ const agentInfo = existing.connection.getAgentInfo();
1605
+ existing.cwd = cwd;
1606
+ existing.agentName = agentInfo?.title ?? agentInfo?.name ?? existing.agentName;
1607
+ existing.modelId = modelId;
1608
+ existing.modelName = modelName;
1609
+ existing.availableModels = availableModels;
1610
+ existing.modeId = modeId;
1611
+ existing.modeName = modeName;
1612
+ existing.availableModes = availableModes;
1613
+ existing.updatedAt = /* @__PURE__ */ new Date();
1614
+ const summary = this.buildSummary(existing);
1615
+ this.emitSessionsChanged({
1616
+ added: [],
1617
+ updated: [summary],
1618
+ removed: []
1546
1619
  });
1547
- try {
1548
- await connection.connect();
1549
- if (!connection.supportsSessionResume()) {
1550
- throw createCapabilityNotSupportedError(
1551
- "Agent does not support session resuming"
1552
- );
1620
+ this.emitSessionAttached(sessionId, true);
1621
+ logger.info({ sessionId, backendId }, "session_reloaded");
1622
+ return summary;
1623
+ }
1624
+ applySessionUpdateToRecord(record, notification) {
1625
+ const update = notification.update;
1626
+ if (update.sessionUpdate === "current_mode_update") {
1627
+ record.modeId = update.currentModeId;
1628
+ record.modeName = record.availableModes?.find((mode) => mode.id === update.currentModeId)?.name ?? record.modeName;
1629
+ return;
1630
+ }
1631
+ if (update.sessionUpdate === "session_info_update") {
1632
+ if (typeof update.title === "string") {
1633
+ record.title = update.title;
1634
+ }
1635
+ if (typeof update.updatedAt === "string") {
1636
+ record.updatedAt = new Date(update.updatedAt);
1637
+ }
1638
+ }
1639
+ if (update.sessionUpdate === "available_commands_update") {
1640
+ if (update.availableCommands) {
1641
+ record.availableCommands = update.availableCommands;
1553
1642
  }
1554
- const response = await connection.resumeSession(sessionId, cwd);
1555
- connection.setPermissionHandler(
1556
- (params) => this.handlePermissionRequest(sessionId, params)
1557
- );
1558
- const now = /* @__PURE__ */ new Date();
1559
- const agentInfo = connection.getAgentInfo();
1560
- const { modelId, modelName, availableModels } = resolveModelState(
1561
- response.models
1562
- );
1563
- const { modeId, modeName, availableModes } = resolveModeState(
1564
- response.modes
1565
- );
1566
- const record = {
1567
- sessionId,
1568
- title: `Resumed Session`,
1569
- backendId: backend.id,
1570
- backendLabel: backend.label,
1571
- connection,
1572
- createdAt: now,
1573
- updatedAt: now,
1574
- cwd,
1575
- agentName: agentInfo?.title ?? agentInfo?.name,
1576
- modelId,
1577
- modelName,
1578
- modeId,
1579
- modeName,
1580
- availableModes,
1581
- availableModels,
1582
- availableCommands: void 0
1583
- };
1584
- this.setupSessionSubscriptions(record);
1585
- this.sessions.set(sessionId, record);
1586
- const summary = this.buildSummary(record);
1587
- this.emitSessionsChanged({
1588
- added: [summary],
1589
- updated: [],
1590
- removed: []
1591
- });
1592
- logger.info({ sessionId, backendId: backend.id }, "session_resumed");
1593
- return summary;
1594
- } catch (error) {
1595
- await connection.disconnect();
1596
- throw error;
1597
1643
  }
1598
1644
  }
1599
1645
  /**
1600
1646
  * Set up event subscriptions for a session record.
1601
1647
  */
1602
- setupSessionSubscriptions(record) {
1648
+ setupSessionSubscriptions(record, options) {
1603
1649
  const { sessionId, connection } = record;
1604
- record.unsubscribe = connection.onSessionUpdate(
1605
- (notification) => {
1606
- this.touchSession(sessionId);
1607
- this.sessionUpdateEmitter.emit("update", notification);
1608
- const update = notification.update;
1609
- if (update.sessionUpdate === "current_mode_update") {
1610
- record.modeId = update.currentModeId;
1611
- record.modeName = record.availableModes?.find(
1612
- (mode) => mode.id === update.currentModeId
1613
- )?.name ?? record.modeName;
1614
- return;
1615
- }
1616
- if (update.sessionUpdate === "session_info_update") {
1617
- if (typeof update.title === "string") {
1618
- record.title = update.title;
1619
- }
1620
- if (typeof update.updatedAt === "string") {
1621
- record.updatedAt = new Date(update.updatedAt);
1622
- }
1623
- }
1624
- if (update.sessionUpdate === "available_commands_update") {
1625
- if (update.availableCommands) {
1626
- record.availableCommands = update.availableCommands;
1627
- }
1650
+ if (!options?.skipSessionUpdates) {
1651
+ record.unsubscribe = connection.onSessionUpdate(
1652
+ (notification) => {
1653
+ record.updatedAt = /* @__PURE__ */ new Date();
1654
+ this.sessionUpdateEmitter.emit("update", notification);
1655
+ this.applySessionUpdateToRecord(record, notification);
1628
1656
  }
1629
- }
1630
- );
1657
+ );
1658
+ }
1631
1659
  record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
1632
1660
  this.terminalOutputEmitter.emit("output", event);
1633
1661
  });
@@ -1637,6 +1665,7 @@ var SessionManager = class {
1637
1665
  sessionId,
1638
1666
  error: status.error
1639
1667
  });
1668
+ this.emitSessionDetached(sessionId, "agent_exit");
1640
1669
  }
1641
1670
  });
1642
1671
  }
@@ -1647,7 +1676,6 @@ var SessionManager = class {
1647
1676
  title: record.title,
1648
1677
  backendId: record.backendId,
1649
1678
  backendLabel: record.backendLabel,
1650
- state: status.state,
1651
1679
  error: status.error,
1652
1680
  pid: status.pid,
1653
1681
  createdAt: record.createdAt.toISOString(),
@@ -1667,11 +1695,189 @@ var SessionManager = class {
1667
1695
 
1668
1696
  // src/daemon/socket-client.ts
1669
1697
  import { EventEmitter as EventEmitter3 } from "events";
1670
- import fs3 from "fs/promises";
1698
+ import fs4 from "fs/promises";
1671
1699
  import { homedir } from "os";
1672
- import path4 from "path";
1700
+ import path5 from "path";
1673
1701
  import ignore from "ignore";
1674
1702
  import { io } from "socket.io-client";
1703
+
1704
+ // src/lib/git-utils.ts
1705
+ import { exec } from "child_process";
1706
+ import path4 from "path";
1707
+ import { promisify } from "util";
1708
+ var execAsync = promisify(exec);
1709
+ var MAX_BUFFER = 10 * 1024 * 1024;
1710
+ async function isGitRepo(cwd) {
1711
+ try {
1712
+ await execAsync("git rev-parse --is-inside-work-tree", {
1713
+ cwd,
1714
+ maxBuffer: MAX_BUFFER
1715
+ });
1716
+ return true;
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+ async function getGitBranch(cwd) {
1722
+ try {
1723
+ const { stdout } = await execAsync("git branch --show-current", {
1724
+ cwd,
1725
+ maxBuffer: MAX_BUFFER
1726
+ });
1727
+ const branch = stdout.trim();
1728
+ if (branch) {
1729
+ return branch;
1730
+ }
1731
+ const { stdout: hashOut } = await execAsync("git rev-parse --short HEAD", {
1732
+ cwd,
1733
+ maxBuffer: MAX_BUFFER
1734
+ });
1735
+ return hashOut.trim() || void 0;
1736
+ } catch {
1737
+ return void 0;
1738
+ }
1739
+ }
1740
+ function parseGitStatus(output) {
1741
+ const files = [];
1742
+ const lines = output.split("\n").filter((line) => line.length > 0);
1743
+ for (const line of lines) {
1744
+ const indexStatus = line[0];
1745
+ const workTreeStatus = line[1];
1746
+ const filePath = line.slice(3).split(" -> ").pop()?.trim();
1747
+ if (!filePath) {
1748
+ continue;
1749
+ }
1750
+ let status;
1751
+ if (indexStatus === "?" || workTreeStatus === "?") {
1752
+ status = "?";
1753
+ } else if (indexStatus === "!" || workTreeStatus === "!") {
1754
+ status = "!";
1755
+ } else if (indexStatus === "A" || workTreeStatus === "A") {
1756
+ status = "A";
1757
+ } else if (indexStatus === "D" || workTreeStatus === "D") {
1758
+ status = "D";
1759
+ } else if (indexStatus === "R" || workTreeStatus === "R") {
1760
+ status = "R";
1761
+ } else if (indexStatus === "C" || workTreeStatus === "C") {
1762
+ status = "C";
1763
+ } else if (indexStatus === "U" || workTreeStatus === "U") {
1764
+ status = "U";
1765
+ } else if (indexStatus === "M" || workTreeStatus === "M" || indexStatus !== " " || workTreeStatus !== " ") {
1766
+ status = "M";
1767
+ } else {
1768
+ continue;
1769
+ }
1770
+ files.push({ path: filePath, status });
1771
+ }
1772
+ return files;
1773
+ }
1774
+ async function getGitStatus(cwd) {
1775
+ try {
1776
+ const { stdout } = await execAsync("git status --porcelain=v1", {
1777
+ cwd,
1778
+ maxBuffer: MAX_BUFFER
1779
+ });
1780
+ return parseGitStatus(stdout);
1781
+ } catch {
1782
+ return [];
1783
+ }
1784
+ }
1785
+ function aggregateDirStatus(files) {
1786
+ const dirStatus = {};
1787
+ const statusPriority = {
1788
+ A: 7,
1789
+ D: 6,
1790
+ M: 5,
1791
+ R: 4,
1792
+ C: 3,
1793
+ U: 2,
1794
+ "?": 1,
1795
+ "!": 0
1796
+ };
1797
+ for (const file of files) {
1798
+ const parts = file.path.split("/");
1799
+ let currentPath = "";
1800
+ for (let i = 0; i < parts.length - 1; i++) {
1801
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
1802
+ const existing = dirStatus[currentPath];
1803
+ if (!existing || statusPriority[file.status] > statusPriority[existing]) {
1804
+ dirStatus[currentPath] = file.status;
1805
+ }
1806
+ }
1807
+ }
1808
+ return dirStatus;
1809
+ }
1810
+ function parseDiffOutput(diffOutput) {
1811
+ const addedLines = [];
1812
+ const modifiedLines = [];
1813
+ const deletedLines = [];
1814
+ const lines = diffOutput.split("\n");
1815
+ let currentLine = 0;
1816
+ let inHunk = false;
1817
+ let pendingDeletionLine = 0;
1818
+ for (const line of lines) {
1819
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
1820
+ if (hunkMatch) {
1821
+ currentLine = Number.parseInt(hunkMatch[1], 10);
1822
+ inHunk = true;
1823
+ pendingDeletionLine = 0;
1824
+ continue;
1825
+ }
1826
+ if (!inHunk) {
1827
+ continue;
1828
+ }
1829
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1830
+ addedLines.push(currentLine);
1831
+ currentLine++;
1832
+ pendingDeletionLine = 0;
1833
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
1834
+ if (pendingDeletionLine === 0) {
1835
+ pendingDeletionLine = currentLine;
1836
+ }
1837
+ const deletionPos = Math.max(1, currentLine);
1838
+ if (!deletedLines.includes(deletionPos)) {
1839
+ deletedLines.push(deletionPos);
1840
+ }
1841
+ } else if (!line.startsWith("\\")) {
1842
+ currentLine++;
1843
+ pendingDeletionLine = 0;
1844
+ }
1845
+ }
1846
+ return { addedLines, modifiedLines, deletedLines };
1847
+ }
1848
+ async function getFileDiff(cwd, filePath) {
1849
+ try {
1850
+ const relativePath = path4.isAbsolute(filePath) ? path4.relative(cwd, filePath) : filePath;
1851
+ const { stdout } = await execAsync(`git diff HEAD -- "${relativePath}"`, {
1852
+ cwd,
1853
+ maxBuffer: MAX_BUFFER
1854
+ });
1855
+ if (!stdout.trim()) {
1856
+ const { stdout: statusOut } = await execAsync(
1857
+ `git status --porcelain=v1 -- "${relativePath}"`,
1858
+ { cwd, maxBuffer: MAX_BUFFER }
1859
+ );
1860
+ if (statusOut.startsWith("?") || statusOut.startsWith("A")) {
1861
+ const { stdout: wcOut } = await execAsync(`wc -l < "${relativePath}"`, {
1862
+ cwd,
1863
+ maxBuffer: MAX_BUFFER
1864
+ });
1865
+ const lineCount = Number.parseInt(wcOut.trim(), 10) || 0;
1866
+ return {
1867
+ addedLines: Array.from({ length: lineCount }, (_, i) => i + 1),
1868
+ modifiedLines: [],
1869
+ deletedLines: []
1870
+ };
1871
+ }
1872
+ return { addedLines: [], modifiedLines: [], deletedLines: [] };
1873
+ }
1874
+ return parseDiffOutput(stdout);
1875
+ } catch {
1876
+ return { addedLines: [], modifiedLines: [], deletedLines: [] };
1877
+ }
1878
+ }
1879
+
1880
+ // src/daemon/socket-client.ts
1675
1881
  var SESSION_ROOT_NAME = "Working Directory";
1676
1882
  var MAX_RESOURCE_FILES = 2e3;
1677
1883
  var DEFAULT_IGNORES = [
@@ -1691,15 +1897,15 @@ var DEFAULT_IGNORES = [
1691
1897
  var loadGitignore = async (rootPath) => {
1692
1898
  const ig = ignore().add(DEFAULT_IGNORES);
1693
1899
  try {
1694
- const gitignorePath = path4.join(rootPath, ".gitignore");
1695
- const content = await fs3.readFile(gitignorePath, "utf8");
1900
+ const gitignorePath = path5.join(rootPath, ".gitignore");
1901
+ const content = await fs4.readFile(gitignorePath, "utf8");
1696
1902
  ig.add(content);
1697
1903
  } catch {
1698
1904
  }
1699
1905
  return ig;
1700
1906
  };
1701
1907
  var resolveImageMimeType = (filePath) => {
1702
- const extension = path4.extname(filePath).toLowerCase();
1908
+ const extension = path5.extname(filePath).toLowerCase();
1703
1909
  switch (extension) {
1704
1910
  case ".apng":
1705
1911
  return "image/apng";
@@ -1722,14 +1928,14 @@ var resolveImageMimeType = (filePath) => {
1722
1928
  }
1723
1929
  };
1724
1930
  var readDirectoryEntries = async (dirPath) => {
1725
- const entries = await fs3.readdir(dirPath, { withFileTypes: true });
1931
+ const entries = await fs4.readdir(dirPath, { withFileTypes: true });
1726
1932
  const resolvedEntries = await Promise.all(
1727
1933
  entries.map(async (entry) => {
1728
- const entryPath = path4.join(dirPath, entry.name);
1934
+ const entryPath = path5.join(dirPath, entry.name);
1729
1935
  let isDirectory = entry.isDirectory();
1730
1936
  if (!isDirectory && entry.isSymbolicLink()) {
1731
1937
  try {
1732
- const stats = await fs3.stat(entryPath);
1938
+ const stats = await fs4.stat(entryPath);
1733
1939
  isDirectory = stats.isDirectory();
1734
1940
  } catch {
1735
1941
  }
@@ -1813,14 +2019,24 @@ var SocketClient = class extends EventEmitter3 {
1813
2019
  this.socket.on("cli:registered", async (info) => {
1814
2020
  logger.info({ machineId: info.machineId }, "gateway_registered");
1815
2021
  try {
1816
- const { sessions, capabilities } = await this.options.sessionManager.discoverSessions();
1817
- if (sessions.length > 0) {
1818
- this.socket.emit("sessions:discovered", { sessions, capabilities });
1819
- logger.info(
1820
- { count: sessions.length, capabilities },
1821
- "historical_sessions_discovered"
1822
- );
1823
- }
2022
+ let cursor;
2023
+ let page = 0;
2024
+ do {
2025
+ const { sessions, capabilities, nextCursor } = await this.options.sessionManager.discoverSessions({ cursor });
2026
+ cursor = nextCursor;
2027
+ if (sessions.length > 0) {
2028
+ this.socket.emit("sessions:discovered", {
2029
+ sessions,
2030
+ capabilities,
2031
+ nextCursor
2032
+ });
2033
+ logger.info(
2034
+ { count: sessions.length, capabilities, page },
2035
+ "historical_sessions_discovered"
2036
+ );
2037
+ }
2038
+ page += 1;
2039
+ } while (cursor);
1824
2040
  } catch (error) {
1825
2041
  logger.warn({ err: error }, "session_discovery_failed");
1826
2042
  }
@@ -2115,7 +2331,7 @@ var SocketClient = class extends EventEmitter3 {
2115
2331
  if (!record || !record.cwd) {
2116
2332
  throw new Error("Session not found or no working directory");
2117
2333
  }
2118
- const resolved = requestPath ? path4.isAbsolute(requestPath) ? requestPath : path4.join(record.cwd, requestPath) : record.cwd;
2334
+ const resolved = requestPath ? path5.isAbsolute(requestPath) ? requestPath : path5.join(record.cwd, requestPath) : record.cwd;
2119
2335
  const entries = await readDirectoryEntries(resolved);
2120
2336
  this.sendRpcResponse(request.requestId, { path: resolved, entries });
2121
2337
  } catch (error) {
@@ -2141,10 +2357,10 @@ var SocketClient = class extends EventEmitter3 {
2141
2357
  if (!record || !record.cwd) {
2142
2358
  throw new Error("Session not found or no working directory");
2143
2359
  }
2144
- const resolved = path4.isAbsolute(requestPath) ? requestPath : path4.join(record.cwd, requestPath);
2360
+ const resolved = path5.isAbsolute(requestPath) ? requestPath : path5.join(record.cwd, requestPath);
2145
2361
  const mimeType = resolveImageMimeType(resolved);
2146
2362
  if (mimeType) {
2147
- const buffer = await fs3.readFile(resolved);
2363
+ const buffer = await fs4.readFile(resolved);
2148
2364
  const preview2 = {
2149
2365
  path: resolved,
2150
2366
  previewType: "image",
@@ -2154,7 +2370,7 @@ var SocketClient = class extends EventEmitter3 {
2154
2370
  this.sendRpcResponse(request.requestId, preview2);
2155
2371
  return;
2156
2372
  }
2157
- const content = await fs3.readFile(resolved, "utf8");
2373
+ const content = await fs4.readFile(resolved, "utf8");
2158
2374
  const preview = {
2159
2375
  path: resolved,
2160
2376
  previewType: "code",
@@ -2203,14 +2419,15 @@ var SocketClient = class extends EventEmitter3 {
2203
2419
  });
2204
2420
  this.socket.on("rpc:sessions:discover", async (request) => {
2205
2421
  try {
2206
- const { cwd, backendId } = request.params;
2422
+ const { cwd, backendId, cursor } = request.params;
2207
2423
  logger.info(
2208
- { requestId: request.requestId, cwd, backendId },
2424
+ { requestId: request.requestId, cwd, backendId, cursor },
2209
2425
  "rpc_sessions_discover"
2210
2426
  );
2211
2427
  const result = await sessionManager.discoverSessions({
2212
2428
  cwd,
2213
- backendId
2429
+ backendId,
2430
+ cursor
2214
2431
  });
2215
2432
  this.sendRpcResponse(request.requestId, result);
2216
2433
  } catch (error) {
@@ -2245,14 +2462,14 @@ var SocketClient = class extends EventEmitter3 {
2245
2462
  this.sendRpcError(request.requestId, error);
2246
2463
  }
2247
2464
  });
2248
- this.socket.on("rpc:session:resume", async (request) => {
2465
+ this.socket.on("rpc:session:reload", async (request) => {
2249
2466
  try {
2250
2467
  const { sessionId, cwd } = request.params;
2251
2468
  logger.info(
2252
2469
  { requestId: request.requestId, sessionId, cwd },
2253
- "rpc_session_resume"
2470
+ "rpc_session_reload"
2254
2471
  );
2255
- const session = await sessionManager.resumeSession(sessionId, cwd);
2472
+ const session = await sessionManager.reloadSession(sessionId, cwd);
2256
2473
  this.sendRpcResponse(request.requestId, session);
2257
2474
  } catch (error) {
2258
2475
  logger.error(
@@ -2261,7 +2478,93 @@ var SocketClient = class extends EventEmitter3 {
2261
2478
  requestId: request.requestId,
2262
2479
  sessionId: request.params.sessionId
2263
2480
  },
2264
- "rpc_session_resume_error"
2481
+ "rpc_session_reload_error"
2482
+ );
2483
+ this.sendRpcError(request.requestId, error);
2484
+ }
2485
+ });
2486
+ this.socket.on("rpc:git:status", async (request) => {
2487
+ try {
2488
+ const { sessionId } = request.params;
2489
+ logger.debug(
2490
+ { requestId: request.requestId, sessionId },
2491
+ "rpc_git_status"
2492
+ );
2493
+ const record = sessionManager.getSession(sessionId);
2494
+ if (!record || !record.cwd) {
2495
+ throw new Error("Session not found or no working directory");
2496
+ }
2497
+ const isRepo = await isGitRepo(record.cwd);
2498
+ if (!isRepo) {
2499
+ this.sendRpcResponse(request.requestId, {
2500
+ isGitRepo: false,
2501
+ files: [],
2502
+ dirStatus: {}
2503
+ });
2504
+ return;
2505
+ }
2506
+ const [branch, files] = await Promise.all([
2507
+ getGitBranch(record.cwd),
2508
+ getGitStatus(record.cwd)
2509
+ ]);
2510
+ const dirStatus = aggregateDirStatus(files);
2511
+ this.sendRpcResponse(request.requestId, {
2512
+ isGitRepo: true,
2513
+ branch,
2514
+ files,
2515
+ dirStatus
2516
+ });
2517
+ } catch (error) {
2518
+ logger.error(
2519
+ {
2520
+ err: error,
2521
+ requestId: request.requestId,
2522
+ sessionId: request.params.sessionId
2523
+ },
2524
+ "rpc_git_status_error"
2525
+ );
2526
+ this.sendRpcError(request.requestId, error);
2527
+ }
2528
+ });
2529
+ this.socket.on("rpc:git:fileDiff", async (request) => {
2530
+ try {
2531
+ const { sessionId, path: filePath } = request.params;
2532
+ logger.debug(
2533
+ { requestId: request.requestId, sessionId, path: filePath },
2534
+ "rpc_git_file_diff"
2535
+ );
2536
+ const record = sessionManager.getSession(sessionId);
2537
+ if (!record || !record.cwd) {
2538
+ throw new Error("Session not found or no working directory");
2539
+ }
2540
+ const isRepo = await isGitRepo(record.cwd);
2541
+ if (!isRepo) {
2542
+ this.sendRpcResponse(request.requestId, {
2543
+ isGitRepo: false,
2544
+ path: filePath,
2545
+ addedLines: [],
2546
+ modifiedLines: []
2547
+ });
2548
+ return;
2549
+ }
2550
+ const { addedLines, modifiedLines } = await getFileDiff(
2551
+ record.cwd,
2552
+ filePath
2553
+ );
2554
+ this.sendRpcResponse(request.requestId, {
2555
+ isGitRepo: true,
2556
+ path: filePath,
2557
+ addedLines,
2558
+ modifiedLines
2559
+ });
2560
+ } catch (error) {
2561
+ logger.error(
2562
+ {
2563
+ err: error,
2564
+ requestId: request.requestId,
2565
+ sessionId: request.params.sessionId
2566
+ },
2567
+ "rpc_git_file_diff_error"
2265
2568
  );
2266
2569
  this.sendRpcError(request.requestId, error);
2267
2570
  }
@@ -2310,13 +2613,23 @@ var SocketClient = class extends EventEmitter3 {
2310
2613
  this.socket.emit("sessions:changed", payload);
2311
2614
  }
2312
2615
  });
2616
+ sessionManager.onSessionAttached((payload) => {
2617
+ if (this.connected) {
2618
+ this.socket.emit("session:attached", payload);
2619
+ }
2620
+ });
2621
+ sessionManager.onSessionDetached((payload) => {
2622
+ if (this.connected) {
2623
+ this.socket.emit("session:detached", payload);
2624
+ }
2625
+ });
2313
2626
  }
2314
2627
  async listSessionResources(rootPath) {
2315
2628
  const ig = await loadGitignore(rootPath);
2316
2629
  const allFiles = await this.listAllFiles(rootPath, ig, rootPath, []);
2317
2630
  return allFiles.map((filePath) => ({
2318
- name: path4.basename(filePath),
2319
- relativePath: path4.relative(rootPath, filePath),
2631
+ name: path5.basename(filePath),
2632
+ relativePath: path5.relative(rootPath, filePath),
2320
2633
  path: filePath
2321
2634
  }));
2322
2635
  }
@@ -2324,13 +2637,13 @@ var SocketClient = class extends EventEmitter3 {
2324
2637
  if (collected.length >= MAX_RESOURCE_FILES) {
2325
2638
  return collected;
2326
2639
  }
2327
- const entries = await fs3.readdir(rootPath, { withFileTypes: true });
2640
+ const entries = await fs4.readdir(rootPath, { withFileTypes: true });
2328
2641
  for (const entry of entries) {
2329
2642
  if (collected.length >= MAX_RESOURCE_FILES) {
2330
2643
  break;
2331
2644
  }
2332
- const entryPath = path4.join(rootPath, entry.name);
2333
- const relativePath = path4.relative(baseDir, entryPath);
2645
+ const entryPath = path5.join(rootPath, entry.name);
2646
+ const relativePath = path5.relative(baseDir, entryPath);
2334
2647
  const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
2335
2648
  if (ig.ignores(checkPath)) {
2336
2649
  continue;
@@ -2424,12 +2737,12 @@ var DaemonManager = class {
2424
2737
  this.config = config;
2425
2738
  }
2426
2739
  async ensureHomeDirectory() {
2427
- await fs4.mkdir(this.config.homePath, { recursive: true });
2428
- await fs4.mkdir(this.config.logPath, { recursive: true });
2740
+ await fs5.mkdir(this.config.homePath, { recursive: true });
2741
+ await fs5.mkdir(this.config.logPath, { recursive: true });
2429
2742
  }
2430
2743
  async getPid() {
2431
2744
  try {
2432
- const content = await fs4.readFile(this.config.pidFile, "utf8");
2745
+ const content = await fs5.readFile(this.config.pidFile, "utf8");
2433
2746
  const pid = Number.parseInt(content.trim(), 10);
2434
2747
  if (Number.isNaN(pid)) {
2435
2748
  return null;
@@ -2446,11 +2759,11 @@ var DaemonManager = class {
2446
2759
  }
2447
2760
  }
2448
2761
  async writePidFile(pid) {
2449
- await fs4.writeFile(this.config.pidFile, String(pid), "utf8");
2762
+ await fs5.writeFile(this.config.pidFile, String(pid), "utf8");
2450
2763
  }
2451
2764
  async removePidFile() {
2452
2765
  try {
2453
- await fs4.unlink(this.config.pidFile);
2766
+ await fs5.unlink(this.config.pidFile);
2454
2767
  } catch {
2455
2768
  }
2456
2769
  }
@@ -2517,7 +2830,7 @@ var DaemonManager = class {
2517
2830
  }
2518
2831
  }
2519
2832
  async spawnBackground() {
2520
- const logFile = path5.join(
2833
+ const logFile = path6.join(
2521
2834
  this.config.logPath,
2522
2835
  `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-daemon.log`
2523
2836
  );
@@ -2535,7 +2848,7 @@ var DaemonManager = class {
2535
2848
  logger.error("daemon_spawn_failed");
2536
2849
  throw new Error("Failed to spawn daemon process");
2537
2850
  }
2538
- const logStream = await fs4.open(logFile, "a");
2851
+ const logStream = await fs5.open(logFile, "a");
2539
2852
  const fileHandle = logStream;
2540
2853
  child.stdout?.on("data", (data) => {
2541
2854
  fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {
@@ -2611,14 +2924,14 @@ var DaemonManager = class {
2611
2924
  });
2612
2925
  }
2613
2926
  async logs(options) {
2614
- const files = await fs4.readdir(this.config.logPath);
2927
+ const files = await fs5.readdir(this.config.logPath);
2615
2928
  const logFiles = files.filter((f) => f.endsWith("-daemon.log")).sort().reverse();
2616
2929
  if (logFiles.length === 0) {
2617
2930
  logger.warn("daemon_logs_empty");
2618
2931
  console.log("No log files found");
2619
2932
  return;
2620
2933
  }
2621
- const latestLog = path5.join(this.config.logPath, logFiles[0]);
2934
+ const latestLog = path6.join(this.config.logPath, logFiles[0]);
2622
2935
  logger.info({ logFile: latestLog }, "daemon_logs_latest");
2623
2936
  console.log(`Log file: ${latestLog}
2624
2937
  `);
@@ -2630,7 +2943,7 @@ var DaemonManager = class {
2630
2943
  tail.on("close", () => resolve());
2631
2944
  });
2632
2945
  } else {
2633
- const content = await fs4.readFile(latestLog, "utf8");
2946
+ const content = await fs5.readFile(latestLog, "utf8");
2634
2947
  const lines = content.split("\n");
2635
2948
  const count = options?.lines ?? 50;
2636
2949
  console.log(lines.slice(-count).join("\n"));