claude-code-session-manager 0.122.1 → 0.123.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.
Files changed (93) hide show
  1. package/dist/assets/{AgentLibrary-C2qUX9WU.js → AgentLibrary-T14aM8ie.js} +2 -2
  2. package/dist/assets/{Button-ssBzvFha.js → Button-Dz1kV8Z_.js} +1 -1
  3. package/dist/assets/DataModel-DdYRn2Ec.js +1 -0
  4. package/dist/assets/{History-BRdRp1J_.js → History-zQPvqwE7.js} +2 -2
  5. package/dist/assets/{Hooks--hiGPBQy.js → Hooks-BTrbT742.js} +3 -3
  6. package/dist/assets/{Library-0XYvkhh2.js → Library-BwQ6GRHR.js} +7 -7
  7. package/dist/assets/ListDetail-CunxAhRC.js +1 -0
  8. package/dist/assets/{McpServers-CCjHSwFm.js → McpServers-DpoyxXaO.js} +2 -2
  9. package/dist/assets/{Memory-C7RXoVO4.js → Memory-KDtGYMc3.js} +8 -8
  10. package/dist/assets/{Panel-C92O_aAH.js → Panel-xXmUBo8l.js} +1 -1
  11. package/dist/assets/{Permissions-DuTJ1FUb.js → Permissions-lIzTUrW2.js} +3 -3
  12. package/dist/assets/{Plugins-DSXsP4hM.js → Plugins-BA1Zqk69.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-CWdDkU1m.js → ProvenanceBadge-CmeJG2Kw.js} +1 -1
  14. package/dist/assets/{SaveBar-ob2dJJjH.js → SaveBar-BTJg_9Uc.js} +1 -1
  15. package/dist/assets/{Scheduler-Du4HgPKY.js → Scheduler-CISTDNMU.js} +7 -7
  16. package/dist/assets/{ScopeSwitcher-Dv_6M_Ci.js → ScopeSwitcher-C6NWvzoj.js} +1 -1
  17. package/dist/assets/Settings-BBoGJ34v.js +4 -0
  18. package/dist/assets/{SkillReferenceGraph-Dx1Qs8Y0.js → SkillReferenceGraph-IJv60QAJ.js} +1 -1
  19. package/dist/assets/Skills-L6nGcNF1.js +2 -0
  20. package/dist/assets/SystemPrompt-Cm7I2gJz.js +1 -0
  21. package/dist/assets/TagLibrary-BxPwoPE6.js +1 -0
  22. package/dist/assets/{TiptapBody-Cihe5er3.js → TiptapBody-CR-Zr3Pz.js} +1 -1
  23. package/dist/assets/{Toggle-CpYVC8bF.js → Toggle-BFhlpZ-n.js} +1 -1
  24. package/dist/assets/{ViewTabs-C3FN6P_C.js → ViewTabs-CuGlZ9Ed.js} +1 -1
  25. package/dist/assets/{historyTotals-C4Rs37Cd.js → historyTotals-Dxv9Fzrs.js} +1 -1
  26. package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
  27. package/dist/assets/{index-S-F5jp_h.js → index-Dx8owDeQ.js} +151 -151
  28. package/dist/assets/{settingsSchema-BZcdzHTQ.js → settingsSchema-DaqFSAQH.js} +1 -1
  29. package/dist/assets/{skillFrontmatter-EfNERt4b.js → skillFrontmatter-BIJddxVB.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-BfbrFvB3.js → useAgentsDirWatch-1hjZ4knk.js} +1 -1
  31. package/dist/index.html +2 -2
  32. package/package.json +3 -3
  33. package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -25
  34. package/scripts/scheduler-mcp-server.cjs +124 -74
  35. package/src/main/agentLibrary.cjs +8 -3
  36. package/src/main/build-info.json +4 -4
  37. package/src/main/crashDiagnostics.cjs +24 -0
  38. package/src/main/historyAggregator.cjs +41 -7
  39. package/src/main/index.cjs +13 -3
  40. package/src/main/ipcSchemas.cjs +30 -22
  41. package/src/main/lib/activeSessions.cjs +93 -32
  42. package/src/main/lib/epic/delegationReadiness.cjs +2 -0
  43. package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
  44. package/src/main/lib/epic/epicMint.cjs +1 -1
  45. package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
  46. package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
  47. package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
  48. package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
  49. package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
  50. package/src/main/lib/git/gitWorktree.cjs +9 -4
  51. package/src/main/lib/git/worktreeDeps.cjs +13 -2
  52. package/src/main/lib/git/worktreeSetup.cjs +212 -0
  53. package/src/main/lib/mcpToolCatalog.cjs +31 -2
  54. package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
  55. package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
  56. package/src/main/lib/prd/prdAgentType.cjs +28 -0
  57. package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
  58. package/src/main/lib/prd/prdCreate.cjs +205 -32
  59. package/src/main/lib/prd/prdGateFiles.cjs +31 -0
  60. package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
  61. package/src/main/lib/scheduler/queueStore.cjs +5 -4
  62. package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
  63. package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
  64. package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
  65. package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
  66. package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
  67. package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
  68. package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
  69. package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
  70. package/src/main/pty.cjs +0 -53
  71. package/src/main/runVerify.cjs +38 -11
  72. package/src/main/scheduler/prdParser.cjs +3 -0
  73. package/src/main/scheduler/prdPaths.cjs +48 -2
  74. package/src/main/scheduler.cjs +124 -29
  75. package/src/main/seedDevPlugin.cjs +1 -1
  76. package/src/main/seedSchedulerMcp.cjs +1 -1
  77. package/src/main/templates/PRD_AUTHORING.md +8 -4
  78. package/src/main/webRemote.cjs +38 -132
  79. package/src/preload/api.d.ts +4 -52
  80. package/src/preload/index.cjs +0 -24
  81. package/src/seed/agents/dev-lead.md +2 -1
  82. package/src/seed/agents/validator.md +2 -1
  83. package/dist/assets/DataModel-CENoTvc2.js +0 -1
  84. package/dist/assets/ListDetail-CkKXAM59.js +0 -1
  85. package/dist/assets/RemoteMachines-BFmbIGpW.js +0 -3
  86. package/dist/assets/Settings-DMoI0cqB.js +0 -4
  87. package/dist/assets/Skills-DYWQTQB1.js +0 -2
  88. package/dist/assets/SystemPrompt-Df0IfOuI.js +0 -1
  89. package/dist/assets/TagLibrary-BuMsGx-2.js +0 -1
  90. package/dist/assets/useRemotePeers-CKVakQPc.js +0 -1
  91. package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
  92. package/src/main/remoteClient.cjs +0 -334
  93. package/src/main/remoteClientIpc.cjs +0 -133
@@ -98,6 +98,31 @@ function defaultConfig() {
98
98
  return { remoteEnabled: false, remoteControlEnabled: false, devices: [] };
99
99
  }
100
100
 
101
+ // Retired desktop-to-desktop clients: drop pins that came from one (clientId is a
102
+ // string) so a leftover keypair is never auto-trusted; strip clientId from the rest.
103
+ function pruneDesktopPins(cfg) {
104
+ if (!cfg || !Array.isArray(cfg.devices)) return cfg;
105
+ const devices = cfg.devices.map((d) => {
106
+ if (!d || !Array.isArray(d.verifiedPeerPubKeys)) return d;
107
+ const verifiedPeerPubKeys = d.verifiedPeerPubKeys
108
+ .filter((p) => !(p && typeof p === 'object' && typeof p.clientId === 'string'))
109
+ .map((p) => {
110
+ if (!p || typeof p !== 'object') return p;
111
+ const { clientId: _drop, ...rest } = p;
112
+ return rest;
113
+ });
114
+ return { ...d, verifiedPeerPubKeys };
115
+ });
116
+ return { ...cfg, devices };
117
+ }
118
+
119
+ // Delete the orphaned desktop-client files. Never throws.
120
+ function removeOrphanedClientFiles(dir) {
121
+ for (const name of ['remote-peers.json', 'relay-client.json']) {
122
+ try { fs.rmSync(path.join(dir, name), { force: true }); } catch { /* best effort */ }
123
+ }
124
+ }
125
+
101
126
  function loadConfigSync() {
102
127
  const now = Date.now();
103
128
  if (_configCache && now - _configCacheAt < CONFIG_CACHE_TTL_MS) {
@@ -105,7 +130,7 @@ function loadConfigSync() {
105
130
  }
106
131
  try {
107
132
  const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
108
- _configCache = { ...defaultConfig(), ...JSON.parse(raw) };
133
+ _configCache = pruneDesktopPins({ ...defaultConfig(), ...JSON.parse(raw) });
109
134
  } catch {
110
135
  _configCache = defaultConfig();
111
136
  }
@@ -120,7 +145,7 @@ async function loadConfig() {
120
145
  }
121
146
  try {
122
147
  const raw = await fsp.readFile(CONFIG_PATH, 'utf8');
123
- _configCache = { ...defaultConfig(), ...JSON.parse(raw) };
148
+ _configCache = pruneDesktopPins({ ...defaultConfig(), ...JSON.parse(raw) });
124
149
  } catch {
125
150
  _configCache = defaultConfig();
126
151
  }
@@ -142,14 +167,14 @@ async function saveConfig(data) {
142
167
 
143
168
  // ─── Audit log ────────────────────────────────────────────────────────────────
144
169
 
145
- // Format: <ISO> <type> deviceId=<id> client=<clientId|connKey|-> msgId=<uuid> result=ok|error:<code>
170
+ // Format: <ISO> <type> deviceId=<id> client=<connKey|-> msgId=<uuid> result=ok|error:<code>
146
171
  // NEVER log token values or payload content.
147
172
  async function auditLog(ts, type, deviceId, msgId, result, client) {
148
173
  try {
149
174
  const ymd = ts.slice(0, 10);
150
175
  const logPath = path.join(AUDIT_LOG_DIR, `remote-audit-${ymd}.log`);
151
176
  const clean = (s) => String(s ?? '').replace(/[\r\n\t]+/g, ' ').slice(0, 200);
152
- const line = `${ts} ${clean(type)} deviceId=${clean(deviceId) || '-'} client=${clean(client && (client.clientId || client.key)) || '-'} msgId=${clean(msgId) || '-'} result=${result}\n`;
177
+ const line = `${ts} ${clean(type)} deviceId=${clean(deviceId) || '-'} client=${clean(client && (client.key)) || '-'} msgId=${clean(msgId) || '-'} result=${result}\n`;
153
178
  const handle = await fsp.open(logPath, 'a', 0o600);
154
179
  try {
155
180
  await handle.write(line);
@@ -257,10 +282,10 @@ function isPinnedPeerKey(device, pubKey) {
257
282
  }
258
283
 
259
284
  // Append a pin (never removing another client's), dedupe by pubKey, keep the newest MAX.
260
- function addPinnedPeerKey(device, pubKey, clientId) {
285
+ function addPinnedPeerKey(device, pubKey) {
261
286
  const prev = Array.isArray(device.verifiedPeerPubKeys) ? device.verifiedPeerPubKeys : [];
262
287
  const next = prev.filter((p) => p && p.pubKey !== pubKey);
263
- next.push({ pubKey, clientId: clientId ?? null, pinnedAt: new Date().toISOString() });
288
+ next.push({ pubKey, pinnedAt: new Date().toISOString() });
264
289
  return next.slice(-MAX_PINNED_PEER_KEYS);
265
290
  }
266
291
 
@@ -286,7 +311,6 @@ function computeStatus(cfg) {
286
311
  pendingSas: connected && !authed && pending ? pending.e2e.pendingSas : null,
287
312
  clients: _clients.values().map((r) => ({
288
313
  key: r.key,
289
- clientId: r.clientId ?? null,
290
314
  state: r.e2e.state,
291
315
  pendingSas: r.e2e.state === 'pending_sas' ? r.e2e.pendingSas : null,
292
316
  })),
@@ -680,41 +704,6 @@ function stopAllSessionWatches() {
680
704
  for (const tabId of Array.from(_sessionWatchers.keys())) stopSessionWatch(tabId);
681
705
  }
682
706
 
683
- /**
684
- * Pure builder for the phone's session list: one project entry per tab, then
685
- * each tab's ACTIVE Epics (from active-index.json via readIndex). Never throws.
686
- */
687
- function buildRemoteSessionList(tabs, readIndex) {
688
- const out = [];
689
- const seenEpics = new Set();
690
- for (const tab of Array.isArray(tabs) ? tabs : []) {
691
- if (!tab) continue;
692
- const project = tab.label || path.basename(tab.cwd || '');
693
- out.push({ kind: 'project', tabId: tab.sessionId, cwd: tab.cwd, title: tab.label || tab.cwd, project });
694
- let sessions = null;
695
- try { sessions = readIndex(tab.cwd)?.sessions; } catch { sessions = null; }
696
- if (!sessions || typeof sessions !== 'object') continue;
697
- for (const epic of Object.values(sessions)) {
698
- if (!epic || epic.status !== 'active') continue;
699
- if (typeof epic.claudeSessionId !== 'string' || !epic.claudeSessionId) continue;
700
- if (seenEpics.has(epic.id)) continue;
701
- seenEpics.add(epic.id);
702
- const firstLine = typeof epic.goalText === 'string' ? epic.goalText.split('\n')[0].trim().slice(0, 80).trim() : '';
703
- out.push({
704
- kind: 'epic',
705
- tabId: epic.claudeSessionId,
706
- epicId: epic.id,
707
- cwd: epic.worktree?.dir || epic.cwd,
708
- title: firstLine || epic.id,
709
- project,
710
- tag: epic.tag,
711
- agentType: epic.agentType,
712
- });
713
- }
714
- }
715
- return out;
716
- }
717
-
718
707
  /**
719
708
  * Shared finish-up for any path that transitions a client's e2e state into 'authenticated'
720
709
  * (manual SAS confirmation or pinned-key auto-trust): log, broadcast the new
@@ -780,7 +769,7 @@ async function handleMessage(raw, device) {
780
769
  }
781
770
 
782
771
  // Per-client E2E record, keyed by the relay-stamped fromConn (else 'legacy').
783
- const client = _clients.getOrCreate(_clients.clientKeyOf(envelope), { clientId: envelope.fromClientId ?? null });
772
+ const client = _clients.getOrCreate(_clients.clientKeyOf(envelope));
784
773
 
785
774
  // ── E2E key exchange ───────────────────────────────────────────────────────
786
775
  // Browser sends e2e:hello with its ephemeral P-256 public key (SPKI base64url).
@@ -1000,38 +989,16 @@ function respond(msgId, payload, typeOverride, client) {
1000
989
  }
1001
990
  }
1002
991
 
1003
- function stopClientPtyStream(client, tabId) {
1004
- const unsub = client.ptyStreams.get(tabId);
1005
- client.ptyStreams.delete(tabId);
1006
- if (unsub) {
1007
- try { unsub(); } catch { /* listener already gone */ }
1008
- }
1009
- }
1010
-
1011
992
  // Lazy-loaded dispatch map — avoids circular require at module load time.
1012
993
  let _dispatchMap = null;
1013
994
 
1014
995
  function getDispatchMap() {
1015
996
  if (_dispatchMap) return _dispatchMap;
1016
997
 
1017
- const { manager: ptyManager } = require('./pty.cjs');
1018
998
  const sessionsStore = require('./sessionsStore.cjs');
1019
- const { remote: histRemote } = require('./historyAggregator.cjs');
1020
999
  const chatRunner = require('./chatRunner.cjs');
1021
1000
 
1022
1001
  _dispatchMap = {
1023
- 'cmd:sessions:load': async () =>
1024
- sessionsStore.load(),
1025
-
1026
- 'cmd:sessions:list': async () => {
1027
- const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1028
- const data = await sessionsStore.load();
1029
- return {
1030
- sessions: buildRemoteSessionList(data?.tabs ?? [], readActiveIndex),
1031
- activeTabId: data?.activeTabId ?? null,
1032
- };
1033
- },
1034
-
1035
1002
  'cmd:projects:list': async () => {
1036
1003
  const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1037
1004
  const { listRemoteProjects } = require('./lib/remoteDrilldown.cjs');
@@ -1104,71 +1071,9 @@ function getDispatchMap() {
1104
1071
  return { prds };
1105
1072
  },
1106
1073
 
1107
- 'cmd:sessions:save': async (payload) => {
1108
- const parsed = schemas.sessionsPayload.parse(payload);
1109
- return sessionsStore.save(parsed);
1110
- },
1111
-
1112
- 'cmd:pty:spawn': async (payload, reqClient) => {
1113
- const client = reqClient || _clients.getOrCreate(LEGACY_CLIENT_KEY);
1114
- const parsed = schemas.ptySpawn.parse(payload);
1115
- // Path safety — validatePath rejects anything outside home dir.
1116
- validatePath(parsed.cwd);
1117
- // Strip startupCommand: it is ignored by pty.cjs today, but passing a
1118
- // remotely-controlled 8 KiB string to spawn is a landmine if pty.cjs
1119
- // ever uses it. Remote callers have no legitimate need for it.
1120
- const { startupCommand: _ignored, ...safePayload } = parsed;
1121
- const result = await ptyManager.spawn(safePayload);
1122
- // Stream output to the peer — replaces any prior subscription for this tab.
1123
- // Chunk contents are sensitive: never logged.
1124
- const tabId = safePayload.tabId;
1125
- stopClientPtyStream(client, tabId);
1126
- const unsub = ptyManager.addOutputListener(
1127
- tabId,
1128
- (_id, data) => {
1129
- if (client.e2e.state !== 'authenticated' || !loadConfigSync().remoteEnabled) return;
1130
- pushEventTo(client, 'event:pty:data', { tabId, data });
1131
- },
1132
- (_id, info) => {
1133
- if (client.ptyStreams.get(tabId) === unsub) client.ptyStreams.delete(tabId);
1134
- if (client.e2e.state !== 'authenticated' || !loadConfigSync().remoteEnabled) return;
1135
- pushEventTo(client, 'event:pty:exit', { tabId, exitCode: info?.exitCode, signal: info?.signal });
1136
- },
1137
- );
1138
- client.ptyStreams.set(tabId, unsub);
1139
- return result;
1140
- },
1141
-
1142
- 'cmd:pty:write': async (payload) => {
1143
- const parsed = schemas.ptyWrite.parse(payload);
1144
- return ptyManager.write(parsed);
1145
- },
1146
-
1147
- 'cmd:pty:resize': async (payload) => {
1148
- const parsed = schemas.ptyResize.parse(payload);
1149
- ptyManager.resize(parsed);
1150
- return { ok: true };
1151
- },
1152
-
1153
- 'cmd:pty:kill': async (payload, reqClient) => {
1154
- const parsed = schemas.ptyTabId.parse(payload);
1155
- // Kill ends the PTY itself, so every client's stream for it ends.
1156
- for (const c of _clients.values()) stopClientPtyStream(c, parsed.tabId);
1157
- void reqClient;
1158
- ptyManager.kill(parsed.tabId);
1159
- return { ok: true };
1160
- },
1161
-
1162
- 'cmd:history:aggregate': async (payload) => {
1163
- const parsed = schemas.historyAggregate.parse(payload);
1164
- return histRemote.aggregate(parsed);
1165
- },
1166
-
1167
- 'cmd:app:version': async () =>
1168
- app.getVersion(),
1169
-
1074
+ // Web app's multi-desktop picker (SAS-gated: hostname identifies the machine).
1170
1075
  'cmd:app:info': async () =>
1171
- ({ version: app.getVersion(), platform: process.platform }),
1076
+ ({ version: app.getVersion(), platform: process.platform, hostname: os.hostname() }),
1172
1077
 
1173
1078
  // v2 mobile: start/stop pushing live state for a session.
1174
1079
  'cmd:session:subscribe': async (payload, reqClient) => {
@@ -1388,6 +1293,7 @@ async function revokeAllDevices() {
1388
1293
  // ─── IPC handlers ─────────────────────────────────────────────────────────────
1389
1294
 
1390
1295
  function registerRemoteHandlers() {
1296
+ removeOrphanedClientFiles(path.join(claudeHome(), 'session-manager'));
1391
1297
  const { validated } = require('./ipcSchemas.cjs');
1392
1298
 
1393
1299
  // Returns current status without tokens — safe to expose to renderer.
@@ -1425,7 +1331,7 @@ function registerRemoteHandlers() {
1425
1331
  const cfg = await loadConfig();
1426
1332
  const devices = (cfg.devices || []).map((d) =>
1427
1333
  d.deviceId === target.deviceId
1428
- ? { ...d, verifiedPeerPubKeys: addPinnedPeerKey(d, target.peerPubKey, target.clientId) }
1334
+ ? { ...d, verifiedPeerPubKeys: addPinnedPeerKey(d, target.peerPubKey) }
1429
1335
  : d
1430
1336
  );
1431
1337
  await saveConfig({ ...cfg, devices });
@@ -1536,13 +1442,14 @@ module.exports = {
1536
1442
  getE2eState: (clientKey = LEGACY_CLIENT_KEY) => (_clients.get(clientKey) || { e2e: makeState() }).e2e,
1537
1443
  },
1538
1444
  __test: {
1445
+ pruneDesktopPins,
1446
+ removeOrphanedClientFiles,
1539
1447
  getDispatchMap,
1540
1448
  auditResultFor,
1541
1449
  startSessionWatch,
1542
1450
  pollSessionWatcher,
1543
1451
  getSessionWatcher: (tabId) => _sessionWatchers.get(tabId),
1544
1452
  stopAllSessionWatches,
1545
- buildRemoteSessionList,
1546
1453
  setWs: (ws) => { _ws = ws; },
1547
1454
  connect,
1548
1455
  hasReconnectTimer: () => _reconnectTimer !== null,
@@ -1556,6 +1463,5 @@ module.exports = {
1556
1463
  getClient: (clientKey = LEGACY_CLIENT_KEY) => _clients.getOrCreate(clientKey),
1557
1464
  revokeAllDevices,
1558
1465
  pair,
1559
- streamCount: () => _clients.values().reduce((n, c) => n + c.ptyStreams.size, 0),
1560
1466
  },
1561
1467
  };
@@ -342,6 +342,8 @@ export interface AgentPersona {
342
342
  title?: string | null;
343
343
  /** Bundled-persona version stamp (seedAgentPersonas.cjs). Null for a persona that was never seeded, or has no stamp. */
344
344
  seedVersion?: number | null;
345
+ /** `executorOnly: true` frontmatter — persona only executes a scoped PRD; New Epic warns when it is picked as Actor. */
346
+ executorOnly?: boolean;
345
347
  path: string;
346
348
  body: string;
347
349
  /** Project names (basename of cwd) whose currently-open tab overlays this agent. */
@@ -374,6 +376,8 @@ export interface AgentPersonaSaveInput {
374
376
  projectName?: string;
375
377
  /** Bundled-persona version stamp (seedAgentPersonas.cjs). Omit to carry over the existing file's stamp unchanged. */
376
378
  seedVersion?: number;
379
+ /** Omit to keep the existing file's value. */
380
+ executorOnly?: boolean;
377
381
  body: string;
378
382
  }
379
383
 
@@ -1671,43 +1675,6 @@ export interface WebRemoteDevice {
1671
1675
  lastConnectedAt: string | null;
1672
1676
  }
1673
1677
 
1674
- export interface RemoteMachinePeer {
1675
- peerId: string;
1676
- deviceId: string;
1677
- machineName: string;
1678
- pairedAt: string;
1679
- clientPublicKey: string;
1680
- verifiedDevicePubKey: string | null;
1681
- /** process.platform of the remote machine, last known (null until first authenticated). */
1682
- os: string | null;
1683
- status: string;
1684
- reason: string | null;
1685
- sas: string | null;
1686
- }
1687
-
1688
- export interface RemoteClientDevice {
1689
- deviceId: string;
1690
- email: string;
1691
- issuedAt: string;
1692
- /** null = never expires (relay keeps tokens until revoked). */
1693
- expiresAt: string | null;
1694
- isOnline: boolean;
1695
- devicePubKey: string;
1696
- }
1697
-
1698
- export interface RemoteClientStatus {
1699
- peerId: string;
1700
- status: string;
1701
- reason: string | null;
1702
- sas: string | null;
1703
- }
1704
-
1705
- export interface RemoteClientEvent {
1706
- peerId: string;
1707
- type: string;
1708
- payload: unknown;
1709
- }
1710
-
1711
1678
  export interface WebRemoteStatus {
1712
1679
  enabled: boolean;
1713
1680
  remoteControlEnabled: boolean;
@@ -1728,7 +1695,6 @@ export interface WebRemoteStatus {
1728
1695
 
1729
1696
  export interface WebRemoteClientStatus {
1730
1697
  key: string;
1731
- clientId: string | null;
1732
1698
  state: 'idle' | 'pending_sas' | 'authenticated' | 'failed';
1733
1699
  pendingSas: string | null;
1734
1700
  }
@@ -2242,20 +2208,6 @@ export interface SessionManagerAPI {
2242
2208
  * renderer needs per-row badges without a separate git call per file. */
2243
2209
  fileStatus: (cwd: string) => Promise<GitFileStatusMap>;
2244
2210
  };
2245
- remoteClient: {
2246
- list: () => Promise<RemoteMachinePeer[]>;
2247
- connect: (peerId: string) => Promise<{ status: string; reason: string | null; sas: string | null }>;
2248
- confirmSas: (peerId: string) => Promise<{ ok: boolean; reason?: string }>;
2249
- remove: (peerId: string) => Promise<{ ok: boolean }>;
2250
- authStatus: () => Promise<{ paired: boolean; clientId: string | null }>;
2251
- pair: (code: string, clientName?: string) => Promise<{ ok: boolean; clientId?: string; error?: string }>;
2252
- unpair: () => Promise<{ ok: boolean }>;
2253
- devices: () => Promise<{ ok: boolean; devices?: RemoteClientDevice[]; error?: string }>;
2254
- add: (deviceId: string, machineName: string) => Promise<RemoteMachinePeer>;
2255
- send: (peerId: string, type: string, payload?: unknown) => Promise<unknown>;
2256
- onStatus: (handler: (status: RemoteClientStatus) => void) => () => void;
2257
- onEvent: (handler: (ev: RemoteClientEvent) => void) => () => void;
2258
- };
2259
2211
  updater: {
2260
2212
  status: () => Promise<AutoUpdaterStatus>;
2261
2213
  check: () => Promise<AutoUpdaterStatus>;
@@ -404,30 +404,6 @@ contextBridge.exposeInMainWorld('api', {
404
404
  status: (cwd) => ipcRenderer.invoke('git:status', { cwd }),
405
405
  fileStatus: (cwd) => ipcRenderer.invoke('git:file-status', { cwd }),
406
406
  },
407
- remoteClient: {
408
- /** Paired remote machines merged with live connection status. */
409
- list: () => ipcRenderer.invoke('remoteClient:list'),
410
- connect: (peerId) => ipcRenderer.invoke('remoteClient:connect', { peerId }),
411
- /** Confirm the short authentication string matches the remote machine. */
412
- confirmSas: (peerId) => ipcRenderer.invoke('remoteClient:confirm-sas', { peerId }),
413
- remove: (peerId) => ipcRenderer.invoke('remoteClient:remove', { peerId }),
414
- authStatus: () => ipcRenderer.invoke('remoteClient:auth-status'),
415
- pair: (code, clientName) => ipcRenderer.invoke('remoteClient:pair', { code, clientName }),
416
- unpair: () => ipcRenderer.invoke('remoteClient:unpair'),
417
- devices: () => ipcRenderer.invoke('remoteClient:devices'),
418
- add: (deviceId, machineName) => ipcRenderer.invoke('remoteClient:add', { deviceId, machineName }),
419
- send: (peerId, type, payload) => ipcRenderer.invoke('remoteClient:send', { peerId, type, payload }),
420
- onStatus: (handler) => {
421
- const listener = (_e, payload) => handler(payload);
422
- ipcRenderer.on('remoteClient:status', listener);
423
- return () => ipcRenderer.removeListener('remoteClient:status', listener);
424
- },
425
- onEvent: (handler) => {
426
- const listener = (_e, payload) => handler(payload);
427
- ipcRenderer.on('remoteClient:event', listener);
428
- return () => ipcRenderer.removeListener('remoteClient:event', listener);
429
- },
430
- },
431
407
  updater: {
432
408
  status: () => ipcRenderer.invoke('updater:status'),
433
409
  check: () => ipcRenderer.invoke('updater:check'),
@@ -5,7 +5,8 @@ tools: Read, Grep, Glob, Bash, Edit, Write
5
5
  model: sonnet
6
6
  effort: medium
7
7
  title: Engineering — Software Engineer
8
- seedVersion: 4
8
+ seedVersion: 5
9
+ executorOnly: true
9
10
  ---
10
11
 
11
12
  You are dev-lead. You execute one PRD — nothing above it, nothing beyond it. You don't decide
@@ -4,7 +4,8 @@ description: Validates a finished PLAN (the PRDs its validate PRD lists) once, a
4
4
  tools: Read, Grep, Glob, Bash
5
5
  model: sonnet
6
6
  title: Engineering — Plan Validator
7
- seedVersion: 3
7
+ seedVersion: 4
8
+ executorOnly: true
8
9
  ---
9
10
 
10
11
  You are validator. You judge whether a plan's PRDs actually landed what they promised. You do not fix anything, you do not re-implement, you do not queue PRDs — your output is evidence and verdicts, read by the architect who owns the plan.
@@ -1 +0,0 @@
1
- import{aF as d,j,aq as $,a9 as g,ap as e,n as N,at as E,aa as b,aJ as S,a as f,a4 as T,k as v,i as D}from"./index-S-F5jp_h.js";import{P as R}from"./Panel-C92O_aAH.js";import{V}from"./ViewTabs-C3FN6P_C.js";import"./monaco-editor-BsIlgG7c.js";const C=[{key:"diagram",label:"Diagram"},{key:"tables",label:"Tables"}];function G(){const[s,t]=d.useState("diagram"),[a,i]=d.useState(null),[l,n]=d.useState(()=>new Set),c=d.useMemo(()=>j.filter(r=>!l.has(r.group)),[l]),m=d.useMemo(()=>$(c),[c]),o=r=>{n(x=>{const h=new Set(x);return h.has(r)?h.delete(r):h.add(r),h}),i(null)},u=a?g(a):null,y=r=>c.some(x=>x.id===r);return e.jsx(R,{toolbar:e.jsxs(e.Fragment,{children:[e.jsx(V,{options:C,active:s,onChange:t}),e.jsx(M,{hiddenGroups:l,onToggle:o}),e.jsxs("span",{className:"ml-auto text-fg-faint",children:[j.length," entities"]})]}),children:s==="diagram"?e.jsxs("div",{className:"h-full flex",children:[e.jsx("div",{className:"flex-1 min-w-0 overflow-auto",children:e.jsx(O,{layout:m,selectedId:a,onSelect:r=>i(x=>x===r?null:r)})}),e.jsx("div",{className:"w-96 shrink-0 border-l border-line overflow-y-auto",children:u?e.jsx(F,{entity:u,onSelectRelated:r=>y(r)?i(r):void 0}):e.jsx(N,{title:"select an entity",hint:"click a box in the diagram to see its storage, fields, and relations"})})]}):e.jsx(P,{})})}const X=d.memo(G);function M({hiddenGroups:s,onToggle:t}){return e.jsx("div",{className:"flex items-center gap-1.5 flex-wrap",children:v.map(a=>{const i=s.has(a.id);return e.jsxs("button",{onClick:()=>t(a.id),title:`${a.blurb} (${a.realm})`,className:`flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10.5px] ${i?"border-line text-fg-faint opacity-50":"border-line text-fg-dim hover:text-fg"}`,children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full ${a.tone.dot}`}),a.label,e.jsx("span",{className:"text-fg-faint",children:a.realm})]},a.id)})})}function O({layout:s,selectedId:t,onSelect:a}){const i=d.useMemo(()=>t?new Set(E(t)):null,[t]),l=n=>!!t&&(n.relation.from===t||n.relation.to===t);return s.boxes.length===0?e.jsx(N,{title:"no entities in view",hint:"toggle a group in the legend to show its entities"}):e.jsxs("div",{className:"relative p-4",style:{width:s.width+32,height:s.height+32},children:[e.jsxs("svg",{width:s.width,height:s.height,className:"absolute left-4 top-4 overflow-visible",children:[s.edges.map(n=>{const c=l(n);return e.jsx("path",{"data-testid":`erd-edge-${n.relation.from}-${n.relation.to}`,d:n.d,fill:"none",className:c?"stroke-accent":"stroke-line",strokeWidth:c?2:1,opacity:t&&!c?.25:1},`${n.relation.from}-${n.relation.to}-${n.relation.label}`)}),s.edges.map(n=>l(n)?e.jsx("text",{x:n.mx,y:n.my,textAnchor:"middle",className:"fill-fg-dim text-[10px]",children:n.relation.label},`label-${n.relation.from}-${n.relation.to}-${n.relation.label}`):null)]}),s.boxes.map(n=>e.jsx(_,{box:n,selected:n.id===t,dimmed:!!t&&n.id!==t&&!(i!=null&&i.has(n.id)),onClick:()=>a(n.id)},n.id))]})}function _({box:s,selected:t,dimmed:a,onClick:i}){const l=g(s.id),n=b(s.group),c=l.fields.slice(0,D),m=l.fields.length-c.length;return e.jsxs("button",{type:"button","data-testid":`erd-box-${s.id}`,onClick:i,className:`absolute text-left rounded border overflow-hidden bg-bg-hi ${t?"border-accent ring-1 ring-accent":"border-line"} ${a?"opacity-30":""}`,style:{left:s.x,top:s.y,width:s.w,height:s.h},children:[e.jsxs("div",{className:"px-2 py-1 border-b border-line flex items-center gap-1.5",children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full shrink-0 ${n.tone.dot}`}),e.jsx("span",{className:"text-[11px] font-semibold text-fg truncate",children:l.name})]}),l.aka&&e.jsx("div",{className:"px-2 pt-0.5 text-[9.5px] text-fg-faint truncate",children:l.aka}),e.jsxs("div",{className:"px-2 py-0.5",children:[c.map(o=>e.jsxs("div",{className:"text-[9.5px] font-mono text-fg-dim truncate flex items-center gap-1",children:[o.key&&e.jsx("span",{className:"text-fg-faint",children:o.key}),e.jsx("span",{className:"truncate",children:o.name})]},o.name)),m>0&&e.jsxs("div",{className:"text-[9.5px] text-fg-faint italic",children:["+",m," more"]})]})]})}function w({fields:s}){return e.jsxs("table",{className:"w-full text-[11px] font-mono",children:[e.jsx("thead",{className:"text-fg-faint uppercase tracking-wide text-[9.5px]",children:e.jsxs("tr",{className:"border-b border-line",children:[e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"name"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"type"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"key"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"note"})]})}),e.jsx("tbody",{children:s.map(t=>e.jsxs("tr",{className:"border-b border-line/50",children:[e.jsxs("td",{className:"px-2 py-1 text-fg",children:[t.name,t.optional&&e.jsx("span",{className:"text-fg-faint",children:"?"})]}),e.jsx("td",{className:"px-2 py-1 text-fg-dim",children:t.type}),e.jsx("td",{className:"px-2 py-1",children:t.key&&e.jsxs(f,{tone:t.key==="pk"?"accent":"dim",children:[t.key,t.ref?` → ${t.ref}`:""]})}),e.jsx("td",{className:"px-2 py-1 text-fg-faint",children:t.note})]},t.name))})]})}function k({entity:s}){return e.jsxs("div",{className:"space-y-1 text-[11px]",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"path"}),e.jsx("span",{className:"font-mono text-fg-dim break-all",children:s.store.path})]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"format"}),e.jsx("span",{className:"font-mono text-fg-dim",children:s.store.format})]}),s.store.writer&&e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"writer"}),e.jsxs(f,{tone:"accent",children:["single-writer: ",s.store.writer]})]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"definedIn"}),e.jsx("span",{className:"font-mono text-fg-dim break-all",children:s.store.definedIn})]})]})}function F({entity:s,onSelectRelated:t}){const a=b(s.group),i=S(s.id);return e.jsxs("div",{className:"p-3 space-y-3",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${a.tone.dot}`}),e.jsx("span",{className:"text-sm font-semibold text-fg",children:s.name})]}),s.aka&&e.jsx("div",{className:"text-[10.5px] text-fg-faint",children:s.aka}),e.jsx(f,{tone:"dim",className:"mt-1",children:a.label})]}),e.jsx("div",{className:"text-xs text-fg-dim leading-relaxed",children:s.summary}),e.jsxs("div",{children:[e.jsx(p,{children:"storage"}),e.jsx(k,{entity:s})]}),e.jsxs("div",{children:[e.jsx(p,{children:"fields"}),e.jsx(w,{fields:s.fields})]}),s.notes&&s.notes.length>0&&e.jsxs("div",{children:[e.jsx(p,{children:"notes"}),e.jsx("ul",{className:"list-disc pl-4 space-y-0.5 text-xs text-fg-dim",children:s.notes.map(l=>e.jsx("li",{children:l},l))})]}),e.jsxs("div",{children:[e.jsx(p,{children:"relations"}),i.length===0?e.jsx("div",{className:"text-xs text-fg-faint italic",children:"no relations"}):e.jsx("ul",{className:"space-y-1",children:i.map(l=>{const n=l.from===s.id?l.to:l.from,c=g(n);return e.jsxs("li",{className:"flex items-center gap-1.5 text-xs",children:[e.jsx("span",{className:"font-mono text-fg-faint w-10 shrink-0",children:T(l.cardinality)}),e.jsx("span",{className:"text-fg-dim",children:l.label}),l.via&&e.jsxs("span",{className:"font-mono text-[10px] text-fg-faint",children:["via ",l.via]}),e.jsx("button",{type:"button",onClick:()=>t(n),className:"ml-auto text-accent hover:underline font-mono",children:c.name})]},`${l.from}-${l.to}-${l.label}`)})})]})]})}function p({children:s}){return e.jsx("div",{className:"text-[10.5px] uppercase tracking-wide text-fg-faint mb-1 font-mono",children:s})}function P(){return e.jsx("div",{className:"p-4 space-y-6",children:v.map(s=>{const t=j.filter(a=>a.group===s.id);return t.length===0?null:e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-1.5 mb-2",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${s.tone.dot}`}),e.jsx("span",{className:"text-xs font-semibold text-fg",children:s.label}),e.jsx("span",{className:"text-[10.5px] text-fg-faint",children:s.realm})]}),e.jsx("div",{className:"grid grid-cols-1 xl:grid-cols-2 gap-3",children:t.map(a=>e.jsxs("div",{className:"border border-line rounded bg-bg-hi p-3 space-y-2",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-sm font-semibold text-fg",children:a.name}),a.aka&&e.jsx("span",{className:"text-[10.5px] text-fg-faint",children:a.aka})]}),e.jsx(f,{tone:"dim",className:"mt-1",children:s.label})]}),e.jsx("div",{className:"text-xs text-fg-dim leading-relaxed",children:a.summary}),e.jsx(k,{entity:a}),e.jsx(w,{fields:a.fields}),a.notes&&a.notes.length>0&&e.jsx("ul",{className:"list-disc pl-4 space-y-0.5 text-xs text-fg-dim",children:a.notes.map(i=>e.jsx("li",{children:i},i))})]},a.id))})]},s.id)})})}export{X as DataModel};
@@ -1 +0,0 @@
1
- import{ap as e}from"./index-S-F5jp_h.js";function o({sidebar:r,detail:s,sidebarWidth:i="18rem"}){return e.jsxs("div",{className:"h-full flex",children:[e.jsx("div",{className:"shrink-0 border-r border-line overflow-y-auto",style:{width:i},children:r}),e.jsx("div",{className:"flex-1 min-w-0 overflow-auto",children:s})]})}export{o as L};
@@ -1,3 +0,0 @@
1
- import{bf as _,aF as d,a2 as P,ap as e,a as N,bn as D,X as O,D as A,a0 as H,a3 as U}from"./index-S-F5jp_h.js";import{b as L,P as E,E as T,R as z,u as W,S as G}from"./useRemotePeers-CKVakQPc.js";import"./monaco-editor-BsIlgG7c.js";function M(o){return o instanceof Error?o.message:String(o)}function V({peerId:o,machineName:i,onOpenTerminal:y}){const c=_(a=>a.show),[x,m]=d.useState([]),[p,w]=d.useState("loading"),[h,k]=d.useState(null),[t,j]=d.useState(""),[C,I]=d.useState(!1),f=d.useRef(0),v=d.useCallback(async()=>{const a=++f.current;w("loading");try{let u;try{const s=await window.api.remoteClient.send(o,"cmd:sessions:list",{});u=Array.isArray(s==null?void 0:s.sessions)?s.sessions:[]}catch{if(a!==f.current)return;const s=await window.api.remoteClient.send(o,"cmd:sessions:load",{});u=(Array.isArray(s==null?void 0:s.tabs)?s.tabs:[]).map(b=>({kind:"project",tabId:b.sessionId,cwd:b.cwd,title:b.label||b.cwd,project:b.label||P(b.cwd)}))}if(a!==f.current)return;m(u),w("ready")}catch(u){if(a!==f.current)return;w("error"),c("error",`Could not load sessions from ${i}: ${M(u)}`)}},[o,i,c]);d.useEffect(()=>(v(),()=>{f.current++}),[v]);const S=d.useMemo(()=>{const a=new Map;for(const s of x){const g=s.project||P(s.cwd);(a.get(g)??a.set(g,[]).get(g)).push(s)}const u=s=>s.kind==="project"?0:1;for(const s of a.values())s.sort((g,b)=>u(g)-u(b));return[...a.entries()].sort((s,g)=>s[0].localeCompare(g[0]))},[x]),n=t.trim(),$=h!==null&&n.length>0&&!C,R=d.useCallback(async()=>{if(!(h===null||t.trim().length===0)){I(!0);try{await window.api.remoteClient.send(o,"cmd:chat:send",{tabId:h,prompt:t}),j("")}catch(a){c("error",`Could not send prompt to ${i}: ${M(a)}`)}finally{I(!1)}}},[o,i,h,t,c]);return e.jsxs("div",{"data-testid":"remote-sessions-panel",className:"flex flex-col gap-3",children:[e.jsx(L,{right:e.jsx(N,{tone:"accent",className:"",title:`Sessions on ${i}`,children:e.jsxs("span",{"data-testid":"remote-sessions-badge",children:["Remote: ",i]})}),children:"Remote sessions"}),e.jsx("div",{className:"flex justify-end",children:e.jsx(E,{icon:"history",testId:"remote-sessions-refresh",onClick:()=>{v()},disabled:p==="loading",children:"Refresh"})}),p==="loading"&&e.jsx(T,{testId:"remote-sessions-loading",children:"Loading sessions…"}),p==="error"&&e.jsxs(T,{testId:"remote-sessions-error",children:["Could not load sessions from ",i,"."]}),p==="ready"&&x.length===0&&e.jsxs(T,{testId:"remote-sessions-empty",children:["No sessions open on ",i,"."]}),p==="ready"&&S.map(([a,u])=>e.jsx("div",{"data-testid":"remote-sessions-group",className:"flex flex-col gap-2",children:u.map(s=>e.jsx(z,{testId:`remote-session-${s.tabId}`,icon:"terminal",title:s.title||s.cwd,subtitle:s.cwd,badge:s.kind==="epic"&&s.tag?e.jsx(N,{tone:"accent",children:s.tag}):void 0,action:e.jsxs("div",{className:"flex items-center gap-2",children:[s.kind==="project"&&y&&e.jsx(E,{icon:"terminal",kind:"ghost",testId:`remote-session-terminal-${s.tabId}`,onClick:()=>y(s.cwd),children:"Terminal here"}),e.jsx(E,{kind:h===s.tabId?"soft":"ghost",testId:`remote-session-select-${s.tabId}`,onClick:()=>k(s.tabId),children:h===s.tabId?"Selected":"Select"})]})},s.tabId))},a)),p==="ready"&&x.length>0&&e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx("textarea",{"data-testid":"remote-sessions-prompt",value:t,onChange:a=>j(a.target.value),rows:3,placeholder:h?`Prompt for the selected session on ${i}`:"Select a session first",className:"w-full rounded-lg bg-bg-hi border border-line px-3 py-2 text-[13px] text-fg placeholder:text-fg-faint"}),e.jsx("div",{className:"flex justify-end",children:e.jsx(E,{icon:"play",testId:"remote-sessions-send",onClick:()=>{R()},disabled:!$,children:"Send"})})]})]})}const X=260;function q({peerId:o,machineName:i,cwd:y,onClose:c}){const x=d.useRef(null),m=d.useRef(null),p=_(t=>t.show),[w,h]=d.useState(!1);d.useEffect(()=>{const t=x.current;if(!t)return;const j=crypto.randomUUID();let C=!0,I=!1,f=!1;const v=(r,l)=>{C&&p("error",`Remote terminal ${r} failed: ${l instanceof Error?l.message:String(l)}`)},S=(r,l)=>window.api.remoteClient.send(o,r,l),n=new D.Terminal({fontFamily:'"IBM Plex Mono", JetBrains Mono, ui-monospace, Menlo, monospace',fontSize:A.fontSize,lineHeight:1.2,cursorBlink:!0,allowProposedApi:!0,theme:O[A.theme]}),$=new H.FitAddon;n.loadAddon($);const R=()=>{if(U(t.clientWidth,t.clientHeight))try{$.fit()}catch{}};n.open(t),R(),n.focus();const a=window.api.remoteClient.onEvent(r=>{if(r.peerId!==o)return;const l=r.payload??{};l.tabId===j&&(r.type==="event:pty:data"?typeof l.data=="string"&&n.write(l.data):r.type==="event:pty:exit"&&(I=!0,n.options.disableStdin=!0,n.write(`\r
2
- \x1B[38;5;240m[remote session exited code=${l.exitCode??"?"}]\x1B[0m\r
3
- `),h(!0)))}),u=n.onData(r=>{I||S("cmd:pty:write",{tabId:j,data:r}).catch(l=>v("write",l))}),s=n.onResize(({cols:r,rows:l})=>{I||S("cmd:pty:resize",{tabId:j,cols:r,rows:l}).catch(F=>v("resize",F))});S("cmd:pty:spawn",{tabId:j,cwd:y,cols:n.cols,rows:n.rows}).catch(r=>v("spawn",r)),m.current=()=>{f||(f=!0,S("cmd:pty:kill",{tabId:j}).catch(r=>v("kill",r)))};const g=()=>R();window.addEventListener("resize",g);const b=new ResizeObserver(()=>R());return b.observe(t),()=>{var r;window.removeEventListener("resize",g),b.disconnect(),a(),u.dispose(),s.dispose(),(r=m.current)==null||r.call(m),m.current=null,C=!1,n.dispose()}},[o,y,p]);const k=()=>{var t;(t=m.current)==null||t.call(m),c()};return e.jsxs("div",{"data-testid":"remote-terminal-panel",className:"bg-bg-hi border border-line rounded-xl overflow-hidden",children:[e.jsxs("div",{className:"flex items-center justify-between gap-3 px-[18px] pt-3.5 pb-2",children:[e.jsxs("div",{className:"flex items-center gap-2.5 min-w-0",children:[e.jsx(L,{children:"Remote terminal"}),e.jsx(N,{tone:"accent",children:e.jsxs("span",{"data-testid":"remote-terminal-badge",children:["Remote: ",i]})}),w&&e.jsx(N,{tone:"dim",children:"exited"})]}),e.jsx(E,{kind:"ghost",onClick:k,testId:"remote-terminal-close",children:"Close"})]}),e.jsx("div",{ref:x,"data-testid":"remote-terminal-host",className:"w-full bg-bg",style:{height:X}})]})}const J="relay_client_auth_unavailable",K="device_offline",Q={win32:"Windows",darwin:"macOS",linux:"Linux"},B=o=>o?Q[o]??o:"OS unknown";function Y({peers:o,selectedId:i,onSelect:y}){return e.jsxs("div",{className:"w-[240px] shrink-0 border-r border-line p-3 space-y-2 overflow-y-auto","data-testid":"remote-view-switcher",children:[e.jsx(L,{children:"Machines"}),o.map(c=>{const x=c.status==="authenticated";return e.jsxs("button",{type:"button","data-testid":"remote-view-machine","aria-pressed":c.peerId===i,onClick:()=>y(c.peerId),className:`w-full text-left rounded-lg border px-3 py-2 transition-colors ${c.peerId===i?"bg-bg-hi border-accent/40":"bg-bg border-line hover:bg-bg-hi"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsx("span",{className:"text-sm font-semibold text-fg truncate",children:c.machineName}),x?e.jsx(N,{tone:"good",children:"online"}):e.jsx(N,{tone:"dim",children:"offline"})]}),e.jsx("div",{className:"text-[11.5px] text-fg-faint",children:B(c.os)})]},c.peerId)})]})}function se({onOpenSettings:o}){const{peers:i,connect:y,confirmSas:c}=W(),[x,m]=d.useState(null),[p,w]=d.useState("sessions"),[h,k]=d.useState("");if(i.length===0)return e.jsxs("div",{className:"p-6 space-y-3","data-testid":"remote-view",children:[e.jsx(T,{testId:"remote-view-empty",children:"No machines paired yet. Pair a machine in Settings → Phone remote."}),e.jsx(E,{testId:"remote-view-open-settings",onClick:o,children:"Open Settings"})]});const t=i.find(n=>n.peerId===x)??i[0],j=t.status==="authenticated",C=t.status==="failed"&&t.reason===J,I=t.status==="connecting",f=`Remote: ${t.machineName}`,v=n=>{m(n),w("sessions"),k("")},S=n=>{k(n),w("terminal")};return e.jsxs("div",{className:"flex h-full min-h-0","data-testid":"remote-view",children:[e.jsx(Y,{peers:i,selectedId:t.peerId,onSelect:v}),e.jsxs("div",{className:"flex-1 min-w-0 flex flex-col p-4 gap-3 overflow-y-auto",children:[e.jsxs("div",{"data-testid":"remote-view-header",className:"flex items-center gap-2.5",children:[e.jsx(N,{tone:"accent",className:"",title:t.peerId,children:e.jsx("span",{"data-testid":"remote-view-header-badge",children:f})}),e.jsx("span",{className:"text-[12.5px] text-fg-faint",children:B(t.os)})]}),t.status==="pending_sas"&&t.sas&&e.jsx(G,{peer:"other machine",testId:"remote-view-sas",code:t.sas,onConfirm:()=>{c(t.peerId)}}),j?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"flex gap-2 border-b border-line pb-2",children:["sessions","terminal"].map(n=>e.jsxs("button",{type:"button","data-testid":`remote-view-tab-${n}`,onClick:()=>w(n),className:`inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-[13px] font-semibold ${p===n?"bg-bg-hi text-fg border border-line":"text-fg-dim hover:text-fg"}`,children:[n==="sessions"?"Sessions":"Terminal",e.jsx(N,{tone:"accent",children:f})]},n))}),p==="sessions"?e.jsx(V,{peerId:t.peerId,machineName:t.machineName,onOpenTerminal:S},t.peerId):e.jsx(q,{peerId:t.peerId,machineName:t.machineName,cwd:h,onClose:()=>w("sessions")},t.peerId)]}):e.jsxs("div",{className:"space-y-2","data-testid":"remote-view-offline",children:[e.jsx(T,{children:C?"The bilko.run relay does not support desktop clients yet":t.reason===K?`${t.machineName} is offline.`:`Not connected to ${t.machineName}.`}),!C&&t.status!=="pending_sas"&&e.jsx(z,{icon:"terminal",title:t.machineName,subtitle:t.peerId,action:e.jsx(E,{testId:"remote-view-connect",disabled:I,onClick:()=>{y(t.peerId)},children:I?"Connecting…":"Connect"})})]})]})]})}export{se as RemoteMachines};