@staff0rd/assist 0.297.4 → 0.298.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.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.297.4",
9
+ version: "0.298.1",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -18412,6 +18412,44 @@ function daemonLog(message) {
18412
18412
  console.log(`${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${message}`);
18413
18413
  }
18414
18414
 
18415
+ // src/commands/sessions/daemon/loadActiveSelection.ts
18416
+ import { z as z5 } from "zod";
18417
+ var ACTIVE_FILE = "active.json";
18418
+ var activeSelectionSchema = z5.record(z5.string(), z5.string());
18419
+ function loadActiveSelection() {
18420
+ const parsed = activeSelectionSchema.safeParse(
18421
+ loadJson(ACTIVE_FILE)
18422
+ );
18423
+ return parsed.success ? parsed.data : {};
18424
+ }
18425
+ function saveActiveSelection(active) {
18426
+ saveJson(ACTIVE_FILE, active);
18427
+ }
18428
+
18429
+ // src/commands/sessions/daemon/ActiveSelection.ts
18430
+ var ActiveSelection = class {
18431
+ // why: onChange triggers a broadcast only when the selection actually changes
18432
+ constructor(onChange) {
18433
+ this.onChange = onChange;
18434
+ }
18435
+ byRepo = /* @__PURE__ */ new Map();
18436
+ set(cwd, sessionId) {
18437
+ if (!cwd) return;
18438
+ this.byRepo.delete(cwd);
18439
+ this.byRepo.set(cwd, sessionId);
18440
+ saveActiveSelection(this.toJSON());
18441
+ this.onChange();
18442
+ }
18443
+ // why: on daemon restart, reload persisted selections but drop any whose session was not restored
18444
+ restore(isLive) {
18445
+ for (const [cwd, sessionId] of Object.entries(loadActiveSelection()))
18446
+ if (isLive(sessionId)) this.byRepo.set(cwd, sessionId);
18447
+ }
18448
+ toJSON() {
18449
+ return Object.fromEntries(this.byRepo);
18450
+ }
18451
+ };
18452
+
18415
18453
  // src/commands/sessions/daemon/broadcast.ts
18416
18454
  function sendTo(client, msg) {
18417
18455
  client.send(JSON.stringify(msg));
@@ -18424,17 +18462,17 @@ function broadcast(clients, msg) {
18424
18462
  }
18425
18463
 
18426
18464
  // src/commands/sessions/daemon/loadPersistedSessions.ts
18427
- import { z as z5 } from "zod";
18465
+ import { z as z6 } from "zod";
18428
18466
  var SESSIONS_FILE = "sessions.json";
18429
- var persistedSessionSchema = z5.object({
18430
- name: z5.string(),
18431
- commandType: z5.enum(["claude", "run", "assist"]),
18432
- cwd: z5.string(),
18433
- startedAt: z5.number(),
18434
- claudeSessionId: z5.string().optional(),
18435
- runName: z5.string().optional(),
18436
- runArgs: z5.array(z5.string()).optional(),
18437
- assistArgs: z5.array(z5.string()).optional(),
18467
+ var persistedSessionSchema = z6.object({
18468
+ name: z6.string(),
18469
+ commandType: z6.enum(["claude", "run", "assist"]),
18470
+ cwd: z6.string(),
18471
+ startedAt: z6.number(),
18472
+ claudeSessionId: z6.string().optional(),
18473
+ runName: z6.string().optional(),
18474
+ runArgs: z6.array(z6.string()).optional(),
18475
+ assistArgs: z6.array(z6.string()).optional(),
18438
18476
  activity: activitySchema.optional()
18439
18477
  });
18440
18478
  function loadPersistedSessions() {
@@ -18501,11 +18539,13 @@ function toSessionInfo({
18501
18539
  }
18502
18540
 
18503
18541
  // src/commands/sessions/daemon/broadcastSessions.ts
18504
- function broadcastSessions(sessions, clients, windowsSessions = []) {
18542
+ function broadcastSessions(sessions, clients, windowsSessions = [], active) {
18505
18543
  persistLiveSessions(sessions);
18544
+ const local = [...sessions.values()].map(toSessionInfo);
18506
18545
  broadcast(clients, {
18507
18546
  type: "sessions",
18508
- sessions: [...sessions.values()].map(toSessionInfo).concat(windowsSessions)
18547
+ sessions: local.concat(windowsSessions),
18548
+ active: active?.toJSON() ?? {}
18509
18549
  });
18510
18550
  }
18511
18551
 
@@ -18643,8 +18683,7 @@ function replayScrollback(sessions, client) {
18643
18683
  }
18644
18684
 
18645
18685
  // src/commands/sessions/daemon/greetClient.ts
18646
- function greetClient(client, sessions, list4, windowsProxy) {
18647
- sendTo(client, { type: "sessions", sessions: list4() });
18686
+ function greetClient(client, sessions, windowsProxy) {
18648
18687
  replayScrollback(sessions, client);
18649
18688
  windowsProxy.replayScrollback(client);
18650
18689
  void windowsProxy.discover();
@@ -18887,33 +18926,6 @@ function shutdownSessions(sessions) {
18887
18926
  }
18888
18927
  }
18889
18928
 
18890
- // src/commands/sessions/daemon/autoHealWindowsDaemon.ts
18891
- async function autoHealWindowsDaemon(conn, state, heal, version2) {
18892
- daemonLog(`windows proxy: auto-healing windows daemon (mismatch ${version2})`);
18893
- notifyHealing(state);
18894
- try {
18895
- conn.dispose();
18896
- await heal();
18897
- daemonLog("windows proxy: heal complete, reconnecting to windows daemon");
18898
- await conn.ensure();
18899
- } catch (error) {
18900
- const message = error instanceof Error ? error.message : String(error);
18901
- daemonLog(`windows proxy: auto-heal failed: ${message}`);
18902
- state.broadcast({
18903
- type: "error",
18904
- message: `Windows host auto-update failed: ${message}`
18905
- });
18906
- }
18907
- }
18908
- function notifyHealing(state) {
18909
- for (const client of state.pendingCreators)
18910
- sendTo(client, {
18911
- type: "error",
18912
- message: "Windows host is out of date; updating it now \u2014 reselect the repo once the update finishes."
18913
- });
18914
- state.pendingCreators = [];
18915
- }
18916
-
18917
18929
  // src/commands/sessions/daemon/connectToWindowsDaemon.ts
18918
18930
  import * as net2 from "net";
18919
18931
 
@@ -19320,30 +19332,94 @@ var WindowsConnection = class {
19320
19332
  }
19321
19333
  };
19322
19334
 
19335
+ // src/commands/sessions/daemon/autoHealWindowsDaemon.ts
19336
+ async function autoHealWindowsDaemon(conn, state, heal, version2) {
19337
+ daemonLog(`windows proxy: auto-healing windows daemon (mismatch ${version2})`);
19338
+ notifyHealing(state);
19339
+ try {
19340
+ conn.dispose();
19341
+ await heal();
19342
+ daemonLog("windows proxy: heal complete, reconnecting to windows daemon");
19343
+ await conn.ensure();
19344
+ } catch (error) {
19345
+ const message = error instanceof Error ? error.message : String(error);
19346
+ daemonLog(`windows proxy: auto-heal failed: ${message}`);
19347
+ state.broadcast({
19348
+ type: "error",
19349
+ message: `Windows host auto-update failed: ${message}`
19350
+ });
19351
+ }
19352
+ }
19353
+ function notifyHealing(state) {
19354
+ for (const client of state.pendingCreators)
19355
+ sendTo(client, {
19356
+ type: "error",
19357
+ message: "Windows host is out of date; updating it now \u2014 reselect the repo once the update finishes."
19358
+ });
19359
+ state.pendingCreators = [];
19360
+ }
19361
+
19362
+ // src/commands/sessions/daemon/WindowsVersionHealer.ts
19363
+ var UNRECOVERABLE_MESSAGE = "Windows host is on an incompatible version that auto-update could not resolve; not reconnecting. Update the Windows host manually, then restart.";
19364
+ var WindowsVersionHealer = class {
19365
+ constructor(conn, state, heal) {
19366
+ this.conn = conn;
19367
+ this.state = state;
19368
+ this.heal = heal;
19369
+ }
19370
+ healAttempted = false;
19371
+ healing = false;
19372
+ unrecoverable = false;
19373
+ get blocked() {
19374
+ return this.unrecoverable;
19375
+ }
19376
+ refusal() {
19377
+ return { type: "error", message: UNRECOVERABLE_MESSAGE };
19378
+ }
19379
+ async onMismatch(version2) {
19380
+ if (this.healing || this.unrecoverable) return;
19381
+ if (this.healAttempted) return this.giveUp(version2);
19382
+ this.healing = true;
19383
+ this.healAttempted = true;
19384
+ try {
19385
+ await autoHealWindowsDaemon(this.conn, this.state, this.heal, version2);
19386
+ } finally {
19387
+ this.healing = false;
19388
+ }
19389
+ }
19390
+ giveUp(version2) {
19391
+ this.unrecoverable = true;
19392
+ daemonLog(
19393
+ `windows proxy: version mismatch ${version2} persists after heal; not reconnecting until the WSL daemon restarts`
19394
+ );
19395
+ this.conn.dispose();
19396
+ this.state.broadcast(this.refusal());
19397
+ }
19398
+ };
19399
+
19323
19400
  // src/commands/sessions/daemon/WindowsProxy.ts
19324
19401
  var WindowsProxy = class {
19402
+ state;
19403
+ conn;
19404
+ healer;
19325
19405
  constructor(clients, onSessionsChanged, connect3 = defaultConnect, heal = healWindowsDaemon) {
19326
- this.heal = heal;
19327
19406
  this.state = createState(
19328
19407
  (msg) => broadcast(clients, msg),
19329
19408
  onSessionsChanged,
19330
- (version2) => void this.handleVersionMismatch(version2)
19409
+ (version2) => void this.healer.onMismatch(version2)
19331
19410
  );
19332
19411
  this.conn = new WindowsConnection({
19333
19412
  connect: connect3,
19334
19413
  onLine: (line) => handleInbound(this.state, line),
19335
19414
  onClose: () => this.handleClose()
19336
19415
  });
19416
+ this.healer = new WindowsVersionHealer(this.conn, this.state, heal);
19337
19417
  }
19338
- state;
19339
- conn;
19340
- // why: heal runs once per proxy lifetime; if WSL is the older side, updating Windows can't close the gap, so a repeat mismatch must not loop
19341
- healAttempted = false;
19342
- healing = false;
19343
19418
  sessions() {
19344
19419
  return this.state.windowsSessions;
19345
19420
  }
19346
19421
  discover() {
19422
+ if (this.healer.blocked) return Promise.resolve();
19347
19423
  return discoverWindowsSessions(this.conn);
19348
19424
  }
19349
19425
  replayScrollback(client) {
@@ -19353,7 +19429,8 @@ var WindowsProxy = class {
19353
19429
  // windows-origin cwd is forwarded, as is I/O for a namespaced session id.
19354
19430
  route(client, data) {
19355
19431
  if (isWindowsCreate(data)) {
19356
- void forwardWindowsCreate(this.conn, this.state, client, data);
19432
+ if (this.healer.blocked) sendTo(client, this.healer.refusal());
19433
+ else void forwardWindowsCreate(this.conn, this.state, client, data);
19357
19434
  return true;
19358
19435
  }
19359
19436
  if (isWindowsIo(data)) {
@@ -19366,16 +19443,6 @@ var WindowsProxy = class {
19366
19443
  dispose() {
19367
19444
  this.conn.dispose();
19368
19445
  }
19369
- async handleVersionMismatch(version2) {
19370
- if (this.healing || this.healAttempted) return;
19371
- this.healing = true;
19372
- this.healAttempted = true;
19373
- try {
19374
- await autoHealWindowsDaemon(this.conn, this.state, this.heal, version2);
19375
- } finally {
19376
- this.healing = false;
19377
- }
19378
- }
19379
19446
  handleClose() {
19380
19447
  daemonLog("windows proxy: connection to windows daemon closed");
19381
19448
  for (const client of this.state.pendingCreators)
@@ -19688,6 +19755,8 @@ var SessionManager = class {
19688
19755
  this.onIdleChange = onIdleChange;
19689
19756
  }
19690
19757
  sessions = /* @__PURE__ */ new Map();
19758
+ // why: dispatch calls active.set() on card click; broadcasts include active.toJSON()
19759
+ active = new ActiveSelection(() => this.notify());
19691
19760
  clients = new ClientHub();
19692
19761
  nextId = 1;
19693
19762
  shuttingDown = false;
@@ -19695,16 +19764,14 @@ var SessionManager = class {
19695
19764
  windowsProxy = new WindowsProxy(this.clients, () => this.notify());
19696
19765
  addClient(client) {
19697
19766
  this.clients.add(client);
19698
- this.onIdleChange?.(this.isIdle());
19699
- greetClient(client, this.sessions, this.listSessions, this.windowsProxy);
19767
+ this.notify();
19768
+ greetClient(client, this.sessions, this.windowsProxy);
19700
19769
  }
19701
19770
  removeClient(client) {
19702
19771
  this.clients.delete(client);
19703
19772
  this.onIdleChange?.(this.isIdle());
19704
19773
  }
19705
- isIdle() {
19706
- return this.sessions.size === 0 && this.clients.size === 0;
19707
- }
19774
+ isIdle = () => this.sessions.size === 0 && this.clients.size === 0;
19708
19775
  shutdown() {
19709
19776
  this.shuttingDown = true;
19710
19777
  shutdownSessions(this.sessions);
@@ -19772,7 +19839,7 @@ var SessionManager = class {
19772
19839
  notify = () => {
19773
19840
  if (this.shuttingDown) return;
19774
19841
  const windows = this.windowsProxy.sessions();
19775
- broadcastSessions(this.sessions, this.clients, windows);
19842
+ broadcastSessions(this.sessions, this.clients, windows, this.active);
19776
19843
  this.onIdleChange?.(this.isIdle());
19777
19844
  };
19778
19845
  };
@@ -19950,7 +20017,8 @@ var handlers = {
19950
20017
  ),
19951
20018
  "set-autoadvance": routed(
19952
20019
  (_client, m, d) => m.setAutoAdvance(d.sessionId, d.enabled)
19953
- )
20020
+ ),
20021
+ "set-active": (_client, m, d) => m.active.set(d.cwd, d.sessionId)
19954
20022
  };
19955
20023
  function dispatchMessage(client, manager, data) {
19956
20024
  handlers[data.type]?.(client, manager, data);
@@ -20021,6 +20089,8 @@ function onListening(manager, checkAutoExit) {
20021
20089
  });
20022
20090
  process.on("exit", cleanupOwnedFiles);
20023
20091
  const restored = manager.restore();
20092
+ const liveIds = new Set(manager.listSessions().map((s) => s.id));
20093
+ manager.active.restore((id2) => liveIds.has(id2));
20024
20094
  daemonLog(
20025
20095
  restored.length > 0 ? `restored ${restored.length} session(s): ${restored.join(", ")}` : "no persisted sessions to restore"
20026
20096
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.297.4",
3
+ "version": "0.298.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {