tmux-ide 2.9.0-beta.22 → 2.9.0-beta.26
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/bin/cli.js +22 -3
- package/package.json +2 -2
- package/packages/contracts/src/fleet-lifecycle.ts +5 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +6 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +15 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +21 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- package/packages/daemon/src/command-center/tmux-servers.ts +11 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +17 -1
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +22 -0
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +16 -16
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -0
- package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
package/bin/cli.js
CHANGED
|
@@ -4960,6 +4960,7 @@ var SafeDisplayNameSchemaZ, OwnerPathSchemaZ, WorkspaceSessionCreateArgumentsSch
|
|
|
4960
4960
|
var init_fleet_lifecycle = __esm({
|
|
4961
4961
|
"packages/contracts/src/fleet-lifecycle.ts"() {
|
|
4962
4962
|
"use strict";
|
|
4963
|
+
init_workspace_catalog_resource();
|
|
4963
4964
|
init_fleet_catalog();
|
|
4964
4965
|
init_desktop_workspace_name();
|
|
4965
4966
|
init_semantic_identity();
|
|
@@ -4973,6 +4974,8 @@ var init_fleet_lifecycle = __esm({
|
|
|
4973
4974
|
OwnerPathSchemaZ = /* @__PURE__ */ (() => z34.string().min(1).max(4096).refine((value) => !value.includes("\0")))();
|
|
4974
4975
|
WorkspaceSessionCreateArgumentsSchemaZ = /* @__PURE__ */ (() => z34.object({
|
|
4975
4976
|
displayName: SafeDisplayNameSchemaZ,
|
|
4977
|
+
/** Opt in to an incarnation-bearing receipt; legacy clients keep their strict result shape. */
|
|
4978
|
+
includeLiveSessionId: z34.boolean().optional(),
|
|
4976
4979
|
cwd: OwnerPathSchemaZ.optional(),
|
|
4977
4980
|
expectedDaemonInstanceId: z34.uuid().optional()
|
|
4978
4981
|
}).strict())();
|
|
@@ -4980,6 +4983,8 @@ var init_fleet_lifecycle = __esm({
|
|
|
4980
4983
|
operationId: z34.uuid(),
|
|
4981
4984
|
daemonInstanceId: z34.uuid(),
|
|
4982
4985
|
outcome: z34.enum(["created", "adopted", "replayed"]),
|
|
4986
|
+
/** Captured atomically by new-session; absent from older daemons and adopted routes. */
|
|
4987
|
+
liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ.optional(),
|
|
4983
4988
|
fleetSessionId: FleetSessionIdSchemaZ,
|
|
4984
4989
|
/** Canonical registered route; displayName is presentation only. */
|
|
4985
4990
|
workspaceName: DesktopWorkspaceNameSchemaZ,
|
|
@@ -11274,7 +11279,7 @@ var require_package = __commonJS({
|
|
|
11274
11279
|
"package.json"(exports, module) {
|
|
11275
11280
|
module.exports = {
|
|
11276
11281
|
name: "tmux-ide",
|
|
11277
|
-
version: "2.9.0-beta.
|
|
11282
|
+
version: "2.9.0-beta.26",
|
|
11278
11283
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
11279
11284
|
type: "module",
|
|
11280
11285
|
bin: {
|
|
@@ -11338,7 +11343,7 @@ var require_package = __commonJS({
|
|
|
11338
11343
|
postinstall: "node scripts/postinstall.js",
|
|
11339
11344
|
docs: "turbo run dev --filter=@tmux-ide/docs",
|
|
11340
11345
|
"demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
|
|
11341
|
-
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
|
|
11346
|
+
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/guided-tour-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
|
|
11342
11347
|
"test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
|
|
11343
11348
|
"test:tui-live": "node scripts/tui-testdrive.mjs smoke",
|
|
11344
11349
|
"test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
|
|
@@ -24855,6 +24860,7 @@ var MAX_REPLAY_OPERATIONS, sleep2, FleetLifecycleAuthorityError, FleetLifecycleA
|
|
|
24855
24860
|
var init_fleet_lifecycle_authority = __esm({
|
|
24856
24861
|
"packages/daemon/src/lib/fleet-lifecycle-authority.ts"() {
|
|
24857
24862
|
"use strict";
|
|
24863
|
+
init_live_session_identity();
|
|
24858
24864
|
init_src();
|
|
24859
24865
|
init_discovery();
|
|
24860
24866
|
init_fleet_catalog2();
|
|
@@ -24934,10 +24940,14 @@ var init_fleet_lifecycle_authority = __esm({
|
|
|
24934
24940
|
}
|
|
24935
24941
|
}
|
|
24936
24942
|
let created = false;
|
|
24943
|
+
let liveSessionId;
|
|
24937
24944
|
try {
|
|
24938
|
-
this.#runTmux([
|
|
24945
|
+
const createdId = this.#runTmux([
|
|
24939
24946
|
"new-session",
|
|
24940
24947
|
"-d",
|
|
24948
|
+
"-P",
|
|
24949
|
+
"-F",
|
|
24950
|
+
"#{pid} #{session_id} #{session_created}",
|
|
24941
24951
|
...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
|
|
24942
24952
|
"-s",
|
|
24943
24953
|
identity2.sessionName,
|
|
@@ -24947,6 +24957,9 @@ var init_fleet_lifecycle_authority = __esm({
|
|
|
24947
24957
|
cwd,
|
|
24948
24958
|
TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND
|
|
24949
24959
|
]);
|
|
24960
|
+
const [serverPid, sessionId, sessionCreated] = createdId.trim().split(" ");
|
|
24961
|
+
if (serverPid && /^\d+$/u.test(serverPid) && sessionId && /^\$\d+$/u.test(sessionId) && sessionCreated && /^\d+$/u.test(sessionCreated))
|
|
24962
|
+
liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
|
|
24950
24963
|
created = true;
|
|
24951
24964
|
prepareTmuxTruecolorEnvironment(this.#runTmux, identity2.sessionName);
|
|
24952
24965
|
this.#runTmux(["set-environment", "-t", identity2.sessionName, "TMUX_IDE", "1"]);
|
|
@@ -24980,6 +24993,7 @@ var init_fleet_lifecycle_authority = __esm({
|
|
|
24980
24993
|
operationId,
|
|
24981
24994
|
daemonInstanceId: this.#daemonInstanceId,
|
|
24982
24995
|
outcome: "created",
|
|
24996
|
+
...input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {},
|
|
24983
24997
|
fleetSessionId: fleetSessionIdForName(identity2.sessionName),
|
|
24984
24998
|
workspaceName: identity2.workspaceName,
|
|
24985
24999
|
displayName: input.displayName
|
|
@@ -71501,6 +71515,11 @@ function mountTmuxServerRoutes(app, options) {
|
|
|
71501
71515
|
workspace.sessionName,
|
|
71502
71516
|
c.req.raw.signal
|
|
71503
71517
|
);
|
|
71518
|
+
const currentSessions = await owner.catalog();
|
|
71519
|
+
if (!currentSessions.some(
|
|
71520
|
+
(row) => row.sessionName === workspace.sessionName && row.liveSessionId === c.req.query("liveSessionId")
|
|
71521
|
+
))
|
|
71522
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
71504
71523
|
return snapshot2 ? projectApplicationShellResource(snapshot2) : null;
|
|
71505
71524
|
});
|
|
71506
71525
|
return resource3 ? c.json({ version: 1, server, resource: resource3 }) : c.json({ error: { code: "workspace-not-found" } }, 404);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "2.9.0-beta.
|
|
3
|
+
"version": "2.9.0-beta.26",
|
|
4
4
|
"description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"postinstall": "node scripts/postinstall.js",
|
|
65
65
|
"docs": "turbo run dev --filter=@tmux-ide/docs",
|
|
66
66
|
"demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
|
|
67
|
-
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
|
|
67
|
+
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/guided-tour-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
|
|
68
68
|
"test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
|
|
69
69
|
"test:tui-live": "node scripts/tui-testdrive.mjs smoke",
|
|
70
70
|
"test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorkspaceCatalogLiveSessionIdSchemaZ } from "./workspace-catalog-resource.ts";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import {
|
|
3
4
|
FleetAgentIdSchemaZ,
|
|
@@ -29,6 +30,8 @@ const OwnerPathSchemaZ = z
|
|
|
29
30
|
export const WorkspaceSessionCreateArgumentsSchemaZ = z
|
|
30
31
|
.object({
|
|
31
32
|
displayName: SafeDisplayNameSchemaZ,
|
|
33
|
+
/** Opt in to an incarnation-bearing receipt; legacy clients keep their strict result shape. */
|
|
34
|
+
includeLiveSessionId: z.boolean().optional(),
|
|
32
35
|
cwd: OwnerPathSchemaZ.optional(),
|
|
33
36
|
expectedDaemonInstanceId: z.uuid().optional(),
|
|
34
37
|
})
|
|
@@ -42,6 +45,8 @@ export const WorkspaceSessionCreateResultSchemaZ = z
|
|
|
42
45
|
operationId: z.uuid(),
|
|
43
46
|
daemonInstanceId: z.uuid(),
|
|
44
47
|
outcome: z.enum(["created", "adopted", "replayed"]),
|
|
48
|
+
/** Captured atomically by new-session; absent from older daemons and adopted routes. */
|
|
49
|
+
liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ.optional(),
|
|
45
50
|
fleetSessionId: FleetSessionIdSchemaZ,
|
|
46
51
|
/** Canonical registered route; displayName is presentation only. */
|
|
47
52
|
workspaceName: DesktopWorkspaceNameSchemaZ,
|
|
@@ -182,6 +182,12 @@ export function mountTmuxServerRoutes(app, options) {
|
|
|
182
182
|
row.liveSessionId === c.req.query("liveSessionId")))
|
|
183
183
|
throw new TmuxServerScopeError("stale-generation");
|
|
184
184
|
const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(workspace.sessionName, c.req.raw.signal);
|
|
185
|
+
// Discovery can yield while a same-name session is replaced. Revalidate
|
|
186
|
+
// the captured incarnation before publishing agent metadata as current.
|
|
187
|
+
const currentSessions = await owner.catalog();
|
|
188
|
+
if (!currentSessions.some((row) => row.sessionName === workspace.sessionName &&
|
|
189
|
+
row.liveSessionId === c.req.query("liveSessionId")))
|
|
190
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
185
191
|
return snapshot ? projectApplicationShellResource(snapshot) : null;
|
|
186
192
|
});
|
|
187
193
|
return resource
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { realpath, stat } from "node:fs/promises";
|
|
@@ -79,10 +80,14 @@ export class FleetLifecycleAuthority {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
let created = false;
|
|
83
|
+
let liveSessionId;
|
|
82
84
|
try {
|
|
83
|
-
this.#runTmux([
|
|
85
|
+
const createdId = this.#runTmux([
|
|
84
86
|
"new-session",
|
|
85
87
|
"-d",
|
|
88
|
+
"-P",
|
|
89
|
+
"-F",
|
|
90
|
+
"#{pid}\t#{session_id}\t#{session_created}",
|
|
86
91
|
...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
|
|
87
92
|
"-s",
|
|
88
93
|
identity.sessionName,
|
|
@@ -92,6 +97,14 @@ export class FleetLifecycleAuthority {
|
|
|
92
97
|
cwd,
|
|
93
98
|
TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND,
|
|
94
99
|
]);
|
|
100
|
+
const [serverPid, sessionId, sessionCreated] = createdId.trim().split("\t");
|
|
101
|
+
if (serverPid &&
|
|
102
|
+
/^\d+$/u.test(serverPid) &&
|
|
103
|
+
sessionId &&
|
|
104
|
+
/^\$\d+$/u.test(sessionId) &&
|
|
105
|
+
sessionCreated &&
|
|
106
|
+
/^\d+$/u.test(sessionCreated))
|
|
107
|
+
liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
|
|
95
108
|
created = true;
|
|
96
109
|
prepareTmuxTruecolorEnvironment(this.#runTmux, identity.sessionName);
|
|
97
110
|
this.#runTmux(["set-environment", "-t", identity.sessionName, "TMUX_IDE", "1"]);
|
|
@@ -129,6 +142,7 @@ export class FleetLifecycleAuthority {
|
|
|
129
142
|
operationId,
|
|
130
143
|
daemonInstanceId: this.#daemonInstanceId,
|
|
131
144
|
outcome: "created",
|
|
145
|
+
...(input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {}),
|
|
132
146
|
fleetSessionId: fleetSessionIdForName(identity.sessionName),
|
|
133
147
|
workspaceName: identity.workspaceName,
|
|
134
148
|
displayName: input.displayName,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createEffect, createMemo, Show } from "solid-js";
|
|
4
|
+
import { createApplicationGuidedTourOwner } from "./application-guided-tour-owner.js";
|
|
5
|
+
import { GuidedTourCoach } from "./guided-tour-coach.jsx";
|
|
6
|
+
import { createFleetSession } from "./fleet-lifecycle-client.js";
|
|
7
|
+
import { applicationMachineAuthorityManager } from "./application-machine-authority.js";
|
|
8
|
+
export function createApplicationGuidedTourIntegration(options) {
|
|
9
|
+
const { machines, lifecycle, generation, generationMachineId, layoutSnapshot, sessionName, focusedPane, activeSurface, appearance, paletteOpen, paletteCommands, dimensions, blocked, } = options;
|
|
10
|
+
const practiceRows = () => machines.catalog
|
|
11
|
+
.getSnapshot()
|
|
12
|
+
.groups.filter((group) => group.id === "local" || group.routeIds?.includes("local"))
|
|
13
|
+
.flatMap((group) => group.sessions)
|
|
14
|
+
.filter((row) => !row.disabled);
|
|
15
|
+
const practiceIdentity = (row) => {
|
|
16
|
+
const daemon = applicationMachineAuthorityManager.getMachine("local")?.read();
|
|
17
|
+
if (!row.liveSessionId || !daemon)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
machineId: "local",
|
|
21
|
+
serverId: row.server?.serverId ?? "default",
|
|
22
|
+
generation: row.server?.generation ?? daemon.instanceId,
|
|
23
|
+
sessionId: row.liveSessionId,
|
|
24
|
+
sessionName: row.name,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const tour = createApplicationGuidedTourOwner({
|
|
28
|
+
async createPractice() {
|
|
29
|
+
const handle = applicationMachineAuthorityManager.getMachine("local");
|
|
30
|
+
if (!handle || handle.endpoint().state !== "ready")
|
|
31
|
+
throw new Error("Connect this machine before creating a practice session.");
|
|
32
|
+
const created = await createFleetSession(handle, `tmux-ide-practice-${randomUUID().slice(0, 8)}`, undefined, { includeLiveSessionId: true });
|
|
33
|
+
if (!created || created.outcome !== "created")
|
|
34
|
+
throw new Error("Could not create a fresh practice session. Try again.");
|
|
35
|
+
if (!created.liveSessionId)
|
|
36
|
+
throw new Error("Update the local tmux-ide daemon to use the guided practice session, then restart the tour.");
|
|
37
|
+
return await new Promise((resolve, reject) => {
|
|
38
|
+
let done = false;
|
|
39
|
+
let stop = () => { };
|
|
40
|
+
const abort = () => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
stop();
|
|
43
|
+
reject(new Error("Tour closed."));
|
|
44
|
+
};
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
lifecycle.signal.removeEventListener("abort", abort);
|
|
47
|
+
stop();
|
|
48
|
+
reject(new Error("Practice session was created but has not appeared yet. Find it in the local session list."));
|
|
49
|
+
}, 10_000);
|
|
50
|
+
lifecycle.signal.addEventListener("abort", abort, { once: true });
|
|
51
|
+
const check = () => {
|
|
52
|
+
const rows = practiceRows().filter((row) => row.name === created.workspaceName && row.liveSessionId === created.liveSessionId);
|
|
53
|
+
const identity = rows.length === 1 ? practiceIdentity(rows[0]) : null;
|
|
54
|
+
if (!identity || identity.generation !== created.daemonInstanceId)
|
|
55
|
+
return;
|
|
56
|
+
done = true;
|
|
57
|
+
lifecycle.signal.removeEventListener("abort", abort);
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
stop();
|
|
60
|
+
resolve(identity);
|
|
61
|
+
};
|
|
62
|
+
stop = machines.catalog.subscribe(check);
|
|
63
|
+
if (done)
|
|
64
|
+
stop();
|
|
65
|
+
else if (lifecycle.signal.aborted)
|
|
66
|
+
abort();
|
|
67
|
+
else
|
|
68
|
+
check();
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
openPractice(practice) {
|
|
72
|
+
if (practice.machineId !== "local")
|
|
73
|
+
throw new Error("Practice sessions must belong to this machine. Restart the tour.");
|
|
74
|
+
const row = practiceRows().find((row) => {
|
|
75
|
+
const identity = practiceIdentity(row);
|
|
76
|
+
return (identity &&
|
|
77
|
+
identity.serverId === practice.serverId &&
|
|
78
|
+
identity.generation === practice.generation &&
|
|
79
|
+
identity.sessionId === practice.sessionId);
|
|
80
|
+
});
|
|
81
|
+
if (!row)
|
|
82
|
+
throw new Error("That practice session is unavailable or has been replaced. Replay the tour to create a new one.");
|
|
83
|
+
machines.sidebar.onOpen("local", row.name, "mouse", row.id);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const committedTourTheme = createMemo((previous) => appearance.pickerOpen()
|
|
87
|
+
? (previous ?? JSON.stringify(appearance.theme().canonical))
|
|
88
|
+
: JSON.stringify(appearance.theme().canonical));
|
|
89
|
+
createEffect(() => {
|
|
90
|
+
const current = generation();
|
|
91
|
+
const connection = current?.status === "live" ? current.connection : null;
|
|
92
|
+
const machineId = generationMachineId();
|
|
93
|
+
const practice = connection?.liveSessionId && current?.daemonGeneration && machineId
|
|
94
|
+
? {
|
|
95
|
+
machineId,
|
|
96
|
+
serverId: connection.server?.serverId ?? "default",
|
|
97
|
+
generation: connection.server?.generation ?? current.daemonGeneration,
|
|
98
|
+
sessionId: connection.liveSessionId,
|
|
99
|
+
sessionName: sessionName() ?? "",
|
|
100
|
+
}
|
|
101
|
+
: null;
|
|
102
|
+
tour.observe({
|
|
103
|
+
practice,
|
|
104
|
+
panes: (layoutSnapshot().current?.panes ?? []).flatMap((pane) => pane.pane ? [{ id: pane.pane, width: pane.width, height: pane.height }] : []),
|
|
105
|
+
focusedPane: focusedPane(),
|
|
106
|
+
surface: activeSurface(),
|
|
107
|
+
paletteOpen: paletteOpen(),
|
|
108
|
+
theme: committedTourTheme(),
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
...tour,
|
|
113
|
+
Coach: () => (<Show when={tour.state().active && !blocked()}>
|
|
114
|
+
<box position="absolute" right={1} bottom={2} width={Math.max(1, Math.min(68, dimensions().width - 2))} zIndex={30}>
|
|
115
|
+
<GuidedTourCoach height={Math.max(4, Math.min(10, dimensions().height - 4))} state={tour.state()} theme={appearance.theme()} width={Math.max(1, Math.min(68, dimensions().width - 2))} shortcuts={{ commands: "F5", home: "F1" }} error={tour.error()} busy={tour.busy()} onPause={tour.pause} onReplay={tour.replay} onWelcomeRead={tour.welcomeRead} onCreatePractice={() => void tour.createPractice()} onOpenCommands={() => paletteCommands.setOpen(true, "mouse")} onOpenAppearance={appearance.openPicker} onOpenHome={() => paletteCommands.openSurface("home", "mouse")} onOpenPractice={() => void tour.openPractice()}/>
|
|
116
|
+
</box>
|
|
117
|
+
</Show>),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { initialGuidedTourState, reduceGuidedTour, sameGuidedTourPractice, } from "./guided-tour.js";
|
|
3
|
+
import { readGuidedTourState, writeGuidedTourState } from "./guided-tour-storage.js";
|
|
4
|
+
/** Compare confirmed snapshots; unrelated sessions and request attempts never count. */
|
|
5
|
+
export function observedGuidedTourEvent(state, previous, current) {
|
|
6
|
+
if (!state.active || !previous)
|
|
7
|
+
return null;
|
|
8
|
+
if (state.step === "commands" && current.paletteOpen && !previous.paletteOpen)
|
|
9
|
+
return { type: "commands-opened" };
|
|
10
|
+
if (state.step === "theme" && current.theme !== previous.theme)
|
|
11
|
+
return { type: "theme-changed" };
|
|
12
|
+
if (state.step === "leave" && current.surface === "home")
|
|
13
|
+
return { type: "home-opened" };
|
|
14
|
+
if (!current.practice ||
|
|
15
|
+
!sameGuidedTourPractice(state.practice, current.practice) ||
|
|
16
|
+
current.surface !== "terminals")
|
|
17
|
+
return null;
|
|
18
|
+
if (state.step === "reopen" &&
|
|
19
|
+
(previous.surface !== "terminals" ||
|
|
20
|
+
!sameGuidedTourPractice(previous.practice, current.practice)))
|
|
21
|
+
return { type: "reopened", practice: current.practice };
|
|
22
|
+
if (!previous.practice || !sameGuidedTourPractice(previous.practice, current.practice))
|
|
23
|
+
return null;
|
|
24
|
+
if (state.step === "split" &&
|
|
25
|
+
previous.panes.length > 0 &&
|
|
26
|
+
current.panes.length > previous.panes.length &&
|
|
27
|
+
previous.panes.every((old) => current.panes.some((p) => p.id === old.id)) &&
|
|
28
|
+
current.panes.some((p) => !previous.panes.some((old) => old.id === p.id)))
|
|
29
|
+
return { type: "split-confirmed", practice: current.practice };
|
|
30
|
+
if (state.step === "focus" &&
|
|
31
|
+
previous.focusedPane &&
|
|
32
|
+
current.focusedPane &&
|
|
33
|
+
previous.focusedPane !== current.focusedPane &&
|
|
34
|
+
current.panes.some((p) => p.id === current.focusedPane) &&
|
|
35
|
+
current.panes.some((p) => p.id === previous.focusedPane))
|
|
36
|
+
return { type: "focus-confirmed", practice: current.practice };
|
|
37
|
+
// A divider reallocates space between the same panes: one grows while
|
|
38
|
+
// another shrinks on the same axis. Whole-app growth/shrink is not practice.
|
|
39
|
+
const samePanes = current.panes.length >= 2 &&
|
|
40
|
+
current.panes.length === previous.panes.length &&
|
|
41
|
+
previous.panes.every((old) => current.panes.some((pane) => pane.id === old.id));
|
|
42
|
+
const reallocated = ["width", "height"].some((axis) => {
|
|
43
|
+
const changes = current.panes.map((pane) => pane[axis] - (previous.panes.find((old) => old.id === pane.id)?.[axis] ?? pane[axis]));
|
|
44
|
+
return changes.some((change) => change > 0) && changes.some((change) => change < 0);
|
|
45
|
+
});
|
|
46
|
+
if (state.step === "resize" && samePanes && reallocated)
|
|
47
|
+
return { type: "resize-confirmed", practice: current.practice };
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export function createApplicationGuidedTourOwner(options) {
|
|
51
|
+
const [state, setState] = createSignal((options.read ?? readGuidedTourState)());
|
|
52
|
+
const [busy, setBusy] = createSignal(false);
|
|
53
|
+
const [error, setError] = createSignal(null);
|
|
54
|
+
let previous = null;
|
|
55
|
+
let disposed = false;
|
|
56
|
+
onCleanup(() => {
|
|
57
|
+
disposed = true;
|
|
58
|
+
});
|
|
59
|
+
const send = (event) => {
|
|
60
|
+
const next = reduceGuidedTour(state(), event);
|
|
61
|
+
if (next === state())
|
|
62
|
+
return;
|
|
63
|
+
setState(next);
|
|
64
|
+
if (!(options.write ?? writeGuidedTourState)(next))
|
|
65
|
+
setError("Progress could not be saved. You can still continue this tour.");
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
state,
|
|
69
|
+
busy,
|
|
70
|
+
error,
|
|
71
|
+
label: () => state().step === "complete"
|
|
72
|
+
? "Replay walkthrough"
|
|
73
|
+
: state().step === initialGuidedTourState().step
|
|
74
|
+
? "Learn tmux-ide"
|
|
75
|
+
: "Resume walkthrough",
|
|
76
|
+
open() {
|
|
77
|
+
previous = null;
|
|
78
|
+
send({ type: state().step === "complete" ? "replay" : "resume" });
|
|
79
|
+
},
|
|
80
|
+
replay() {
|
|
81
|
+
previous = null;
|
|
82
|
+
send({ type: "replay" });
|
|
83
|
+
},
|
|
84
|
+
pause() {
|
|
85
|
+
send({ type: "pause" });
|
|
86
|
+
},
|
|
87
|
+
welcomeRead() {
|
|
88
|
+
send({ type: "welcome-read" });
|
|
89
|
+
},
|
|
90
|
+
observe(current) {
|
|
91
|
+
const event = observedGuidedTourEvent(state(), previous, current);
|
|
92
|
+
previous = current;
|
|
93
|
+
if (event)
|
|
94
|
+
send(event);
|
|
95
|
+
},
|
|
96
|
+
async createPractice() {
|
|
97
|
+
if (busy() || state().step !== "practice")
|
|
98
|
+
return;
|
|
99
|
+
setBusy(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
try {
|
|
102
|
+
const practice = await options.createPractice();
|
|
103
|
+
if (disposed)
|
|
104
|
+
return;
|
|
105
|
+
send({ type: "practice-created", practice });
|
|
106
|
+
if (state().active)
|
|
107
|
+
await options.openPractice(practice);
|
|
108
|
+
}
|
|
109
|
+
catch (cause) {
|
|
110
|
+
if (!disposed)
|
|
111
|
+
setError(cause instanceof Error ? cause.message : "Could not create the practice session.");
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (!disposed)
|
|
115
|
+
setBusy(false);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async openPractice() {
|
|
119
|
+
const practice = state().practice;
|
|
120
|
+
if (!practice || busy())
|
|
121
|
+
return;
|
|
122
|
+
setError(null);
|
|
123
|
+
try {
|
|
124
|
+
if (state().active)
|
|
125
|
+
await options.openPractice(practice);
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
setError(cause instanceof Error ? cause.message : "Practice session is unavailable.");
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -23,7 +23,8 @@ export function HomeAgentRoster(props) {
|
|
|
23
23
|
const [hovered, setHovered] = createSignal(null);
|
|
24
24
|
const width = () => Math.max(0, Math.floor(props.width));
|
|
25
25
|
const height = () => Math.max(0, Math.floor(props.height));
|
|
26
|
-
const stale = (row) =>
|
|
26
|
+
const stale = (row) => row.disabled ||
|
|
27
|
+
(props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
|
|
27
28
|
(props.snapshot.unavailableSessionKeys ?? []).includes(row.sessionKey);
|
|
28
29
|
const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
|
|
29
30
|
props.onRetry) ||
|
|
@@ -50,8 +51,8 @@ export function HomeAgentRoster(props) {
|
|
|
50
51
|
if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
|
|
51
52
|
return "No agents reported";
|
|
52
53
|
const count = props.snapshot.rows.length;
|
|
53
|
-
const attention = props.snapshot.rows.filter((row) => row.attention || row.activity === "waiting" || row.activity === "failed").length;
|
|
54
|
-
const working = props.snapshot.rows.filter((row) => row.activity === "running").length;
|
|
54
|
+
const attention = props.snapshot.rows.filter((row) => !stale(row) && (row.attention || row.activity === "waiting" || row.activity === "failed")).length;
|
|
55
|
+
const working = props.snapshot.rows.filter((row) => !stale(row) && row.activity === "running").length;
|
|
55
56
|
return `${count} observed ${count === 1 ? "agent" : "agents"} · ${attention} ${attention === 1 ? "needs" : "need"} attention · ${working} working`;
|
|
56
57
|
};
|
|
57
58
|
const coverage = () => `Scope: ${props.snapshot.observedSessions} of ${props.snapshot.totalSessions} sessions observed${props.snapshot.phase === "partial" ? " · partial" : ""}${props.snapshot.loadingSessions ? ` · ${props.snapshot.loadingSessions} loading` : ""}${props.snapshot.unavailableSessions ? ` · ${props.snapshot.unavailableSessions} unavailable` : ""}${props.snapshot.truncatedSessions ? ` · ${props.snapshot.truncatedSessions} not loaded` : ""}`;
|
|
@@ -69,15 +70,22 @@ export function HomeAgentRoster(props) {
|
|
|
69
70
|
return "Enlarge the terminal to view agents.";
|
|
70
71
|
const selected = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
|
|
71
72
|
if (selected && stale(selected))
|
|
72
|
-
return `${selected.sessionName} · last observed; waiting for fresh signals`;
|
|
73
|
-
if (width() < 44 && selected)
|
|
74
|
-
return `${selected.
|
|
73
|
+
return `${[selected.machineLabel, selected.serverLabel, selected.sessionName].filter(Boolean).join(" / ")} · last observed; waiting for fresh signals`;
|
|
74
|
+
if (selected?.machineLabel || (width() < 44 && selected))
|
|
75
|
+
return `${[selected?.machineLabel, selected?.serverLabel, selected?.sessionName].filter(Boolean).join(" / ")} · Enter open`;
|
|
75
76
|
return `${offset() + 1}–${Math.min(props.snapshot.rows.length, offset() + visibleCount())} of ${props.snapshot.rows.length} · ↑↓ select · Enter open`;
|
|
76
77
|
};
|
|
77
78
|
useKeyboardRoute((event) => {
|
|
78
79
|
if (!props.inputActive || event.eventType !== "press" || event.ctrl || event.meta)
|
|
79
80
|
return false;
|
|
80
81
|
const key = event.name.toLowerCase();
|
|
82
|
+
const filter = key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined;
|
|
83
|
+
if (filter) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
filter();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
81
89
|
const retry = key === "r" &&
|
|
82
90
|
(props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
|
|
83
91
|
props.onRetry;
|
|
@@ -120,10 +128,12 @@ export function HomeAgentRoster(props) {
|
|
|
120
128
|
<text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
|
|
121
129
|
{clipTerminal(coverage(), width())}
|
|
122
130
|
</text>
|
|
123
|
-
<
|
|
131
|
+
<text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
|
|
132
|
+
{clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine · a attention` : "", width())}
|
|
133
|
+
</text>
|
|
124
134
|
<Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() + 1)} flexShrink={0}/>}>
|
|
125
135
|
<text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
|
|
126
|
-
{clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells("SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
|
|
136
|
+
{clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells(props.filterLabel ? "MACHINE / SERVER / SESSION" : "SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
|
|
127
137
|
</text>
|
|
128
138
|
<box height={visibleCount()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
129
139
|
<For each={keys()}>
|
|
@@ -131,7 +141,9 @@ export function HomeAgentRoster(props) {
|
|
|
131
141
|
const row = () => byKey().get(key);
|
|
132
142
|
return (<box height={1} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
|
|
133
143
|
<NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={padCells(row().name, columns().agent) +
|
|
134
|
-
padCells(row().
|
|
144
|
+
padCells([row().machineLabel, row().serverLabel, row().sessionName]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity)}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={!stale(row()) && row().attention} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
|
|
135
147
|
if (!props.inputActive || row().paneId === null || stale(row()))
|
|
136
148
|
return;
|
|
137
149
|
props.onSelect(key);
|