aimux-cli 0.1.1 → 0.1.3
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/daemon.js +35 -3
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-feedback.d.ts +38 -0
- package/dist/dashboard-feedback.js +113 -0
- package/dist/dashboard-feedback.js.map +1 -0
- package/dist/dashboard-pending-actions.d.ts +12 -0
- package/dist/dashboard-pending-actions.js +52 -0
- package/dist/dashboard-pending-actions.js.map +1 -0
- package/dist/dashboard-session-actions.d.ts +29 -0
- package/dist/dashboard-session-actions.js +82 -0
- package/dist/dashboard-session-actions.js.map +1 -0
- package/dist/dashboard-targets.d.ts +18 -0
- package/dist/dashboard-targets.js +236 -0
- package/dist/dashboard-targets.js.map +1 -0
- package/dist/dashboard-ui-state-store.d.ts +12 -0
- package/dist/dashboard-ui-state-store.js +106 -0
- package/dist/dashboard-ui-state-store.js.map +1 -0
- package/dist/dashboard.d.ts +3 -1
- package/dist/dashboard.js +4 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/main.js +41 -207
- package/dist/main.js.map +1 -1
- package/dist/metadata-server.js +6 -77
- package/dist/metadata-server.js.map +1 -1
- package/dist/multiplexer-runtime-sync.d.ts +26 -0
- package/dist/multiplexer-runtime-sync.js +58 -0
- package/dist/multiplexer-runtime-sync.js.map +1 -0
- package/dist/multiplexer.d.ts +25 -10
- package/dist/multiplexer.js +353 -274
- package/dist/multiplexer.js.map +1 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +3 -0
- package/dist/paths.js.map +1 -1
- package/dist/statusline-model.js +1 -0
- package/dist/statusline-model.js.map +1 -1
- package/dist/tmux-runtime-manager.d.ts +0 -8
- package/dist/tmux-runtime-manager.js +34 -36
- package/dist/tmux-runtime-manager.js.map +1 -1
- package/dist/tmux-switcher.d.ts +11 -0
- package/dist/tmux-switcher.js +115 -0
- package/dist/tmux-switcher.js.map +1 -0
- package/dist/tmux-window-open.d.ts +9 -0
- package/dist/tmux-window-open.js +71 -0
- package/dist/tmux-window-open.js.map +1 -0
- package/dist/tool-output-watchers.d.ts +14 -1
- package/dist/tool-output-watchers.js +18 -7
- package/dist/tool-output-watchers.js.map +1 -1
- package/dist/tui/screens/dashboard-renderers.js +2 -1
- package/dist/tui/screens/dashboard-renderers.js.map +1 -1
- package/package.json +1 -1
package/dist/multiplexer.js
CHANGED
|
@@ -25,7 +25,7 @@ import { TmuxRuntimeManager } from "./tmux-runtime-manager.js";
|
|
|
25
25
|
import { isDashboardWindowName } from "./tmux-runtime-manager.js";
|
|
26
26
|
import { TmuxSessionTransport } from "./tmux-session-transport.js";
|
|
27
27
|
import { MetadataServer } from "./metadata-server.js";
|
|
28
|
-
import { loadMetadataState, removeMetadataEndpoint, resolveProjectServiceEndpoint } from "./metadata-store.js";
|
|
28
|
+
import { loadMetadataState, removeMetadataEndpoint, resolveProjectServiceEndpoint, updateSessionMetadata, } from "./metadata-store.js";
|
|
29
29
|
import { PluginRuntime } from "./plugin-runtime.js";
|
|
30
30
|
import { SessionBootstrapService } from "./session-bootstrap.js";
|
|
31
31
|
import { ensureDaemonRunning, ensureProjectService, loadDaemonInfo } from "./daemon.js";
|
|
@@ -43,6 +43,7 @@ import { deriveSessionSemantics } from "./session-semantics.js";
|
|
|
43
43
|
import { injectClaudeHookArgs } from "./claude-hooks.js";
|
|
44
44
|
import { navigationUrgencyScore } from "./fast-control.js";
|
|
45
45
|
import { requestJson } from "./http-client.js";
|
|
46
|
+
import { openDashboardTarget } from "./dashboard-targets.js";
|
|
46
47
|
import { clearNotifications, listNotifications, markNotificationsRead, } from "./notifications.js";
|
|
47
48
|
import { updateNotificationContext } from "./notification-context.js";
|
|
48
49
|
import { buildThreadEntries, buildWorkflowEntries, describeWorkflowNextAction, filterWorkflowEntries, } from "./workflow.js";
|
|
@@ -50,7 +51,14 @@ import { renderActivityScreen, renderGraveyardDetails, renderGraveyardScreen, re
|
|
|
50
51
|
import { renderDashboardBusyOverlay, renderDashboardErrorOverlay, renderHelpOverlay, renderLabelInputOverlay, renderMigratePickerOverlay, renderNotificationPanel, renderServiceInputOverlay, renderSwitcherOverlay, renderWorktreeListOverlay, renderWorktreeRemoveConfirmOverlay, } from "./tui/screens/overlay-renderers.js";
|
|
51
52
|
import { composeTwoPane, stripAnsi, truncateAnsi, truncatePlain, wrapKeyValue, wrapText } from "./tui/render/text.js";
|
|
52
53
|
import { loadStatusline, renderTmuxStatuslineFromData } from "./tmux-statusline.js";
|
|
54
|
+
import { DashboardUiStateStore } from "./dashboard-ui-state-store.js";
|
|
55
|
+
import { DashboardPendingActions } from "./dashboard-pending-actions.js";
|
|
56
|
+
import { DashboardFeedbackController, } from "./dashboard-feedback.js";
|
|
57
|
+
import { MultiplexerRuntimeSync } from "./multiplexer-runtime-sync.js";
|
|
58
|
+
import { openManagedServiceWindow, openManagedSessionWindow, selectLinkedOrOpenTarget } from "./tmux-window-open.js";
|
|
59
|
+
import { graveyardSessionWithFeedback as runGraveyardSessionWithFeedback, resumeOfflineSessionWithFeedback as runResumeOfflineSessionWithFeedback, stopSessionToOfflineWithFeedback as runStopSessionToOfflineWithFeedback, waitForSessionExit, waitForSessionStart, } from "./dashboard-session-actions.js";
|
|
53
60
|
export class Multiplexer {
|
|
61
|
+
projectRoot;
|
|
54
62
|
sessions = [];
|
|
55
63
|
activeIndex = 0;
|
|
56
64
|
mode = "dashboard";
|
|
@@ -83,9 +91,10 @@ export class Multiplexer {
|
|
|
83
91
|
worktreeListActive = false;
|
|
84
92
|
worktreeRemoveConfirm = null;
|
|
85
93
|
worktreeRemovalJob = null;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
dashboardFeedback = new DashboardFeedbackController({
|
|
95
|
+
renderDashboard: () => this.renderDashboard(),
|
|
96
|
+
isDashboardMode: () => this.mode === "dashboard",
|
|
97
|
+
});
|
|
89
98
|
migratePickerActive = false;
|
|
90
99
|
migratePickerWorktrees = [];
|
|
91
100
|
graveyardEntries = [];
|
|
@@ -102,7 +111,11 @@ export class Multiplexer {
|
|
|
102
111
|
planEntries = [];
|
|
103
112
|
planIndex = 0;
|
|
104
113
|
notificationPanelState = null;
|
|
105
|
-
|
|
114
|
+
dashboardPendingActions = new DashboardPendingActions(() => {
|
|
115
|
+
if (this.mode === "dashboard") {
|
|
116
|
+
this.renderCurrentDashboardView();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
106
119
|
stoppingSessionIds = new Set();
|
|
107
120
|
graveyardAfterStopSessionIds = new Set();
|
|
108
121
|
/** Quick switcher overlay state */
|
|
@@ -115,8 +128,8 @@ export class Multiplexer {
|
|
|
115
128
|
confirmedRegistered = new Set();
|
|
116
129
|
/** The focused worktree path on the dashboard (undefined = main repo) */
|
|
117
130
|
dashboardState = new DashboardState();
|
|
131
|
+
dashboardUiStateStore = new DashboardUiStateStore();
|
|
118
132
|
statusInterval = null;
|
|
119
|
-
heartbeatInterval = null;
|
|
120
133
|
agentTracker = new AgentTracker();
|
|
121
134
|
instanceId = randomUUID();
|
|
122
135
|
contextWatcher = new ContextWatcher((target) => this.tmuxRuntimeManager.captureTarget(target, { startLine: -120 }));
|
|
@@ -148,7 +161,6 @@ export class Multiplexer {
|
|
|
148
161
|
metadataServer = null;
|
|
149
162
|
eventBus = new ProjectEventBus();
|
|
150
163
|
pluginRuntime = null;
|
|
151
|
-
projectServiceInterval = null;
|
|
152
164
|
lastRenderedFrame = null;
|
|
153
165
|
lastStatuslineSnapshotKey = null;
|
|
154
166
|
desktopStateSnapshot = null;
|
|
@@ -159,10 +171,37 @@ export class Multiplexer {
|
|
|
159
171
|
dashboardModelRefreshedAt = 0;
|
|
160
172
|
dashboardServiceSnapshotRefreshing = false;
|
|
161
173
|
dashboardServiceRecovery = null;
|
|
174
|
+
dashboardNextBackgroundRefreshAt = 0;
|
|
175
|
+
runtimeSync;
|
|
162
176
|
constructor() {
|
|
177
|
+
this.projectRoot = (() => {
|
|
178
|
+
try {
|
|
179
|
+
return findMainRepo(process.cwd());
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return process.cwd();
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
163
185
|
this.terminalHost = new TerminalHost();
|
|
164
186
|
this.hotkeys = new HotkeyHandler((action) => this.handleAction(action));
|
|
165
187
|
this.dashboard = new Dashboard();
|
|
188
|
+
this.runtimeSync = new MultiplexerRuntimeSync({
|
|
189
|
+
instanceDirectory: this.instanceDirectory,
|
|
190
|
+
instanceId: this.instanceId,
|
|
191
|
+
cwd: process.cwd(),
|
|
192
|
+
getMode: () => this.mode,
|
|
193
|
+
getConfirmedRegistered: () => this.confirmedRegistered,
|
|
194
|
+
setConfirmedRegistered: (value) => {
|
|
195
|
+
this.confirmedRegistered = value;
|
|
196
|
+
},
|
|
197
|
+
getInstanceSessionRefs: () => this.getInstanceSessionRefs(),
|
|
198
|
+
syncSessionsFromState: () => this.syncSessionsFromState(),
|
|
199
|
+
loadOfflineSessions: () => this.loadOfflineSessions(),
|
|
200
|
+
renderCurrentDashboardView: () => this.renderCurrentDashboardView(),
|
|
201
|
+
renderDashboard: () => this.renderDashboard(),
|
|
202
|
+
handleSessionClaimed: (sessionId) => this.handleSessionClaimed(sessionId),
|
|
203
|
+
writeStatuslineFile: () => this.writeStatuslineFile(),
|
|
204
|
+
});
|
|
166
205
|
this.eventBus.subscribe((event) => {
|
|
167
206
|
if (event.type !== "alert")
|
|
168
207
|
return;
|
|
@@ -219,10 +258,7 @@ export class Multiplexer {
|
|
|
219
258
|
return true;
|
|
220
259
|
}
|
|
221
260
|
openTmuxDashboardTarget() {
|
|
222
|
-
|
|
223
|
-
const openSession = this.tmuxRuntimeManager.getOpenSessionName(session.sessionName);
|
|
224
|
-
const target = this.tmuxRuntimeManager.ensureDashboardWindow(openSession, process.cwd());
|
|
225
|
-
this.tmuxRuntimeManager.openTarget(target, { insideTmux: this.tmuxRuntimeManager.isInsideTmux() });
|
|
261
|
+
openDashboardTarget(this.projectRoot, this.tmuxRuntimeManager);
|
|
226
262
|
}
|
|
227
263
|
invalidateDashboardFrame() {
|
|
228
264
|
this.lastRenderedFrame = null;
|
|
@@ -232,14 +268,20 @@ export class Multiplexer {
|
|
|
232
268
|
}
|
|
233
269
|
handleDashboardFocusIn() {
|
|
234
270
|
this.terminalHost.enterAlternateScreen();
|
|
235
|
-
this.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
271
|
+
if (this.lastRenderedFrame) {
|
|
272
|
+
process.stdout.write(this.lastRenderedFrame);
|
|
273
|
+
}
|
|
274
|
+
this.tmuxRuntimeManager.refreshStatus();
|
|
275
|
+
}
|
|
276
|
+
loadDashboardUiState() {
|
|
277
|
+
this.dashboardUiStateStore.loadInto(this.dashboardState);
|
|
278
|
+
}
|
|
279
|
+
persistDashboardUiState() {
|
|
280
|
+
this.dashboardUiStateStore.persist(this.mode, this.dashboardState, this.activeIndex, this.getDashboardSessions());
|
|
281
|
+
}
|
|
282
|
+
restoreDashboardSelectionFromPreference(dashSessions, hasWorktrees) {
|
|
283
|
+
this.dashboardUiStateStore.consumeSelectionRestore(this.dashboardState, dashSessions, hasWorktrees, () => this.updateWorktreeSessions(), this.activeIndex, (value) => {
|
|
284
|
+
this.activeIndex = value;
|
|
243
285
|
});
|
|
244
286
|
}
|
|
245
287
|
writeFrame(output, force = false) {
|
|
@@ -314,25 +356,12 @@ export class Multiplexer {
|
|
|
314
356
|
});
|
|
315
357
|
}
|
|
316
358
|
applyDashboardModel(dashSessions, dashServices, worktreeGroups, mainCheckoutInfo) {
|
|
317
|
-
this.dashboardSessionsCache = this.
|
|
318
|
-
this.dashboardServicesCache = dashServices;
|
|
359
|
+
this.dashboardSessionsCache = this.dashboardPendingActions.applyToSessions(dashSessions);
|
|
360
|
+
this.dashboardServicesCache = this.dashboardPendingActions.applyToServices(dashServices);
|
|
319
361
|
this.dashboardWorktreeGroupsCache = worktreeGroups;
|
|
320
362
|
this.dashboardMainCheckoutInfoCache = mainCheckoutInfo;
|
|
321
363
|
this.dashboardModelRefreshedAt = Date.now();
|
|
322
|
-
|
|
323
|
-
applyPendingDashboardSessionStates(sessions) {
|
|
324
|
-
if (this.pendingDashboardSessionActions.size === 0)
|
|
325
|
-
return sessions;
|
|
326
|
-
return sessions.map((session) => {
|
|
327
|
-
const pendingAction = this.pendingDashboardSessionActions.get(session.id);
|
|
328
|
-
if (!pendingAction)
|
|
329
|
-
return session;
|
|
330
|
-
return {
|
|
331
|
-
...session,
|
|
332
|
-
pendingAction,
|
|
333
|
-
optimistic: true,
|
|
334
|
-
};
|
|
335
|
-
});
|
|
364
|
+
this.dashboardUiStateStore.markSelectionDirty();
|
|
336
365
|
}
|
|
337
366
|
invalidateDesktopStateSnapshot() {
|
|
338
367
|
this.desktopStateSnapshot = null;
|
|
@@ -805,11 +834,43 @@ export class Multiplexer {
|
|
|
805
834
|
delete offline.label;
|
|
806
835
|
}
|
|
807
836
|
}
|
|
837
|
+
applyDashboardSessionLabel(sessionId, label) {
|
|
838
|
+
const trimmed = label?.trim();
|
|
839
|
+
this.dashboardSessionsCache = this.dashboardSessionsCache.map((session) => session.id === sessionId ? { ...session, label: trimmed || undefined } : session);
|
|
840
|
+
this.dashboardWorktreeGroupsCache = this.dashboardWorktreeGroupsCache.map((group) => ({
|
|
841
|
+
...group,
|
|
842
|
+
sessions: group.sessions.map((session) => session.id === sessionId ? { ...session, label: trimmed || undefined } : session),
|
|
843
|
+
}));
|
|
844
|
+
this.dashboardState.worktreeSessions = this.dashboardState.worktreeSessions.map((session) => session.id === sessionId ? { ...session, label: trimmed || undefined } : session);
|
|
845
|
+
}
|
|
808
846
|
async updateSessionLabel(sessionId, label) {
|
|
847
|
+
if (this.mode === "dashboard") {
|
|
848
|
+
this.applySessionLabel(sessionId, label);
|
|
849
|
+
this.applyDashboardSessionLabel(sessionId, label);
|
|
850
|
+
this.setPendingDashboardSessionAction(sessionId, "renaming");
|
|
851
|
+
this.writeStatuslineFile();
|
|
852
|
+
this.renderCurrentDashboardView();
|
|
853
|
+
void this.postToProjectService("/agents/rename", { sessionId, label })
|
|
854
|
+
.then(() => {
|
|
855
|
+
this.invalidateDesktopStateSnapshot();
|
|
856
|
+
this.setPendingDashboardSessionAction(sessionId, null);
|
|
857
|
+
this.writeStatuslineFile();
|
|
858
|
+
this.renderCurrentDashboardView();
|
|
859
|
+
})
|
|
860
|
+
.catch((err) => {
|
|
861
|
+
this.setPendingDashboardSessionAction(sessionId, null);
|
|
862
|
+
this.footerFlash = `Rename failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
863
|
+
this.footerFlashTicks = 4;
|
|
864
|
+
this.writeStatuslineFile();
|
|
865
|
+
this.renderCurrentDashboardView();
|
|
866
|
+
});
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
809
869
|
this.applySessionLabel(sessionId, label);
|
|
870
|
+
this.invalidateDesktopStateSnapshot();
|
|
810
871
|
const localSession = this.sessions.find((session) => session.id === sessionId)?.transport;
|
|
811
872
|
if (localSession instanceof TmuxSessionTransport) {
|
|
812
|
-
localSession.renameWindow(
|
|
873
|
+
localSession.renameWindow(localSession.command);
|
|
813
874
|
const target = localSession.tmuxTarget;
|
|
814
875
|
this.sessionTmuxTargets.set(sessionId, target);
|
|
815
876
|
this.syncTmuxWindowMetadata(sessionId);
|
|
@@ -1221,6 +1282,7 @@ export class Multiplexer {
|
|
|
1221
1282
|
process.stdout.on("resize", this.onResize);
|
|
1222
1283
|
// Enter dashboard mode directly
|
|
1223
1284
|
this.mode = "dashboard";
|
|
1285
|
+
this.loadDashboardUiState();
|
|
1224
1286
|
const primed = await this.refreshDashboardModelFromService(true);
|
|
1225
1287
|
if (!primed) {
|
|
1226
1288
|
throw new Error("dashboard requires a live project service desktop-state endpoint");
|
|
@@ -1408,17 +1470,12 @@ export class Multiplexer {
|
|
|
1408
1470
|
if (session instanceof TmuxSessionTransport) {
|
|
1409
1471
|
this.syncTmuxWindowMetadata(sessionId);
|
|
1410
1472
|
}
|
|
1411
|
-
// Focus the new session
|
|
1412
1473
|
this.activeIndex = this.sessions.length - 1;
|
|
1413
1474
|
if (this.startedInDashboard && this.mode === "dashboard") {
|
|
1414
1475
|
this.invalidateDesktopStateSnapshot();
|
|
1415
1476
|
this.refreshLocalDashboardModel();
|
|
1416
1477
|
this.updateWorktreeSessions();
|
|
1417
|
-
this.
|
|
1418
|
-
const selectedIndex = this.dashboardState.worktreeEntries.findIndex((entry) => entry.kind === "session" && entry.id === sessionId);
|
|
1419
|
-
if (selectedIndex >= 0) {
|
|
1420
|
-
this.dashboardState.sessionIndex = selectedIndex;
|
|
1421
|
-
}
|
|
1478
|
+
this.preferDashboardEntrySelection("session", sessionId, worktreePath);
|
|
1422
1479
|
this.renderDashboard();
|
|
1423
1480
|
}
|
|
1424
1481
|
this.saveState();
|
|
@@ -1507,18 +1564,7 @@ export class Multiplexer {
|
|
|
1507
1564
|
const target = this.sessionTmuxTargets.get(sid);
|
|
1508
1565
|
if (target) {
|
|
1509
1566
|
this.saveState();
|
|
1510
|
-
|
|
1511
|
-
if (insideTmux) {
|
|
1512
|
-
const currentClientSession = this.tmuxRuntimeManager.currentClientSession();
|
|
1513
|
-
if (currentClientSession) {
|
|
1514
|
-
const linkedTarget = this.tmuxRuntimeManager.getTargetByWindowId(currentClientSession, target.windowId);
|
|
1515
|
-
if (linkedTarget) {
|
|
1516
|
-
this.tmuxRuntimeManager.selectWindow(linkedTarget);
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
this.tmuxRuntimeManager.openTarget(target, { insideTmux });
|
|
1567
|
+
selectLinkedOrOpenTarget(this.tmuxRuntimeManager, target);
|
|
1522
1568
|
}
|
|
1523
1569
|
}
|
|
1524
1570
|
handleAction(action) {
|
|
@@ -1717,13 +1763,18 @@ export class Multiplexer {
|
|
|
1717
1763
|
: undefined;
|
|
1718
1764
|
if (!selEntry)
|
|
1719
1765
|
return;
|
|
1720
|
-
|
|
1766
|
+
const runtime = this.sessions.find((s) => s.id === selEntry.id);
|
|
1767
|
+
const effectivelyOffline = selEntry.status === "offline" ||
|
|
1768
|
+
selEntry.pendingAction === "stopping" ||
|
|
1769
|
+
!runtime ||
|
|
1770
|
+
!this.isSessionRuntimeLive(runtime);
|
|
1771
|
+
if (effectivelyOffline) {
|
|
1721
1772
|
// Second [x] on offline → move to graveyard
|
|
1722
1773
|
void this.graveyardSessionWithFeedback(selEntry.id, hasWorktrees);
|
|
1723
1774
|
return;
|
|
1724
1775
|
}
|
|
1725
1776
|
// First [x] on running → stop PTY, keep as offline for resume
|
|
1726
|
-
const pty =
|
|
1777
|
+
const pty = runtime;
|
|
1727
1778
|
if (pty) {
|
|
1728
1779
|
void this.stopSessionToOfflineWithFeedback(pty);
|
|
1729
1780
|
}
|
|
@@ -1787,7 +1838,7 @@ export class Multiplexer {
|
|
|
1787
1838
|
}
|
|
1788
1839
|
if (entry?.status === "offline") {
|
|
1789
1840
|
const offline = this.offlineSessions.find((s) => s.id === entry.id);
|
|
1790
|
-
if (offline) {
|
|
1841
|
+
if (offline && entry.pendingAction !== "starting") {
|
|
1791
1842
|
void this.resumeOfflineSessionWithFeedback(offline);
|
|
1792
1843
|
}
|
|
1793
1844
|
return;
|
|
@@ -1887,7 +1938,7 @@ export class Multiplexer {
|
|
|
1887
1938
|
}
|
|
1888
1939
|
if (dashEntry.status === "offline") {
|
|
1889
1940
|
const offline = this.offlineSessions.find((s) => s.id === dashEntry.id);
|
|
1890
|
-
if (offline) {
|
|
1941
|
+
if (offline && dashEntry.pendingAction !== "starting") {
|
|
1891
1942
|
void this.resumeOfflineSessionWithFeedback(offline);
|
|
1892
1943
|
}
|
|
1893
1944
|
return;
|
|
@@ -1917,6 +1968,9 @@ export class Multiplexer {
|
|
|
1917
1968
|
async activateDashboardEntry(entry) {
|
|
1918
1969
|
if (!entry)
|
|
1919
1970
|
return;
|
|
1971
|
+
if (entry.pendingAction === "creating") {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1920
1974
|
if (this.openLiveTmuxWindowForEntry(entry)) {
|
|
1921
1975
|
return;
|
|
1922
1976
|
}
|
|
@@ -1926,7 +1980,7 @@ export class Multiplexer {
|
|
|
1926
1980
|
}
|
|
1927
1981
|
if (entry.status === "offline") {
|
|
1928
1982
|
const offline = this.offlineSessions.find((session) => session.id === entry.id);
|
|
1929
|
-
if (offline) {
|
|
1983
|
+
if (offline && entry.pendingAction !== "starting") {
|
|
1930
1984
|
await this.resumeOfflineSessionWithFeedback(offline);
|
|
1931
1985
|
}
|
|
1932
1986
|
return;
|
|
@@ -2671,6 +2725,7 @@ export class Multiplexer {
|
|
|
2671
2725
|
this.dashboardState.setScreen(screen);
|
|
2672
2726
|
this.syncTuiNotificationContext(false);
|
|
2673
2727
|
this.writeDashboardClientStatuslineFile();
|
|
2728
|
+
this.persistDashboardUiState();
|
|
2674
2729
|
this.tmuxRuntimeManager.refreshStatus();
|
|
2675
2730
|
}
|
|
2676
2731
|
handleActiveDashboardOverlayKey(data) {
|
|
@@ -2855,27 +2910,18 @@ export class Multiplexer {
|
|
|
2855
2910
|
return false;
|
|
2856
2911
|
}
|
|
2857
2912
|
openLiveTmuxWindowForEntry(entry) {
|
|
2858
|
-
const
|
|
2859
|
-
|
|
2860
|
-
sessionId: entry.id,
|
|
2861
|
-
backendSessionId: entry.backendSessionId,
|
|
2862
|
-
});
|
|
2863
|
-
if (!match)
|
|
2913
|
+
const target = openManagedSessionWindow(this.tmuxRuntimeManager, process.cwd(), entry);
|
|
2914
|
+
if (!target)
|
|
2864
2915
|
return false;
|
|
2865
2916
|
this.agentTracker.markSeen(entry.id);
|
|
2866
2917
|
this.noteLastUsedItem(entry.id);
|
|
2867
|
-
this.tmuxRuntimeManager.openTarget(match.target, { insideTmux: this.tmuxRuntimeManager.isInsideTmux() });
|
|
2868
2918
|
return true;
|
|
2869
2919
|
}
|
|
2870
2920
|
openLiveTmuxWindowForService(serviceId) {
|
|
2871
|
-
const
|
|
2872
|
-
|
|
2873
|
-
sessionId: serviceId,
|
|
2874
|
-
});
|
|
2875
|
-
if (!match || match.metadata.kind !== "service")
|
|
2921
|
+
const target = openManagedServiceWindow(this.tmuxRuntimeManager, process.cwd(), serviceId);
|
|
2922
|
+
if (!target)
|
|
2876
2923
|
return false;
|
|
2877
2924
|
this.noteLastUsedItem(serviceId);
|
|
2878
|
-
this.tmuxRuntimeManager.openTarget(match.target, { insideTmux: this.tmuxRuntimeManager.isInsideTmux() });
|
|
2879
2925
|
return true;
|
|
2880
2926
|
}
|
|
2881
2927
|
noteLastUsedItem(itemId) {
|
|
@@ -3367,7 +3413,21 @@ export class Multiplexer {
|
|
|
3367
3413
|
}
|
|
3368
3414
|
this.pickerMode = "create";
|
|
3369
3415
|
this.forkSourceSessionId = null;
|
|
3370
|
-
this.
|
|
3416
|
+
const sessionId = this.generateDashboardSessionId(tool.command);
|
|
3417
|
+
const shouldRenderPending = this.startedInDashboard && this.mode === "dashboard";
|
|
3418
|
+
if (shouldRenderPending) {
|
|
3419
|
+
this.setPendingDashboardSessionAction(sessionId, "creating");
|
|
3420
|
+
}
|
|
3421
|
+
try {
|
|
3422
|
+
this.createSession(tool.command, tool.args, tool.preambleFlag, toolKey, undefined, tool.sessionIdFlag, wtPath, undefined, sessionId, shouldRenderPending);
|
|
3423
|
+
this.settleDashboardCreatePending(sessionId);
|
|
3424
|
+
}
|
|
3425
|
+
catch (error) {
|
|
3426
|
+
if (shouldRenderPending) {
|
|
3427
|
+
this.setPendingDashboardSessionAction(sessionId, null);
|
|
3428
|
+
}
|
|
3429
|
+
throw error;
|
|
3430
|
+
}
|
|
3371
3431
|
}
|
|
3372
3432
|
showToolPicker(sourceSessionId) {
|
|
3373
3433
|
const config = loadConfig();
|
|
@@ -3530,7 +3590,9 @@ export class Multiplexer {
|
|
|
3530
3590
|
throw new Error(`Unable to fork session ${opts.sourceSessionId}`);
|
|
3531
3591
|
}
|
|
3532
3592
|
if (opts.open !== false && result.target) {
|
|
3533
|
-
this.
|
|
3593
|
+
if (!this.openLiveTmuxWindowForEntry({ id: result.sessionId })) {
|
|
3594
|
+
this.tmuxRuntimeManager.openTarget(result.target, { insideTmux: this.tmuxRuntimeManager.isInsideTmux() });
|
|
3595
|
+
}
|
|
3534
3596
|
}
|
|
3535
3597
|
return {
|
|
3536
3598
|
sessionId: result.sessionId,
|
|
@@ -3554,7 +3616,9 @@ export class Multiplexer {
|
|
|
3554
3616
|
const transport = this.createSession(toolCfg.command, toolCfg.args, toolCfg.preambleFlag, opts.toolConfigKey, undefined, toolCfg.sessionIdFlag, targetWorktreePath);
|
|
3555
3617
|
const target = this.sessionTmuxTargets.get(transport.id);
|
|
3556
3618
|
if (opts.open !== false && target) {
|
|
3557
|
-
this.
|
|
3619
|
+
if (!this.openLiveTmuxWindowForEntry({ id: transport.id })) {
|
|
3620
|
+
this.tmuxRuntimeManager.openTarget(target, { insideTmux: this.tmuxRuntimeManager.isInsideTmux() });
|
|
3621
|
+
}
|
|
3558
3622
|
}
|
|
3559
3623
|
return { sessionId: transport.id };
|
|
3560
3624
|
}
|
|
@@ -3581,7 +3645,7 @@ export class Multiplexer {
|
|
|
3581
3645
|
if (!this.stoppingSessionIds.has(sessionId)) {
|
|
3582
3646
|
this.stopSessionToOffline(runningSession);
|
|
3583
3647
|
}
|
|
3584
|
-
await
|
|
3648
|
+
await waitForSessionExit(runningSession);
|
|
3585
3649
|
this.saveState();
|
|
3586
3650
|
return { sessionId, status: "offline" };
|
|
3587
3651
|
}
|
|
@@ -3595,7 +3659,7 @@ export class Multiplexer {
|
|
|
3595
3659
|
if (!this.stoppingSessionIds.has(sessionId)) {
|
|
3596
3660
|
this.stopSessionToOffline(runningSession);
|
|
3597
3661
|
}
|
|
3598
|
-
await
|
|
3662
|
+
await waitForSessionExit(runningSession);
|
|
3599
3663
|
this.saveState();
|
|
3600
3664
|
}
|
|
3601
3665
|
else {
|
|
@@ -3620,7 +3684,7 @@ export class Multiplexer {
|
|
|
3620
3684
|
throw new Error(`Session "${sessionId}" not found`);
|
|
3621
3685
|
}
|
|
3622
3686
|
await this.migrateAgent(sessionId, targetWorktreePath);
|
|
3623
|
-
await
|
|
3687
|
+
await waitForSessionExit(runningSession);
|
|
3624
3688
|
return { sessionId, worktreePath: this.getSessionWorktreePath(sessionId) };
|
|
3625
3689
|
}
|
|
3626
3690
|
serviceLabelForCommand(commandLine) {
|
|
@@ -3630,6 +3694,22 @@ export class Multiplexer {
|
|
|
3630
3694
|
const first = trimmed.split(/\s+/)[0] ?? "service";
|
|
3631
3695
|
return basename(first);
|
|
3632
3696
|
}
|
|
3697
|
+
generateDashboardSessionId(command) {
|
|
3698
|
+
return `${command}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3699
|
+
}
|
|
3700
|
+
settleDashboardCreatePending(itemId) {
|
|
3701
|
+
if (!(this.startedInDashboard && this.mode === "dashboard"))
|
|
3702
|
+
return;
|
|
3703
|
+
this.dashboardPendingActions.settleCreatePending(itemId, () => {
|
|
3704
|
+
this.refreshLocalDashboardModel();
|
|
3705
|
+
this.renderDashboard();
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
preferDashboardEntrySelection(kind, id, worktreePath) {
|
|
3709
|
+
if (!(this.startedInDashboard && this.mode === "dashboard"))
|
|
3710
|
+
return;
|
|
3711
|
+
this.dashboardUiStateStore.preferEntrySelection(this.dashboardState, kind, id, worktreePath);
|
|
3712
|
+
}
|
|
3633
3713
|
createService(commandLine, worktreePath) {
|
|
3634
3714
|
const serviceId = `service-${randomUUID().slice(0, 8)}`;
|
|
3635
3715
|
const cwd = worktreePath ?? process.cwd();
|
|
@@ -3639,28 +3719,37 @@ export class Multiplexer {
|
|
|
3639
3719
|
const args = trimmed ? ["-lc", trimmed] : ["-l"];
|
|
3640
3720
|
const label = this.serviceLabelForCommand(trimmed);
|
|
3641
3721
|
const tmuxSession = this.tmuxRuntimeManager.ensureProjectSession(process.cwd());
|
|
3642
|
-
const
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
this.
|
|
3722
|
+
const shouldRenderPending = this.startedInDashboard && this.mode === "dashboard";
|
|
3723
|
+
if (shouldRenderPending) {
|
|
3724
|
+
this.setPendingDashboardSessionAction(serviceId, "creating");
|
|
3725
|
+
}
|
|
3726
|
+
try {
|
|
3727
|
+
const target = this.tmuxRuntimeManager.createWindow(tmuxSession.sessionName, label, cwd, command, args, {
|
|
3728
|
+
detached: true,
|
|
3729
|
+
});
|
|
3730
|
+
this.tmuxRuntimeManager.setWindowMetadata(target, {
|
|
3731
|
+
kind: "service",
|
|
3732
|
+
sessionId: serviceId,
|
|
3733
|
+
command,
|
|
3734
|
+
args,
|
|
3735
|
+
toolConfigKey: "service",
|
|
3736
|
+
worktreePath,
|
|
3737
|
+
label,
|
|
3738
|
+
});
|
|
3739
|
+
this.tmuxRuntimeManager.applyManagedAgentWindowPolicy(target, "service");
|
|
3740
|
+
this.invalidateDesktopStateSnapshot();
|
|
3741
|
+
this.refreshLocalDashboardModel();
|
|
3742
|
+
this.updateWorktreeSessions();
|
|
3743
|
+
this.preferDashboardEntrySelection("service", serviceId, worktreePath);
|
|
3744
|
+
this.settleDashboardCreatePending(serviceId);
|
|
3745
|
+
return { serviceId };
|
|
3746
|
+
}
|
|
3747
|
+
catch (error) {
|
|
3748
|
+
if (shouldRenderPending) {
|
|
3749
|
+
this.setPendingDashboardSessionAction(serviceId, null);
|
|
3750
|
+
}
|
|
3751
|
+
throw error;
|
|
3662
3752
|
}
|
|
3663
|
-
return { serviceId };
|
|
3664
3753
|
}
|
|
3665
3754
|
stopService(serviceId) {
|
|
3666
3755
|
const tmuxSession = this.tmuxRuntimeManager.getProjectSession(process.cwd());
|
|
@@ -3689,7 +3778,9 @@ export class Multiplexer {
|
|
|
3689
3778
|
// Ensure focusedWorktreePath is valid
|
|
3690
3779
|
if (!this.dashboardState.worktreeNavOrder.includes(this.dashboardState.focusedWorktreePath)) {
|
|
3691
3780
|
this.dashboardState.focusedWorktreePath = undefined;
|
|
3781
|
+
this.dashboardUiStateStore.markSelectionDirty();
|
|
3692
3782
|
}
|
|
3783
|
+
this.restoreDashboardSelectionFromPreference(dashSessions, hasWorktrees);
|
|
3693
3784
|
// Determine selected session for cursor
|
|
3694
3785
|
let selectedSession;
|
|
3695
3786
|
let selectedService;
|
|
@@ -3708,6 +3799,7 @@ export class Multiplexer {
|
|
|
3708
3799
|
this.dashboard.update(dashSessions, dashServices, worktreeGroups, this.dashboardState.focusedWorktreePath, hasWorktrees ? this.dashboardState.level : "sessions", selectedSession, selectedService, "tmux", mainCheckoutInfo);
|
|
3709
3800
|
this.syncTuiNotificationContext(Boolean(this.notificationPanelState));
|
|
3710
3801
|
this.writeFrame(this.dashboard.render(cols, rows));
|
|
3802
|
+
this.persistDashboardUiState();
|
|
3711
3803
|
if (this.dashboardBusyState) {
|
|
3712
3804
|
this.renderDashboardBusyOverlay();
|
|
3713
3805
|
}
|
|
@@ -3992,49 +4084,19 @@ export class Multiplexer {
|
|
|
3992
4084
|
}
|
|
3993
4085
|
}
|
|
3994
4086
|
startDashboardBusy(title, lines) {
|
|
3995
|
-
this.
|
|
3996
|
-
this.dashboardBusyState = {
|
|
3997
|
-
title,
|
|
3998
|
-
lines,
|
|
3999
|
-
startedAt: Date.now(),
|
|
4000
|
-
spinnerFrame: 0,
|
|
4001
|
-
};
|
|
4002
|
-
if (this.dashboardBusySpinner) {
|
|
4003
|
-
clearInterval(this.dashboardBusySpinner);
|
|
4004
|
-
}
|
|
4005
|
-
this.dashboardBusySpinner = setInterval(() => {
|
|
4006
|
-
if (!this.dashboardBusyState)
|
|
4007
|
-
return;
|
|
4008
|
-
this.dashboardBusyState.spinnerFrame = (this.dashboardBusyState.spinnerFrame + 1) % 10;
|
|
4009
|
-
if (this.mode === "dashboard")
|
|
4010
|
-
this.renderDashboard();
|
|
4011
|
-
}, 120);
|
|
4012
|
-
this.footerFlash = null;
|
|
4013
|
-
this.footerFlashTicks = 0;
|
|
4014
|
-
this.renderDashboard();
|
|
4087
|
+
this.dashboardFeedback.startBusy(title, lines);
|
|
4015
4088
|
}
|
|
4016
4089
|
updateDashboardBusy(lines) {
|
|
4017
|
-
|
|
4018
|
-
return;
|
|
4019
|
-
this.dashboardBusyState.lines = lines;
|
|
4020
|
-
if (this.mode === "dashboard")
|
|
4021
|
-
this.renderDashboard();
|
|
4090
|
+
this.dashboardFeedback.updateBusy(lines);
|
|
4022
4091
|
}
|
|
4023
4092
|
clearDashboardBusy() {
|
|
4024
|
-
|
|
4025
|
-
clearInterval(this.dashboardBusySpinner);
|
|
4026
|
-
this.dashboardBusySpinner = null;
|
|
4027
|
-
}
|
|
4028
|
-
this.dashboardBusyState = null;
|
|
4093
|
+
this.dashboardFeedback.clearBusy();
|
|
4029
4094
|
}
|
|
4030
4095
|
showDashboardError(title, lines) {
|
|
4031
|
-
this.
|
|
4032
|
-
this.dashboardErrorState = { title, lines };
|
|
4033
|
-
this.renderDashboard();
|
|
4096
|
+
this.dashboardFeedback.showError(title, lines);
|
|
4034
4097
|
}
|
|
4035
4098
|
dismissDashboardError() {
|
|
4036
|
-
this.
|
|
4037
|
-
this.renderDashboard();
|
|
4099
|
+
this.dashboardFeedback.dismissError();
|
|
4038
4100
|
}
|
|
4039
4101
|
beginWorktreeRemoval(path, name, oldIdx) {
|
|
4040
4102
|
if (this.worktreeRemovalJob)
|
|
@@ -4609,67 +4671,14 @@ export class Multiplexer {
|
|
|
4609
4671
|
renderMigratePicker() {
|
|
4610
4672
|
renderMigratePickerOverlay(this);
|
|
4611
4673
|
}
|
|
4612
|
-
waitForSessionExit(session, timeoutMs = 15_000) {
|
|
4613
|
-
if (session.exited)
|
|
4614
|
-
return Promise.resolve();
|
|
4615
|
-
return new Promise((resolve, reject) => {
|
|
4616
|
-
const timer = setTimeout(() => reject(new Error(`Timed out waiting for ${session.id} to exit`)), timeoutMs);
|
|
4617
|
-
session.onExit(() => {
|
|
4618
|
-
clearTimeout(timer);
|
|
4619
|
-
resolve();
|
|
4620
|
-
});
|
|
4621
|
-
});
|
|
4622
|
-
}
|
|
4623
4674
|
async runDashboardOperation(title, lines, work, errorTitle = title) {
|
|
4624
|
-
this.
|
|
4625
|
-
const minVisibleMs = 250;
|
|
4626
|
-
const startedAt = Date.now();
|
|
4627
|
-
try {
|
|
4628
|
-
const result = await work();
|
|
4629
|
-
const remaining = minVisibleMs - (Date.now() - startedAt);
|
|
4630
|
-
if (remaining > 0) {
|
|
4631
|
-
await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
4632
|
-
}
|
|
4633
|
-
this.clearDashboardBusy();
|
|
4634
|
-
return result;
|
|
4635
|
-
}
|
|
4636
|
-
catch (err) {
|
|
4637
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4638
|
-
this.showDashboardError(errorTitle, [message]);
|
|
4639
|
-
return undefined;
|
|
4640
|
-
}
|
|
4675
|
+
return this.dashboardFeedback.runOperation(title, lines, work, errorTitle);
|
|
4641
4676
|
}
|
|
4642
4677
|
setPendingDashboardSessionAction(sessionId, kind) {
|
|
4643
|
-
|
|
4644
|
-
this.pendingDashboardSessionActions.set(sessionId, kind);
|
|
4645
|
-
}
|
|
4646
|
-
else {
|
|
4647
|
-
this.pendingDashboardSessionActions.delete(sessionId);
|
|
4648
|
-
}
|
|
4649
|
-
if (this.mode === "dashboard") {
|
|
4650
|
-
this.refreshLocalDashboardModel();
|
|
4651
|
-
this.renderDashboard();
|
|
4652
|
-
}
|
|
4678
|
+
this.dashboardPendingActions.set(sessionId, kind);
|
|
4653
4679
|
}
|
|
4654
4680
|
async stopSessionToOfflineWithFeedback(session) {
|
|
4655
|
-
|
|
4656
|
-
this.setPendingDashboardSessionAction(session.id, "stopping");
|
|
4657
|
-
try {
|
|
4658
|
-
this.stopSessionToOffline(session);
|
|
4659
|
-
await this.waitForSessionExit(session);
|
|
4660
|
-
if (!this.graveyardAfterStopSessionIds.has(session.id)) {
|
|
4661
|
-
this.setPendingDashboardSessionAction(session.id, null);
|
|
4662
|
-
}
|
|
4663
|
-
this.refreshLocalDashboardModel();
|
|
4664
|
-
this.footerFlash = `Stopped ${label}`;
|
|
4665
|
-
this.footerFlashTicks = 3;
|
|
4666
|
-
this.renderDashboard();
|
|
4667
|
-
}
|
|
4668
|
-
catch (err) {
|
|
4669
|
-
this.setPendingDashboardSessionAction(session.id, null);
|
|
4670
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4671
|
-
this.showDashboardError(`Failed to stop "${label}"`, [message]);
|
|
4672
|
-
}
|
|
4681
|
+
await runStopSessionToOfflineWithFeedback(this.dashboardSessionActionDeps(), session);
|
|
4673
4682
|
}
|
|
4674
4683
|
clearDashboardSubscreens() {
|
|
4675
4684
|
this.dashboardState.resetSubscreen();
|
|
@@ -4778,30 +4787,34 @@ export class Multiplexer {
|
|
|
4778
4787
|
}
|
|
4779
4788
|
async graveyardSessionWithFeedback(sessionId, hasWorktrees) {
|
|
4780
4789
|
const session = this.offlineSessions.find((s) => s.id === sessionId) ?? this.sessions.find((s) => s.id === sessionId);
|
|
4781
|
-
|
|
4782
|
-
return;
|
|
4783
|
-
const label = ("label" in session ? session.label : this.getSessionLabel(sessionId)) ?? session.command;
|
|
4784
|
-
this.setPendingDashboardSessionAction(sessionId, "graveyarding");
|
|
4785
|
-
try {
|
|
4786
|
-
await this.sendAgentToGraveyard(sessionId);
|
|
4787
|
-
this.setPendingDashboardSessionAction(sessionId, null);
|
|
4788
|
-
this.adjustAfterRemove(hasWorktrees);
|
|
4789
|
-
this.footerFlash = `Sent ${label} to graveyard`;
|
|
4790
|
-
this.footerFlashTicks = 3;
|
|
4791
|
-
this.renderDashboard();
|
|
4792
|
-
}
|
|
4793
|
-
catch (err) {
|
|
4794
|
-
this.setPendingDashboardSessionAction(sessionId, null);
|
|
4795
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4796
|
-
this.showDashboardError(`Failed to graveyard "${label}"`, [message]);
|
|
4797
|
-
}
|
|
4790
|
+
await runGraveyardSessionWithFeedback(this.dashboardSessionActionDeps(), session, sessionId, hasWorktrees);
|
|
4798
4791
|
}
|
|
4799
4792
|
async resumeOfflineSessionWithFeedback(session) {
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4793
|
+
await runResumeOfflineSessionWithFeedback(this.dashboardSessionActionDeps(), session);
|
|
4794
|
+
}
|
|
4795
|
+
async waitForSessionStart(sessionId, timeoutMs = 8000) {
|
|
4796
|
+
return waitForSessionStart(sessionId, this.dashboardSessionActionDeps(), timeoutMs);
|
|
4797
|
+
}
|
|
4798
|
+
dashboardSessionActionDeps() {
|
|
4799
|
+
return {
|
|
4800
|
+
getSessionLabel: (sessionId) => this.getSessionLabel(sessionId),
|
|
4801
|
+
getPendingAction: (sessionId) => this.dashboardPendingActions.get(sessionId),
|
|
4802
|
+
setPendingAction: (sessionId, kind) => this.setPendingDashboardSessionAction(sessionId, kind),
|
|
4803
|
+
stopSessionToOffline: (session) => this.stopSessionToOffline(session),
|
|
4804
|
+
isGraveyardAfterStop: (sessionId) => this.graveyardAfterStopSessionIds.has(sessionId),
|
|
4805
|
+
sendAgentToGraveyard: (sessionId) => this.sendAgentToGraveyard(sessionId).then(() => undefined),
|
|
4806
|
+
resumeOfflineSession: (session) => this.resumeOfflineSession(session),
|
|
4807
|
+
refreshLocalDashboardModel: () => this.refreshLocalDashboardModel(),
|
|
4808
|
+
adjustAfterRemove: (hasWorktrees) => this.adjustAfterRemove(hasWorktrees),
|
|
4809
|
+
renderDashboard: () => this.renderDashboard(),
|
|
4810
|
+
showDashboardError: (title, lines) => this.showDashboardError(title, lines),
|
|
4811
|
+
setFooterFlash: (message, ticks) => {
|
|
4812
|
+
this.footerFlash = message;
|
|
4813
|
+
this.footerFlashTicks = ticks;
|
|
4814
|
+
},
|
|
4815
|
+
getRuntimeById: (sessionId) => this.sessions.find((session) => session.id === sessionId),
|
|
4816
|
+
isSessionRuntimeLive: (session) => this.isSessionRuntimeLive(session),
|
|
4817
|
+
};
|
|
4805
4818
|
}
|
|
4806
4819
|
async takeoverFromDashEntryWithFeedback(entry) {
|
|
4807
4820
|
const label = entry.label ?? entry.command;
|
|
@@ -4811,7 +4824,7 @@ export class Multiplexer {
|
|
|
4811
4824
|
const label = this.getSessionLabel(session.id) ?? session.command;
|
|
4812
4825
|
await this.runDashboardOperation(`Migrating "${label}"`, [` From: ${this.sessionWorktreePaths.get(session.id) ?? "(main)"}`, ` To: ${targetName}`], async () => {
|
|
4813
4826
|
await this.migrateAgent(session.id, targetPath);
|
|
4814
|
-
await
|
|
4827
|
+
await waitForSessionExit(session);
|
|
4815
4828
|
this.renderDashboard();
|
|
4816
4829
|
}, `Failed to migrate "${label}"`);
|
|
4817
4830
|
}
|
|
@@ -5097,7 +5110,7 @@ export class Multiplexer {
|
|
|
5097
5110
|
label: session.label,
|
|
5098
5111
|
tmuxWindowId: session.tmuxWindowId,
|
|
5099
5112
|
tmuxWindowIndex: session.tmuxWindowIndex,
|
|
5100
|
-
windowName: session.
|
|
5113
|
+
windowName: session.command,
|
|
5101
5114
|
headline: session.headline,
|
|
5102
5115
|
status: session.status,
|
|
5103
5116
|
role: session.role,
|
|
@@ -5112,7 +5125,7 @@ export class Multiplexer {
|
|
|
5112
5125
|
label: service.label,
|
|
5113
5126
|
tmuxWindowId: service.tmuxWindowId,
|
|
5114
5127
|
tmuxWindowIndex: service.tmuxWindowIndex,
|
|
5115
|
-
windowName: service.
|
|
5128
|
+
windowName: service.command,
|
|
5116
5129
|
headline: service.previewLine,
|
|
5117
5130
|
status: service.status,
|
|
5118
5131
|
active: service.active,
|
|
@@ -5273,12 +5286,35 @@ export class Multiplexer {
|
|
|
5273
5286
|
/** Track previous statuses for notification on transition */
|
|
5274
5287
|
prevStatuses = new Map();
|
|
5275
5288
|
/** Flash message shown temporarily in footer, cleared after a few renders */
|
|
5276
|
-
|
|
5277
|
-
|
|
5289
|
+
get dashboardBusyState() {
|
|
5290
|
+
return this.dashboardFeedback.busyState;
|
|
5291
|
+
}
|
|
5292
|
+
set dashboardBusyState(value) {
|
|
5293
|
+
this.dashboardFeedback.busyState = value;
|
|
5294
|
+
}
|
|
5295
|
+
get dashboardErrorState() {
|
|
5296
|
+
return this.dashboardFeedback.errorState;
|
|
5297
|
+
}
|
|
5298
|
+
set dashboardErrorState(value) {
|
|
5299
|
+
this.dashboardFeedback.errorState = value;
|
|
5300
|
+
}
|
|
5301
|
+
get footerFlash() {
|
|
5302
|
+
return this.dashboardFeedback.flash;
|
|
5303
|
+
}
|
|
5304
|
+
set footerFlash(value) {
|
|
5305
|
+
this.dashboardFeedback.flash = value;
|
|
5306
|
+
}
|
|
5307
|
+
get footerFlashTicks() {
|
|
5308
|
+
return this.dashboardFeedback.flashTicks;
|
|
5309
|
+
}
|
|
5310
|
+
set footerFlashTicks(value) {
|
|
5311
|
+
this.dashboardFeedback.flashTicks = value;
|
|
5312
|
+
}
|
|
5278
5313
|
startStatusRefresh() {
|
|
5279
5314
|
if (this.statusInterval)
|
|
5280
5315
|
return;
|
|
5281
5316
|
this.statusInterval = setInterval(() => {
|
|
5317
|
+
let dashboardNeedsRender = false;
|
|
5282
5318
|
if (this.mode === "project-service") {
|
|
5283
5319
|
this.taskDispatcher?.tick(this.sessions.map((s) => s.id));
|
|
5284
5320
|
this.orchestrationDispatcher?.tick(this.sessions.map((s) => s.id));
|
|
@@ -5305,18 +5341,19 @@ export class Multiplexer {
|
|
|
5305
5341
|
this.footerFlash = `↻ Changes requested: ${ev.description}`;
|
|
5306
5342
|
}
|
|
5307
5343
|
this.footerFlashTicks = 3;
|
|
5344
|
+
dashboardNeedsRender = true;
|
|
5308
5345
|
}
|
|
5309
5346
|
const orchestrationEvents = this.orchestrationDispatcher?.drainEvents() ?? [];
|
|
5310
5347
|
for (const event of orchestrationEvents) {
|
|
5311
5348
|
if (event.type === "message_delivered") {
|
|
5312
5349
|
this.footerFlash = `✉ Message delivered → ${event.sessionId}`;
|
|
5313
5350
|
this.footerFlashTicks = 3;
|
|
5351
|
+
dashboardNeedsRender = true;
|
|
5314
5352
|
}
|
|
5315
5353
|
}
|
|
5316
|
-
if (this.
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
this.footerFlash = null;
|
|
5354
|
+
if (this.dashboardFeedback.tickFlashVisibilityChanged()) {
|
|
5355
|
+
dashboardNeedsRender = true;
|
|
5356
|
+
}
|
|
5320
5357
|
for (const session of this.sessions) {
|
|
5321
5358
|
const prev = this.prevStatuses.get(session.id);
|
|
5322
5359
|
const curr = session.status;
|
|
@@ -5333,8 +5370,18 @@ export class Multiplexer {
|
|
|
5333
5370
|
this.prevStatuses.set(session.id, curr);
|
|
5334
5371
|
}
|
|
5335
5372
|
if (this.mode === "dashboard") {
|
|
5336
|
-
|
|
5337
|
-
this.
|
|
5373
|
+
const now = Date.now();
|
|
5374
|
+
if (now >= this.dashboardNextBackgroundRefreshAt) {
|
|
5375
|
+
this.dashboardNextBackgroundRefreshAt = now + 5000;
|
|
5376
|
+
void this.refreshDashboardModelFromService().then((refreshed) => {
|
|
5377
|
+
if (refreshed || dashboardNeedsRender) {
|
|
5378
|
+
this.renderCurrentDashboardView();
|
|
5379
|
+
}
|
|
5380
|
+
});
|
|
5381
|
+
}
|
|
5382
|
+
else if (dashboardNeedsRender) {
|
|
5383
|
+
this.renderCurrentDashboardView();
|
|
5384
|
+
}
|
|
5338
5385
|
}
|
|
5339
5386
|
}, 1000);
|
|
5340
5387
|
}
|
|
@@ -5351,8 +5398,11 @@ export class Multiplexer {
|
|
|
5351
5398
|
this.invalidateDesktopStateSnapshot();
|
|
5352
5399
|
}
|
|
5353
5400
|
loadOfflineSessions(state = Multiplexer.loadState()) {
|
|
5354
|
-
if (!state || state.sessions.length === 0)
|
|
5355
|
-
|
|
5401
|
+
if (!state || state.sessions.length === 0) {
|
|
5402
|
+
const changed = this.offlineSessions.length > 0;
|
|
5403
|
+
this.offlineSessions = [];
|
|
5404
|
+
return changed;
|
|
5405
|
+
}
|
|
5356
5406
|
// Get all session IDs owned by live instances (including ourselves)
|
|
5357
5407
|
const ownedIds = new Set();
|
|
5358
5408
|
for (const s of this.sessions)
|
|
@@ -5363,7 +5413,7 @@ export class Multiplexer {
|
|
|
5363
5413
|
}
|
|
5364
5414
|
// Also exclude by backendSessionId to catch resumed sessions with new IDs
|
|
5365
5415
|
const ownedBackendIds = new Set(this.sessions.map((session) => session.backendSessionId).filter((value) => Boolean(value)));
|
|
5366
|
-
|
|
5416
|
+
const nextOfflineSessions = state.sessions.filter((s) => {
|
|
5367
5417
|
if (ownedIds.has(s.id))
|
|
5368
5418
|
return false;
|
|
5369
5419
|
if (s.backendSessionId && ownedBackendIds.has(s.backendSessionId))
|
|
@@ -5372,9 +5422,17 @@ export class Multiplexer {
|
|
|
5372
5422
|
return false;
|
|
5373
5423
|
return true;
|
|
5374
5424
|
});
|
|
5425
|
+
const previousKey = this.offlineSessions
|
|
5426
|
+
.map((session) => `${session.id}:${session.label ?? ""}:${session.worktreePath ?? ""}`)
|
|
5427
|
+
.join("|");
|
|
5428
|
+
const nextKey = nextOfflineSessions
|
|
5429
|
+
.map((session) => `${session.id}:${session.label ?? ""}:${session.worktreePath ?? ""}`)
|
|
5430
|
+
.join("|");
|
|
5431
|
+
this.offlineSessions = nextOfflineSessions;
|
|
5375
5432
|
if (this.offlineSessions.length > 0) {
|
|
5376
5433
|
debug(`loaded ${this.offlineSessions.length} offline session(s) from state.json`, "session");
|
|
5377
5434
|
}
|
|
5435
|
+
return previousKey !== nextKey;
|
|
5378
5436
|
}
|
|
5379
5437
|
restoreTmuxSessionsFromState(state = Multiplexer.loadState()) {
|
|
5380
5438
|
const savedById = new Map((state?.sessions ?? []).map((session) => [session.id, session]));
|
|
@@ -5396,7 +5454,9 @@ export class Multiplexer {
|
|
|
5396
5454
|
const label = metadata.label ?? saved?.label;
|
|
5397
5455
|
if (label) {
|
|
5398
5456
|
this.sessionLabels.set(metadata.sessionId, label);
|
|
5399
|
-
|
|
5457
|
+
}
|
|
5458
|
+
if (target.windowName !== metadata.command) {
|
|
5459
|
+
transport.renameWindow(metadata.command);
|
|
5400
5460
|
}
|
|
5401
5461
|
this.syncTmuxWindowMetadata(metadata.sessionId);
|
|
5402
5462
|
}
|
|
@@ -5478,55 +5538,85 @@ export class Multiplexer {
|
|
|
5478
5538
|
}
|
|
5479
5539
|
debug(`graveyarded session ${sessionId}`, "session");
|
|
5480
5540
|
}
|
|
5541
|
+
isSessionRuntimeLive(runtime) {
|
|
5542
|
+
if (runtime.exited)
|
|
5543
|
+
return false;
|
|
5544
|
+
const mappedTarget = this.sessionTmuxTargets.get(runtime.id);
|
|
5545
|
+
const runtimeTarget = runtime.transport instanceof TmuxSessionTransport ? runtime.transport.tmuxTarget : undefined;
|
|
5546
|
+
const target = mappedTarget ?? runtimeTarget;
|
|
5547
|
+
if (!target)
|
|
5548
|
+
return false;
|
|
5549
|
+
try {
|
|
5550
|
+
return Boolean(this.tmuxRuntimeManager.getTargetByWindowId(target.sessionName, target.windowId));
|
|
5551
|
+
}
|
|
5552
|
+
catch {
|
|
5553
|
+
return false;
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
evictZombieSession(runtime) {
|
|
5557
|
+
const idx = this.sessions.indexOf(runtime);
|
|
5558
|
+
if (idx >= 0) {
|
|
5559
|
+
this.sessions.splice(idx, 1);
|
|
5560
|
+
}
|
|
5561
|
+
this.stoppingSessionIds.delete(runtime.id);
|
|
5562
|
+
this.sessionTmuxTargets.delete(runtime.id);
|
|
5563
|
+
this.writeSessionsFile();
|
|
5564
|
+
this.updateContextWatcherSessions();
|
|
5565
|
+
this.saveState();
|
|
5566
|
+
}
|
|
5481
5567
|
/** Resume a specific offline session */
|
|
5482
5568
|
resumeOfflineSession(session) {
|
|
5569
|
+
const existing = this.sessions.find((runtime) => runtime.id === session.id);
|
|
5570
|
+
if (existing) {
|
|
5571
|
+
if (this.isSessionRuntimeLive(existing)) {
|
|
5572
|
+
this.offlineSessions = this.offlineSessions.filter((s) => s.id !== session.id);
|
|
5573
|
+
this.invalidateDesktopStateSnapshot();
|
|
5574
|
+
this.writeStatuslineFile();
|
|
5575
|
+
return;
|
|
5576
|
+
}
|
|
5577
|
+
this.evictZombieSession(existing);
|
|
5578
|
+
}
|
|
5483
5579
|
const config = loadConfig();
|
|
5484
5580
|
const toolCfg = config.tools[session.toolConfigKey];
|
|
5485
5581
|
if (!toolCfg)
|
|
5486
5582
|
return;
|
|
5583
|
+
const derived = loadMetadataState().sessions[session.id]?.derived;
|
|
5584
|
+
const relaunchFresh = derived?.activity === "error" || derived?.attention === "error";
|
|
5585
|
+
const useBackendResume = !relaunchFresh && this.sessionBootstrap.canResumeWithBackendSessionId(toolCfg, session.backendSessionId);
|
|
5487
5586
|
let actionArgs;
|
|
5488
|
-
if (
|
|
5587
|
+
if (useBackendResume) {
|
|
5489
5588
|
actionArgs = toolCfg.resumeArgs.map((a) => a.replace("{sessionId}", session.backendSessionId));
|
|
5490
5589
|
}
|
|
5590
|
+
else if (relaunchFresh) {
|
|
5591
|
+
actionArgs = [];
|
|
5592
|
+
}
|
|
5491
5593
|
else {
|
|
5492
5594
|
actionArgs = [...(toolCfg.resumeFallback ?? [])];
|
|
5493
5595
|
}
|
|
5494
|
-
const args =
|
|
5495
|
-
|
|
5596
|
+
const args = [...(toolCfg.args ?? []), ...actionArgs];
|
|
5597
|
+
if (relaunchFresh) {
|
|
5598
|
+
updateSessionMetadata(session.id, (current) => {
|
|
5599
|
+
const next = { ...current };
|
|
5600
|
+
delete next.derived;
|
|
5601
|
+
delete next.status;
|
|
5602
|
+
delete next.progress;
|
|
5603
|
+
return next;
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
5606
|
+
const preservedLabel = session.label ?? this.getSessionLabel(session.id);
|
|
5496
5607
|
this.offlineSessions = this.offlineSessions.filter((s) => s.id !== session.id);
|
|
5497
|
-
|
|
5608
|
+
this.invalidateDesktopStateSnapshot();
|
|
5609
|
+
this.saveState();
|
|
5610
|
+
this.writeStatuslineFile();
|
|
5611
|
+
if (preservedLabel) {
|
|
5612
|
+
this.sessionLabels.set(session.id, preservedLabel);
|
|
5613
|
+
}
|
|
5614
|
+
debug(`resuming offline session ${session.id} (${relaunchFresh ? "fresh" : useBackendResume ? `backend=${session.backendSessionId ?? "none"}` : "fallback"})`, "session");
|
|
5498
5615
|
this.createSession(session.command, args, toolCfg.preambleFlag, session.toolConfigKey, undefined, undefined, // don't pass sessionIdFlag — we're resuming with existing backend ID
|
|
5499
|
-
session.worktreePath, session.backendSessionId);
|
|
5616
|
+
session.worktreePath, useBackendResume ? session.backendSessionId : undefined, session.id, true);
|
|
5500
5617
|
}
|
|
5501
5618
|
startHeartbeat() {
|
|
5502
|
-
|
|
5503
|
-
return;
|
|
5504
|
-
this.heartbeatInterval = setInterval(() => {
|
|
5505
|
-
if (this.mode === "project-service") {
|
|
5506
|
-
this.syncSessionsFromState();
|
|
5507
|
-
return;
|
|
5508
|
-
}
|
|
5509
|
-
const sessions = this.getInstanceSessionRefs();
|
|
5510
|
-
this.instanceDirectory
|
|
5511
|
-
.reconcileHeartbeat(this.instanceId, sessions, process.cwd(), this.confirmedRegistered)
|
|
5512
|
-
.then((result) => {
|
|
5513
|
-
for (const id of result.claimedIds) {
|
|
5514
|
-
debug(`session ${id} claimed: was in confirmedRegistered but not in previousIds`, "instance");
|
|
5515
|
-
this.handleSessionClaimed(id);
|
|
5516
|
-
}
|
|
5517
|
-
if (result.skippedClaimDetection && this.confirmedRegistered.size > 0) {
|
|
5518
|
-
debug(`skipping claim detection: previousIds empty but ${this.confirmedRegistered.size} confirmed sessions (registry entry may have been pruned)`, "instance");
|
|
5519
|
-
}
|
|
5520
|
-
this.confirmedRegistered = result.confirmedIds;
|
|
5521
|
-
})
|
|
5522
|
-
.catch(() => { });
|
|
5523
|
-
// Refresh offline sessions from state.json (picks up cross-instance graveyard/kill)
|
|
5524
|
-
this.loadOfflineSessions();
|
|
5525
|
-
// Refresh dashboard to pick up remote instance changes (skip if overlay is active)
|
|
5526
|
-
if (this.mode === "dashboard") {
|
|
5527
|
-
this.renderCurrentDashboardView();
|
|
5528
|
-
}
|
|
5529
|
-
}, 5000);
|
|
5619
|
+
this.runtimeSync.startHeartbeat();
|
|
5530
5620
|
}
|
|
5531
5621
|
/**
|
|
5532
5622
|
* Handle a session that was claimed (taken over) by another aimux instance.
|
|
@@ -5555,24 +5645,13 @@ export class Multiplexer {
|
|
|
5555
5645
|
this.renderDashboard();
|
|
5556
5646
|
}
|
|
5557
5647
|
stopHeartbeat() {
|
|
5558
|
-
|
|
5559
|
-
clearInterval(this.heartbeatInterval);
|
|
5560
|
-
this.heartbeatInterval = null;
|
|
5561
|
-
}
|
|
5648
|
+
this.runtimeSync.stopHeartbeat();
|
|
5562
5649
|
}
|
|
5563
5650
|
startProjectServiceRefresh() {
|
|
5564
|
-
|
|
5565
|
-
return;
|
|
5566
|
-
this.projectServiceInterval = setInterval(() => {
|
|
5567
|
-
this.syncSessionsFromState();
|
|
5568
|
-
this.writeStatuslineFile();
|
|
5569
|
-
}, 2000);
|
|
5651
|
+
this.runtimeSync.startProjectServiceRefresh();
|
|
5570
5652
|
}
|
|
5571
5653
|
stopProjectServiceRefresh() {
|
|
5572
|
-
|
|
5573
|
-
clearInterval(this.projectServiceInterval);
|
|
5574
|
-
this.projectServiceInterval = null;
|
|
5575
|
-
}
|
|
5654
|
+
this.runtimeSync.stopProjectServiceRefresh();
|
|
5576
5655
|
}
|
|
5577
5656
|
getRemoteInstancesSafe() {
|
|
5578
5657
|
return this.instanceDirectory.getRemoteInstancesSafe(this.instanceId, process.cwd());
|