aimux-cli 0.1.3 → 0.1.4
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/README.md +20 -19
- package/dist/agent-tracker.js +15 -13
- package/dist/agent-tracker.js.map +1 -1
- package/dist/context/context-bridge.js +8 -1
- package/dist/context/context-bridge.js.map +1 -1
- package/dist/daemon.js +8 -1
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-pending-actions.js +1 -1
- package/dist/dashboard-pending-actions.js.map +1 -1
- package/dist/dashboard-session-actions.d.ts +4 -4
- package/dist/dashboard-session-actions.js.map +1 -1
- package/dist/dashboard.d.ts +2 -2
- package/dist/key-parser.js +7 -0
- package/dist/key-parser.js.map +1 -1
- package/dist/main.js +227 -41
- package/dist/main.js.map +1 -1
- package/dist/metadata-server.d.ts +18 -0
- package/dist/metadata-server.js +22 -0
- package/dist/metadata-server.js.map +1 -1
- package/dist/multiplexer.d.ts +14 -0
- package/dist/multiplexer.js +329 -41
- package/dist/multiplexer.js.map +1 -1
- package/dist/notification-context.d.ts +1 -0
- package/dist/notification-context.js +12 -0
- package/dist/notification-context.js.map +1 -1
- package/dist/plugin-runtime.js +8 -2
- package/dist/plugin-runtime.js.map +1 -1
- package/dist/project-scanner.js +14 -3
- package/dist/project-scanner.js.map +1 -1
- package/dist/shell-hooks.d.ts +27 -0
- package/dist/shell-hooks.js +137 -0
- package/dist/shell-hooks.js.map +1 -0
- package/dist/tmux-doctor.d.ts +10 -0
- package/dist/tmux-doctor.js +62 -0
- package/dist/tmux-doctor.js.map +1 -1
- package/dist/tmux-runtime-manager.d.ts +8 -0
- package/dist/tmux-runtime-manager.js +51 -12
- package/dist/tmux-runtime-manager.js.map +1 -1
- package/dist/tmux-statusline.js +1 -1
- package/dist/tmux-statusline.js.map +1 -1
- package/dist/tool-output-watchers.js +128 -33
- 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
|
@@ -41,6 +41,7 @@ import { appendSessionMessage, readSessionMessages } from "./session-message-his
|
|
|
41
41
|
import { ProjectEventBus } from "./project-events.js";
|
|
42
42
|
import { deriveSessionSemantics } from "./session-semantics.js";
|
|
43
43
|
import { injectClaudeHookArgs } from "./claude-hooks.js";
|
|
44
|
+
import { wrapCommandWithShellIntegration, wrapInteractiveShellWithIntegration } from "./shell-hooks.js";
|
|
44
45
|
import { navigationUrgencyScore } from "./fast-control.js";
|
|
45
46
|
import { requestJson } from "./http-client.js";
|
|
46
47
|
import { openDashboardTarget } from "./dashboard-targets.js";
|
|
@@ -60,6 +61,7 @@ import { graveyardSessionWithFeedback as runGraveyardSessionWithFeedback, resume
|
|
|
60
61
|
export class Multiplexer {
|
|
61
62
|
projectRoot;
|
|
62
63
|
sessions = [];
|
|
64
|
+
offlineServices = [];
|
|
63
65
|
activeIndex = 0;
|
|
64
66
|
mode = "dashboard";
|
|
65
67
|
hotkeys;
|
|
@@ -168,6 +170,7 @@ export class Multiplexer {
|
|
|
168
170
|
dashboardServicesCache = [];
|
|
169
171
|
dashboardWorktreeGroupsCache = [];
|
|
170
172
|
dashboardMainCheckoutInfoCache = { name: "Main Checkout", branch: "" };
|
|
173
|
+
dashboardModelSnapshotKey = null;
|
|
171
174
|
dashboardModelRefreshedAt = 0;
|
|
172
175
|
dashboardServiceSnapshotRefreshing = false;
|
|
173
176
|
dashboardServiceRecovery = null;
|
|
@@ -356,12 +359,24 @@ export class Multiplexer {
|
|
|
356
359
|
});
|
|
357
360
|
}
|
|
358
361
|
applyDashboardModel(dashSessions, dashServices, worktreeGroups, mainCheckoutInfo) {
|
|
362
|
+
const snapshotKey = JSON.stringify({
|
|
363
|
+
sessions: dashSessions,
|
|
364
|
+
services: dashServices,
|
|
365
|
+
worktreeGroups,
|
|
366
|
+
mainCheckoutInfo,
|
|
367
|
+
});
|
|
368
|
+
if (snapshotKey === this.dashboardModelSnapshotKey) {
|
|
369
|
+
this.dashboardModelRefreshedAt = Date.now();
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
this.dashboardModelSnapshotKey = snapshotKey;
|
|
359
373
|
this.dashboardSessionsCache = this.dashboardPendingActions.applyToSessions(dashSessions);
|
|
360
374
|
this.dashboardServicesCache = this.dashboardPendingActions.applyToServices(dashServices);
|
|
361
375
|
this.dashboardWorktreeGroupsCache = worktreeGroups;
|
|
362
376
|
this.dashboardMainCheckoutInfoCache = mainCheckoutInfo;
|
|
363
377
|
this.dashboardModelRefreshedAt = Date.now();
|
|
364
378
|
this.dashboardUiStateStore.markSelectionDirty();
|
|
379
|
+
return true;
|
|
365
380
|
}
|
|
366
381
|
invalidateDesktopStateSnapshot() {
|
|
367
382
|
this.desktopStateSnapshot = null;
|
|
@@ -503,10 +518,9 @@ export class Multiplexer {
|
|
|
503
518
|
}
|
|
504
519
|
computeDashboardServices(worktrees = this.listDesktopWorktrees()) {
|
|
505
520
|
const lastUsedState = loadLastUsedState(process.cwd());
|
|
506
|
-
const tmuxSession = this.tmuxRuntimeManager.getProjectSession(process.cwd());
|
|
507
521
|
const worktreeByPath = new Map(worktrees.map((wt) => [wt.path, wt]));
|
|
508
|
-
|
|
509
|
-
.
|
|
522
|
+
const liveServices = this.tmuxRuntimeManager
|
|
523
|
+
.listProjectManagedWindows(process.cwd())
|
|
510
524
|
.filter(({ target, metadata }) => !isDashboardWindowName(target.windowName) && metadata.kind === "service")
|
|
511
525
|
.map(({ target, metadata }) => {
|
|
512
526
|
const worktree = metadata.worktreePath ? worktreeByPath.get(metadata.worktreePath) : undefined;
|
|
@@ -530,6 +544,30 @@ export class Multiplexer {
|
|
|
530
544
|
previewLine: info.previewLine,
|
|
531
545
|
};
|
|
532
546
|
});
|
|
547
|
+
const liveIds = new Set(liveServices.map((service) => service.id));
|
|
548
|
+
const offlineServices = this.offlineServices
|
|
549
|
+
.filter((service) => !liveIds.has(service.id))
|
|
550
|
+
.map((service) => {
|
|
551
|
+
const worktree = service.worktreePath ? worktreeByPath.get(service.worktreePath) : undefined;
|
|
552
|
+
const label = service.label ?? this.serviceLabelForCommand(service.launchCommandLine ?? "");
|
|
553
|
+
const previewLine = service.launchCommandLine?.trim() || "Interactive shell";
|
|
554
|
+
return {
|
|
555
|
+
id: service.id,
|
|
556
|
+
command: service.launchCommandLine?.trim() ?? "",
|
|
557
|
+
args: [],
|
|
558
|
+
lastUsedAt: lastUsedState.items[service.id]?.lastUsedAt,
|
|
559
|
+
worktreePath: service.worktreePath,
|
|
560
|
+
worktreeName: worktree?.name,
|
|
561
|
+
worktreeBranch: worktree?.branch,
|
|
562
|
+
status: "offline",
|
|
563
|
+
active: false,
|
|
564
|
+
label,
|
|
565
|
+
cwd: service.worktreePath,
|
|
566
|
+
foregroundCommand: label,
|
|
567
|
+
previewLine,
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
return [...liveServices, ...offlineServices];
|
|
533
571
|
}
|
|
534
572
|
readTmuxProcessInfo(target) {
|
|
535
573
|
const raw = this.tmuxRuntimeManager.displayMessage("#{pane_current_command}\t#{pane_pid}", target.windowId) ?? "";
|
|
@@ -595,8 +633,7 @@ export class Multiplexer {
|
|
|
595
633
|
const dashServices = body.services ?? [];
|
|
596
634
|
const worktrees = body.worktrees ?? [];
|
|
597
635
|
const worktreeGroups = this.buildDashboardWorktreeGroups(dashSessions, dashServices, worktrees, body.mainCheckoutPath);
|
|
598
|
-
this.applyDashboardModel(dashSessions, dashServices, worktreeGroups, body.mainCheckoutInfo ?? { name: "Main Checkout", branch: "" });
|
|
599
|
-
return true;
|
|
636
|
+
return this.applyDashboardModel(dashSessions, dashServices, worktreeGroups, body.mainCheckoutInfo ?? { name: "Main Checkout", branch: "" });
|
|
600
637
|
}
|
|
601
638
|
}
|
|
602
639
|
catch {
|
|
@@ -638,6 +675,8 @@ export class Multiplexer {
|
|
|
638
675
|
removeWorktree: ({ path }) => this.removeDesktopWorktree(path),
|
|
639
676
|
createService: ({ command, worktreePath }) => this.createService(command ?? "", worktreePath),
|
|
640
677
|
stopService: ({ serviceId }) => this.stopService(serviceId),
|
|
678
|
+
resumeService: ({ serviceId }) => this.resumeOfflineServiceById(serviceId),
|
|
679
|
+
removeService: ({ serviceId }) => this.removeOfflineService(serviceId),
|
|
641
680
|
listGraveyard: () => this.listGraveyardEntries(),
|
|
642
681
|
resurrectGraveyard: ({ sessionId }) => this.resurrectGraveyardSession(sessionId),
|
|
643
682
|
},
|
|
@@ -1437,14 +1476,14 @@ export class Multiplexer {
|
|
|
1437
1476
|
finalArgs = [...finalArgs, ...expandedFlag];
|
|
1438
1477
|
}
|
|
1439
1478
|
const toolCfg = toolConfigKey ? loadConfig().tools[toolConfigKey] : undefined;
|
|
1479
|
+
let projectRoot = process.cwd();
|
|
1480
|
+
try {
|
|
1481
|
+
projectRoot = findMainRepo(worktreePath ?? process.cwd());
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
projectRoot = process.cwd();
|
|
1485
|
+
}
|
|
1440
1486
|
if (toolCfg && toolConfigKey === "claude" && toolCfg.command === command && toolCfg.wrapperEnabled !== false) {
|
|
1441
|
-
let projectRoot = process.cwd();
|
|
1442
|
-
try {
|
|
1443
|
-
projectRoot = findMainRepo(worktreePath ?? process.cwd());
|
|
1444
|
-
}
|
|
1445
|
-
catch {
|
|
1446
|
-
projectRoot = process.cwd();
|
|
1447
|
-
}
|
|
1448
1487
|
finalArgs = injectClaudeHookArgs(finalArgs, {
|
|
1449
1488
|
sessionId,
|
|
1450
1489
|
projectRoot,
|
|
@@ -1452,6 +1491,17 @@ export class Multiplexer {
|
|
|
1452
1491
|
});
|
|
1453
1492
|
launchCommand = toolCfg.command;
|
|
1454
1493
|
}
|
|
1494
|
+
else if (toolCfg && toolCfg.command === command) {
|
|
1495
|
+
const wrapped = wrapCommandWithShellIntegration({
|
|
1496
|
+
projectRoot,
|
|
1497
|
+
sessionId,
|
|
1498
|
+
tool: toolConfigKey ?? command,
|
|
1499
|
+
command: launchCommand,
|
|
1500
|
+
args: finalArgs,
|
|
1501
|
+
});
|
|
1502
|
+
launchCommand = wrapped.command;
|
|
1503
|
+
finalArgs = wrapped.args;
|
|
1504
|
+
}
|
|
1455
1505
|
if (preambleFlag) {
|
|
1456
1506
|
this.sessionBootstrap.finalizePreamble(command, preamble);
|
|
1457
1507
|
}
|
|
@@ -1558,6 +1608,11 @@ export class Multiplexer {
|
|
|
1558
1608
|
const sid = this.sessions[index].id;
|
|
1559
1609
|
this.sessionMRU = [sid, ...this.sessionMRU.filter((id) => id !== sid)];
|
|
1560
1610
|
this.agentTracker.markSeen(sid);
|
|
1611
|
+
updateNotificationContext("tui", {
|
|
1612
|
+
focused: true,
|
|
1613
|
+
sessionId: sid,
|
|
1614
|
+
panelOpen: false,
|
|
1615
|
+
});
|
|
1561
1616
|
this.noteLastUsedItem(sid);
|
|
1562
1617
|
markNotificationsRead({ sessionId: sid });
|
|
1563
1618
|
this.syncTuiNotificationContext(false);
|
|
@@ -1740,13 +1795,19 @@ export class Multiplexer {
|
|
|
1740
1795
|
const selectedService = this.getSelectedDashboardServiceForActions();
|
|
1741
1796
|
if (selectedService) {
|
|
1742
1797
|
try {
|
|
1743
|
-
|
|
1744
|
-
|
|
1798
|
+
if (selectedService.status === "offline") {
|
|
1799
|
+
this.removeOfflineService(selectedService.id);
|
|
1800
|
+
this.footerFlash = `◆ Deleted service ${selectedService.label ?? selectedService.id}`;
|
|
1801
|
+
}
|
|
1802
|
+
else {
|
|
1803
|
+
this.stopService(selectedService.id);
|
|
1804
|
+
this.footerFlash = `◆ Stopped service ${selectedService.label ?? selectedService.id}`;
|
|
1805
|
+
}
|
|
1745
1806
|
this.footerFlashTicks = 3;
|
|
1746
1807
|
this.renderDashboard();
|
|
1747
1808
|
}
|
|
1748
1809
|
catch (error) {
|
|
1749
|
-
this.showDashboardError("Failed to stop service", [error instanceof Error ? error.message : String(error)]);
|
|
1810
|
+
this.showDashboardError(selectedService.status === "offline" ? "Failed to delete service" : "Failed to stop service", [error instanceof Error ? error.message : String(error)]);
|
|
1750
1811
|
}
|
|
1751
1812
|
return;
|
|
1752
1813
|
}
|
|
@@ -1829,7 +1890,10 @@ export class Multiplexer {
|
|
|
1829
1890
|
case "enter": {
|
|
1830
1891
|
const ds = this.getDashboardSessions();
|
|
1831
1892
|
const entry = ds[this.activeIndex];
|
|
1832
|
-
if (entry
|
|
1893
|
+
if (entry?.pendingAction === "creating" || entry?.pendingAction === "starting") {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (entry && this.openLiveTmuxWindowForEntry(entry) !== "missing") {
|
|
1833
1897
|
return;
|
|
1834
1898
|
}
|
|
1835
1899
|
if (entry?.remoteInstanceId) {
|
|
@@ -1838,7 +1902,7 @@ export class Multiplexer {
|
|
|
1838
1902
|
}
|
|
1839
1903
|
if (entry?.status === "offline") {
|
|
1840
1904
|
const offline = this.offlineSessions.find((s) => s.id === entry.id);
|
|
1841
|
-
if (offline
|
|
1905
|
+
if (offline) {
|
|
1842
1906
|
void this.resumeOfflineSessionWithFeedback(offline);
|
|
1843
1907
|
}
|
|
1844
1908
|
return;
|
|
@@ -1923,13 +1987,38 @@ export class Multiplexer {
|
|
|
1923
1987
|
if (!selectedEntry)
|
|
1924
1988
|
break;
|
|
1925
1989
|
if (selectedEntry.kind === "service") {
|
|
1926
|
-
this.
|
|
1990
|
+
const service = this.getDashboardServices().find((entry) => entry.id === selectedEntry.id);
|
|
1991
|
+
if (!service)
|
|
1992
|
+
break;
|
|
1993
|
+
if (service.pendingAction === "creating" || service.pendingAction === "starting") {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (service.status === "offline") {
|
|
1997
|
+
try {
|
|
1998
|
+
this.resumeOfflineServiceById(service.id);
|
|
1999
|
+
this.footerFlash = `◆ Started service ${service.label ?? service.id}`;
|
|
2000
|
+
this.footerFlashTicks = 3;
|
|
2001
|
+
this.renderDashboard();
|
|
2002
|
+
}
|
|
2003
|
+
catch (error) {
|
|
2004
|
+
this.showDashboardError("Failed to start service", [
|
|
2005
|
+
error instanceof Error ? error.message : String(error),
|
|
2006
|
+
]);
|
|
2007
|
+
}
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (this.openLiveTmuxWindowForService(selectedEntry.id) !== "missing") {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
1927
2013
|
break;
|
|
1928
2014
|
}
|
|
1929
2015
|
const dashEntry = this.dashboardState.worktreeSessions.find((entry) => entry.id === selectedEntry.id);
|
|
1930
2016
|
if (!dashEntry)
|
|
1931
2017
|
break;
|
|
1932
|
-
if (
|
|
2018
|
+
if (dashEntry.pendingAction === "creating" || dashEntry.pendingAction === "starting") {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (this.openLiveTmuxWindowForEntry(dashEntry) !== "missing") {
|
|
1933
2022
|
return;
|
|
1934
2023
|
}
|
|
1935
2024
|
if (dashEntry.remoteInstanceId) {
|
|
@@ -1938,7 +2027,7 @@ export class Multiplexer {
|
|
|
1938
2027
|
}
|
|
1939
2028
|
if (dashEntry.status === "offline") {
|
|
1940
2029
|
const offline = this.offlineSessions.find((s) => s.id === dashEntry.id);
|
|
1941
|
-
if (offline
|
|
2030
|
+
if (offline) {
|
|
1942
2031
|
void this.resumeOfflineSessionWithFeedback(offline);
|
|
1943
2032
|
}
|
|
1944
2033
|
return;
|
|
@@ -1968,10 +2057,10 @@ export class Multiplexer {
|
|
|
1968
2057
|
async activateDashboardEntry(entry) {
|
|
1969
2058
|
if (!entry)
|
|
1970
2059
|
return;
|
|
1971
|
-
if (entry.pendingAction === "creating") {
|
|
2060
|
+
if (entry.pendingAction === "creating" || entry.pendingAction === "starting") {
|
|
1972
2061
|
return;
|
|
1973
2062
|
}
|
|
1974
|
-
if (this.openLiveTmuxWindowForEntry(entry)) {
|
|
2063
|
+
if (this.openLiveTmuxWindowForEntry(entry) !== "missing") {
|
|
1975
2064
|
return;
|
|
1976
2065
|
}
|
|
1977
2066
|
if (entry.remoteInstanceId) {
|
|
@@ -1980,7 +2069,7 @@ export class Multiplexer {
|
|
|
1980
2069
|
}
|
|
1981
2070
|
if (entry.status === "offline") {
|
|
1982
2071
|
const offline = this.offlineSessions.find((session) => session.id === entry.id);
|
|
1983
|
-
if (offline
|
|
2072
|
+
if (offline) {
|
|
1984
2073
|
await this.resumeOfflineSessionWithFeedback(offline);
|
|
1985
2074
|
}
|
|
1986
2075
|
return;
|
|
@@ -2910,19 +2999,42 @@ export class Multiplexer {
|
|
|
2910
2999
|
return false;
|
|
2911
3000
|
}
|
|
2912
3001
|
openLiveTmuxWindowForEntry(entry) {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3002
|
+
try {
|
|
3003
|
+
const target = openManagedSessionWindow(this.tmuxRuntimeManager, process.cwd(), entry);
|
|
3004
|
+
if (!target)
|
|
3005
|
+
return "missing";
|
|
3006
|
+
this.agentTracker.markSeen(entry.id);
|
|
3007
|
+
updateNotificationContext("tui", {
|
|
3008
|
+
focused: true,
|
|
3009
|
+
sessionId: entry.id,
|
|
3010
|
+
panelOpen: false,
|
|
3011
|
+
});
|
|
3012
|
+
this.noteLastUsedItem(entry.id);
|
|
3013
|
+
return "opened";
|
|
3014
|
+
}
|
|
3015
|
+
catch (error) {
|
|
3016
|
+
this.showDashboardError("Failed to open agent", [
|
|
3017
|
+
error instanceof Error ? error.message : String(error),
|
|
3018
|
+
"The tmux window may still be starting. Try again in a moment.",
|
|
3019
|
+
]);
|
|
3020
|
+
return "error";
|
|
3021
|
+
}
|
|
2919
3022
|
}
|
|
2920
3023
|
openLiveTmuxWindowForService(serviceId) {
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3024
|
+
try {
|
|
3025
|
+
const target = openManagedServiceWindow(this.tmuxRuntimeManager, process.cwd(), serviceId);
|
|
3026
|
+
if (!target)
|
|
3027
|
+
return "missing";
|
|
3028
|
+
this.noteLastUsedItem(serviceId);
|
|
3029
|
+
return "opened";
|
|
3030
|
+
}
|
|
3031
|
+
catch (error) {
|
|
3032
|
+
this.showDashboardError("Failed to open service", [
|
|
3033
|
+
error instanceof Error ? error.message : String(error),
|
|
3034
|
+
"The tmux window may still be starting. Try again in a moment.",
|
|
3035
|
+
]);
|
|
3036
|
+
return "error";
|
|
3037
|
+
}
|
|
2926
3038
|
}
|
|
2927
3039
|
noteLastUsedItem(itemId) {
|
|
2928
3040
|
markLastUsed(process.cwd(), {
|
|
@@ -3715,8 +3827,30 @@ export class Multiplexer {
|
|
|
3715
3827
|
const cwd = worktreePath ?? process.cwd();
|
|
3716
3828
|
const shell = process.env.SHELL || "zsh";
|
|
3717
3829
|
const trimmed = commandLine.trim();
|
|
3718
|
-
|
|
3719
|
-
|
|
3830
|
+
let projectRoot = process.cwd();
|
|
3831
|
+
try {
|
|
3832
|
+
projectRoot = findMainRepo(cwd);
|
|
3833
|
+
}
|
|
3834
|
+
catch {
|
|
3835
|
+
projectRoot = process.cwd();
|
|
3836
|
+
}
|
|
3837
|
+
const wrapped = trimmed
|
|
3838
|
+
? wrapCommandWithShellIntegration({
|
|
3839
|
+
projectRoot,
|
|
3840
|
+
sessionId: serviceId,
|
|
3841
|
+
tool: "service",
|
|
3842
|
+
command: shell,
|
|
3843
|
+
args: ["-lc", trimmed],
|
|
3844
|
+
shellPath: shell,
|
|
3845
|
+
})
|
|
3846
|
+
: wrapInteractiveShellWithIntegration({
|
|
3847
|
+
projectRoot,
|
|
3848
|
+
sessionId: serviceId,
|
|
3849
|
+
tool: "service",
|
|
3850
|
+
shellPath: shell,
|
|
3851
|
+
});
|
|
3852
|
+
const command = wrapped.command;
|
|
3853
|
+
const args = wrapped.args;
|
|
3720
3854
|
const label = this.serviceLabelForCommand(trimmed);
|
|
3721
3855
|
const tmuxSession = this.tmuxRuntimeManager.ensureProjectSession(process.cwd());
|
|
3722
3856
|
const shouldRenderPending = this.startedInDashboard && this.mode === "dashboard";
|
|
@@ -3730,13 +3864,14 @@ export class Multiplexer {
|
|
|
3730
3864
|
this.tmuxRuntimeManager.setWindowMetadata(target, {
|
|
3731
3865
|
kind: "service",
|
|
3732
3866
|
sessionId: serviceId,
|
|
3733
|
-
command,
|
|
3734
|
-
args,
|
|
3867
|
+
command: trimmed ? shell : "shell",
|
|
3868
|
+
args: trimmed ? ["-lc", trimmed] : ["-l"],
|
|
3735
3869
|
toolConfigKey: "service",
|
|
3736
3870
|
worktreePath,
|
|
3737
3871
|
label,
|
|
3738
3872
|
});
|
|
3739
3873
|
this.tmuxRuntimeManager.applyManagedAgentWindowPolicy(target, "service");
|
|
3874
|
+
this.saveState();
|
|
3740
3875
|
this.invalidateDesktopStateSnapshot();
|
|
3741
3876
|
this.refreshLocalDashboardModel();
|
|
3742
3877
|
this.updateWorktreeSessions();
|
|
@@ -3759,12 +3894,112 @@ export class Multiplexer {
|
|
|
3759
3894
|
if (!match || match.metadata.kind !== "service") {
|
|
3760
3895
|
throw new Error(`Service "${serviceId}" not found`);
|
|
3761
3896
|
}
|
|
3897
|
+
const launchCommandLine = match.metadata.command === "shell"
|
|
3898
|
+
? ""
|
|
3899
|
+
: match.metadata.args?.[0] === "-lc"
|
|
3900
|
+
? (match.metadata.args[1] ?? "")
|
|
3901
|
+
: "";
|
|
3902
|
+
this.offlineServices = [
|
|
3903
|
+
...this.offlineServices.filter((service) => service.id !== serviceId),
|
|
3904
|
+
{
|
|
3905
|
+
id: serviceId,
|
|
3906
|
+
worktreePath: match.metadata.worktreePath,
|
|
3907
|
+
label: match.metadata.label,
|
|
3908
|
+
launchCommandLine,
|
|
3909
|
+
},
|
|
3910
|
+
];
|
|
3762
3911
|
this.tmuxRuntimeManager.killWindow(match.target);
|
|
3912
|
+
this.saveState();
|
|
3763
3913
|
this.invalidateDesktopStateSnapshot();
|
|
3764
3914
|
this.refreshLocalDashboardModel();
|
|
3765
3915
|
this.adjustAfterRemove(this.dashboardWorktreeGroupsCache.length > 0);
|
|
3766
3916
|
return { serviceId, status: "stopped" };
|
|
3767
3917
|
}
|
|
3918
|
+
removeOfflineService(serviceId) {
|
|
3919
|
+
this.offlineServices = this.offlineServices.filter((service) => service.id !== serviceId);
|
|
3920
|
+
const statePath = getStatePath();
|
|
3921
|
+
if (existsSync(statePath)) {
|
|
3922
|
+
try {
|
|
3923
|
+
const state = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
3924
|
+
state.services = (state.services ?? []).filter((service) => service.id !== serviceId);
|
|
3925
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
3926
|
+
}
|
|
3927
|
+
catch { }
|
|
3928
|
+
}
|
|
3929
|
+
this.saveState();
|
|
3930
|
+
this.invalidateDesktopStateSnapshot();
|
|
3931
|
+
this.refreshLocalDashboardModel();
|
|
3932
|
+
this.adjustAfterRemove(this.dashboardWorktreeGroupsCache.length > 0);
|
|
3933
|
+
return { serviceId, status: "removed" };
|
|
3934
|
+
}
|
|
3935
|
+
resumeOfflineService(service) {
|
|
3936
|
+
const existing = this.tmuxRuntimeManager.findManagedWindow(this.tmuxRuntimeManager.getProjectSession(process.cwd()).sessionName, {
|
|
3937
|
+
sessionId: service.id,
|
|
3938
|
+
});
|
|
3939
|
+
if (existing && existing.metadata.kind === "service") {
|
|
3940
|
+
this.offlineServices = this.offlineServices.filter((entry) => entry.id !== service.id);
|
|
3941
|
+
this.saveState();
|
|
3942
|
+
this.invalidateDesktopStateSnapshot();
|
|
3943
|
+
this.refreshLocalDashboardModel();
|
|
3944
|
+
return { serviceId: service.id, status: "running" };
|
|
3945
|
+
}
|
|
3946
|
+
const cwd = service.worktreePath ?? process.cwd();
|
|
3947
|
+
const shell = process.env.SHELL || "zsh";
|
|
3948
|
+
const launchCommandLine = service.launchCommandLine?.trim() ?? "";
|
|
3949
|
+
let projectRoot = process.cwd();
|
|
3950
|
+
try {
|
|
3951
|
+
projectRoot = findMainRepo(cwd);
|
|
3952
|
+
}
|
|
3953
|
+
catch {
|
|
3954
|
+
projectRoot = process.cwd();
|
|
3955
|
+
}
|
|
3956
|
+
const wrapped = launchCommandLine
|
|
3957
|
+
? wrapCommandWithShellIntegration({
|
|
3958
|
+
projectRoot,
|
|
3959
|
+
sessionId: service.id,
|
|
3960
|
+
tool: "service",
|
|
3961
|
+
command: shell,
|
|
3962
|
+
args: ["-lc", launchCommandLine],
|
|
3963
|
+
shellPath: shell,
|
|
3964
|
+
})
|
|
3965
|
+
: wrapInteractiveShellWithIntegration({
|
|
3966
|
+
projectRoot,
|
|
3967
|
+
sessionId: service.id,
|
|
3968
|
+
tool: "service",
|
|
3969
|
+
shellPath: shell,
|
|
3970
|
+
});
|
|
3971
|
+
const command = wrapped.command;
|
|
3972
|
+
const args = wrapped.args;
|
|
3973
|
+
const label = service.label ?? this.serviceLabelForCommand(launchCommandLine);
|
|
3974
|
+
const tmuxSession = this.tmuxRuntimeManager.ensureProjectSession(process.cwd());
|
|
3975
|
+
const target = this.tmuxRuntimeManager.createWindow(tmuxSession.sessionName, label, cwd, command, args, {
|
|
3976
|
+
detached: true,
|
|
3977
|
+
});
|
|
3978
|
+
this.tmuxRuntimeManager.setWindowMetadata(target, {
|
|
3979
|
+
kind: "service",
|
|
3980
|
+
sessionId: service.id,
|
|
3981
|
+
command: launchCommandLine ? shell : "shell",
|
|
3982
|
+
args: launchCommandLine ? ["-lc", launchCommandLine] : ["-l"],
|
|
3983
|
+
toolConfigKey: "service",
|
|
3984
|
+
worktreePath: service.worktreePath,
|
|
3985
|
+
label,
|
|
3986
|
+
});
|
|
3987
|
+
this.tmuxRuntimeManager.applyManagedAgentWindowPolicy(target, "service");
|
|
3988
|
+
this.offlineServices = this.offlineServices.filter((entry) => entry.id !== service.id);
|
|
3989
|
+
this.saveState();
|
|
3990
|
+
this.invalidateDesktopStateSnapshot();
|
|
3991
|
+
this.refreshLocalDashboardModel();
|
|
3992
|
+
this.updateWorktreeSessions();
|
|
3993
|
+
this.preferDashboardEntrySelection("service", service.id, service.worktreePath);
|
|
3994
|
+
return { serviceId: service.id, status: "running" };
|
|
3995
|
+
}
|
|
3996
|
+
resumeOfflineServiceById(serviceId) {
|
|
3997
|
+
const service = this.offlineServices.find((entry) => entry.id === serviceId);
|
|
3998
|
+
if (!service) {
|
|
3999
|
+
throw new Error(`Service "${serviceId}" not found`);
|
|
4000
|
+
}
|
|
4001
|
+
return this.resumeOfflineService(service);
|
|
4002
|
+
}
|
|
3768
4003
|
renderDashboard() {
|
|
3769
4004
|
this.writeStatuslineFile();
|
|
3770
4005
|
const { cols, rows } = this.getViewportSize();
|
|
@@ -5395,6 +5630,7 @@ export class Multiplexer {
|
|
|
5395
5630
|
syncSessionsFromState(state = Multiplexer.loadState()) {
|
|
5396
5631
|
this.restoreTmuxSessionsFromState(state);
|
|
5397
5632
|
this.loadOfflineSessions(state);
|
|
5633
|
+
this.loadOfflineServices(state);
|
|
5398
5634
|
this.invalidateDesktopStateSnapshot();
|
|
5399
5635
|
}
|
|
5400
5636
|
loadOfflineSessions(state = Multiplexer.loadState()) {
|
|
@@ -5434,12 +5670,57 @@ export class Multiplexer {
|
|
|
5434
5670
|
}
|
|
5435
5671
|
return previousKey !== nextKey;
|
|
5436
5672
|
}
|
|
5673
|
+
loadOfflineServices(state = Multiplexer.loadState()) {
|
|
5674
|
+
const savedServices = state?.services ?? [];
|
|
5675
|
+
if (savedServices.length === 0) {
|
|
5676
|
+
const changed = this.offlineServices.length > 0;
|
|
5677
|
+
this.offlineServices = [];
|
|
5678
|
+
return changed;
|
|
5679
|
+
}
|
|
5680
|
+
const liveServiceIds = new Set(this.tmuxRuntimeManager
|
|
5681
|
+
.listProjectManagedWindows(process.cwd())
|
|
5682
|
+
.filter(({ target, metadata }) => !isDashboardWindowName(target.windowName) && metadata.kind === "service")
|
|
5683
|
+
.map(({ metadata }) => metadata.sessionId));
|
|
5684
|
+
const nextOfflineServices = savedServices.filter((service) => {
|
|
5685
|
+
if (liveServiceIds.has(service.id))
|
|
5686
|
+
return false;
|
|
5687
|
+
if (service.worktreePath && !existsSync(service.worktreePath))
|
|
5688
|
+
return false;
|
|
5689
|
+
return true;
|
|
5690
|
+
});
|
|
5691
|
+
const previousKey = this.offlineServices
|
|
5692
|
+
.map((service) => `${service.id}:${service.label ?? ""}:${service.worktreePath ?? ""}:${service.launchCommandLine ?? ""}`)
|
|
5693
|
+
.join("|");
|
|
5694
|
+
const nextKey = nextOfflineServices
|
|
5695
|
+
.map((service) => `${service.id}:${service.label ?? ""}:${service.worktreePath ?? ""}:${service.launchCommandLine ?? ""}`)
|
|
5696
|
+
.join("|");
|
|
5697
|
+
this.offlineServices = nextOfflineServices;
|
|
5698
|
+
return previousKey !== nextKey;
|
|
5699
|
+
}
|
|
5700
|
+
buildLiveServiceStates() {
|
|
5701
|
+
const seen = new Set();
|
|
5702
|
+
const liveServices = [];
|
|
5703
|
+
for (const { metadata } of this.tmuxRuntimeManager.listProjectManagedWindows(process.cwd())) {
|
|
5704
|
+
if (metadata.kind !== "service")
|
|
5705
|
+
continue;
|
|
5706
|
+
if (seen.has(metadata.sessionId))
|
|
5707
|
+
continue;
|
|
5708
|
+
seen.add(metadata.sessionId);
|
|
5709
|
+
const launchCommandLine = metadata.command === "shell" ? "" : metadata.args?.[0] === "-lc" ? (metadata.args[1] ?? "") : "";
|
|
5710
|
+
liveServices.push({
|
|
5711
|
+
id: metadata.sessionId,
|
|
5712
|
+
worktreePath: metadata.worktreePath,
|
|
5713
|
+
label: metadata.label,
|
|
5714
|
+
launchCommandLine,
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
5717
|
+
return liveServices;
|
|
5718
|
+
}
|
|
5437
5719
|
restoreTmuxSessionsFromState(state = Multiplexer.loadState()) {
|
|
5438
5720
|
const savedById = new Map((state?.sessions ?? []).map((session) => [session.id, session]));
|
|
5439
5721
|
const cols = process.stdout.columns ?? 80;
|
|
5440
5722
|
const rows = process.stdout.rows ?? 24;
|
|
5441
|
-
const
|
|
5442
|
-
for (const { target, metadata } of this.tmuxRuntimeManager.listManagedWindows(tmuxSession.sessionName)) {
|
|
5723
|
+
for (const { target, metadata } of this.tmuxRuntimeManager.listProjectManagedWindows(process.cwd())) {
|
|
5443
5724
|
if (isDashboardWindowName(target.windowName))
|
|
5444
5725
|
continue;
|
|
5445
5726
|
if (metadata.kind === "service")
|
|
@@ -5687,11 +5968,14 @@ export class Multiplexer {
|
|
|
5687
5968
|
tmuxTarget: this.sessionTmuxTargets.get(s.id),
|
|
5688
5969
|
}));
|
|
5689
5970
|
const mySessions = [...this.offlineSessions, ...liveSessions];
|
|
5690
|
-
|
|
5971
|
+
const liveServices = this.buildLiveServiceStates();
|
|
5972
|
+
const myServices = [...this.offlineServices, ...liveServices].filter((service, index, services) => services.findIndex((entry) => entry.id === service.id) === index);
|
|
5973
|
+
if (mySessions.length === 0 && myServices.length === 0)
|
|
5691
5974
|
return;
|
|
5692
5975
|
// Merge with existing state (other instances may have written their sessions)
|
|
5693
5976
|
const statePath = Multiplexer.getSharedStatePath();
|
|
5694
5977
|
let mergedSessions = mySessions;
|
|
5978
|
+
let mergedServices = myServices;
|
|
5695
5979
|
if (existsSync(statePath)) {
|
|
5696
5980
|
try {
|
|
5697
5981
|
const existing = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
@@ -5706,6 +5990,9 @@ export class Multiplexer {
|
|
|
5706
5990
|
return true;
|
|
5707
5991
|
});
|
|
5708
5992
|
mergedSessions = [...otherSessions, ...mySessions];
|
|
5993
|
+
const myServiceIds = new Set(myServices.map((service) => service.id));
|
|
5994
|
+
const otherServices = (existing.services ?? []).filter((service) => !myServiceIds.has(service.id));
|
|
5995
|
+
mergedServices = [...otherServices, ...myServices];
|
|
5709
5996
|
}
|
|
5710
5997
|
catch {
|
|
5711
5998
|
// Corrupt file — just overwrite with ours
|
|
@@ -5715,6 +6002,7 @@ export class Multiplexer {
|
|
|
5715
6002
|
savedAt: new Date().toISOString(),
|
|
5716
6003
|
cwd: process.cwd(),
|
|
5717
6004
|
sessions: mergedSessions,
|
|
6005
|
+
services: mergedServices,
|
|
5718
6006
|
};
|
|
5719
6007
|
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
5720
6008
|
this.invalidateDesktopStateSnapshot();
|