-
Notifications
You must be signed in to change notification settings - Fork 59
feat: add tmux control mode embedder API #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -340,6 +340,11 @@ pub const Action = union(Key) { | |
| /// otherwise the terminal-set title. | ||
| copy_title_to_clipboard, | ||
|
|
||
| /// A tmux control mode event from the Viewer. The embedder uses | ||
| /// this to create/destroy native windows and panes that mirror | ||
| /// the tmux server state. | ||
| tmux_control: TmuxControl, | ||
|
|
||
| /// Sync with: ghostty_action_tag_e | ||
| pub const Key = enum(c_int) { | ||
| quit, | ||
|
|
@@ -406,6 +411,7 @@ pub const Action = union(Key) { | |
| search_selected, | ||
| readonly, | ||
| copy_title_to_clipboard, | ||
| tmux_control, | ||
|
|
||
| test "ghostty.h Action.Key" { | ||
| try lib.checkGhosttyHEnum(Key, "GHOSTTY_ACTION_"); | ||
|
|
@@ -998,6 +1004,52 @@ pub const SearchSelected = struct { | |
| } | ||
| }; | ||
|
|
||
| /// Tmux control mode event from the Viewer state machine. | ||
| /// The embedder receives these to manage native windows/panes | ||
| /// that mirror the tmux server state. | ||
| pub const TmuxControl = struct { | ||
| event: Event, | ||
| /// Contextual ID: pane_id for pane_output, window_id for window_* | ||
| /// and layout_change events. Unused for enter/exit/session events. | ||
| id: u32 = 0, | ||
| data: []const u8 = &.{}, | ||
|
|
||
| pub const Event = enum(c_int) { | ||
| enter = 0, | ||
| exit = 1, | ||
| windows_changed = 2, | ||
| pane_output = 3, | ||
| layout_change = 4, | ||
| window_add = 5, | ||
| window_close = 6, | ||
| window_renamed = 7, | ||
| session_changed = 8, | ||
| session_renamed = 9, | ||
|
|
||
| // Sync with: ghostty_tmux_event_e | ||
| test "ghostty.h TmuxControl.Event" { | ||
| try lib.checkGhosttyHEnum(Event, "GHOSTTY_TMUX_"); | ||
| } | ||
| }; | ||
|
|
||
| // Sync with: ghostty_action_tmux_control_s | ||
| pub const C = extern struct { | ||
|
Comment on lines
+1030
to
+1036
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If these are intentionally reserved for a future PR, it would help to add a comment noting their unimplemented status (e.g., |
||
| event: Event, | ||
| id: u32, | ||
| data: [*]const u8, | ||
| data_len: usize, | ||
|
Comment on lines
+1011
to
+1040
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Widen Internal tmux pane/window ids are still 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| pub fn cval(self: TmuxControl) C { | ||
| return .{ | ||
| .event = self.event, | ||
| .id = self.id, | ||
| .data = self.data.ptr, | ||
| .data_len = self.data.len, | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| test { | ||
| _ = std.testing.refAllDeclsRecursive(@This()); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -211,6 +211,13 @@ pub const Viewer = struct { | |
| /// never reuses window IDs within a server process lifetime. | ||
| windows: []const Window, | ||
|
|
||
| /// Pane output data. The embedder should feed this data into its | ||
| /// own virtual terminal surface for the given pane. The Viewer | ||
| /// also feeds this data into its internal Terminal, so both the | ||
| /// Viewer's canonical state and the embedder's rendering surface | ||
| /// receive the same output stream. | ||
| pane_output: PaneOutput, | ||
|
|
||
| pub fn format(self: Action, writer: *std.Io.Writer) !void { | ||
| const T = Action; | ||
| const info = @typeInfo(T).@"union"; | ||
|
|
@@ -253,6 +260,11 @@ pub const Viewer = struct { | |
| } | ||
| }; | ||
|
|
||
| pub const PaneOutput = struct { | ||
| pane_id: usize, | ||
| data: []const u8, | ||
| }; | ||
|
|
||
| pub const Pane = struct { | ||
| terminal: Terminal, | ||
|
|
||
|
|
@@ -459,14 +471,29 @@ pub const Viewer = struct { | |
| command_consumed = true; | ||
| }, | ||
|
|
||
| .output => |out| self.receivedOutput( | ||
| out.pane_id, | ||
| out.data, | ||
| ) catch |err| { | ||
| log.warn( | ||
| "failed to process output for pane id={}: {}", | ||
| .{ out.pane_id, err }, | ||
| ); | ||
| .output => |out| { | ||
| // Feed output into the Viewer's internal Terminal (canonical state). | ||
| self.receivedOutput( | ||
| out.pane_id, | ||
| out.data, | ||
| ) catch |err| { | ||
| log.warn( | ||
| "failed to process output for pane id={}: {}", | ||
| .{ out.pane_id, err }, | ||
| ); | ||
| }; | ||
|
|
||
| // Also emit a pane_output action for the embedder, but only | ||
| // for tracked panes. Untracked pane output is silently dropped | ||
| // by receivedOutput above and should not be forwarded. | ||
| if (self.panes.contains(out.pane_id)) { | ||
| var arena = self.action_arena.promote(self.alloc); | ||
| defer self.action_arena = arena.state; | ||
| actions.append( | ||
| arena.allocator(), | ||
| .{ .pane_output = .{ .pane_id = out.pane_id, .data = out.data } }, | ||
| ) catch return self.defunct(); | ||
|
Comment on lines
+489
to
+495
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Consider asserting or saturating the cast, or widening
Comment on lines
+486
to
+495
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forward
🤖 Prompt for AI Agents |
||
| } | ||
| }, | ||
|
|
||
| // Session changed means we switched to a different tmux session. | ||
|
|
@@ -1767,7 +1794,12 @@ test "initial flow" { | |
| .input = .{ .tmux = .{ .output = .{ .pane_id = 0, .data = "new output" } } }, | ||
| .check = (struct { | ||
| fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void { | ||
| try testing.expectEqual(0, actions.len); | ||
| // Expect .pane_output action for tracked pane | ||
| try testing.expectEqual(1, actions.len); | ||
| try testing.expect(actions[0] == .pane_output); | ||
| try testing.expectEqual(0, actions[0].pane_output.pane_id); | ||
| try testing.expectEqualStrings("new output", actions[0].pane_output.data); | ||
| // Also verify the internal terminal received the data | ||
| const pane: *Viewer.Pane = v.panes.getEntry(0).?.value_ptr; | ||
| const screen: *Screen = pane.terminal.screens.active; | ||
| const str = try screen.dumpStringAlloc( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -428,10 +428,11 @@ pub const StreamHandler = struct { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("tmux viewer action={f}", .{action}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (action) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .exit => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We ignore this because we will fully exit when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // our DCS connection ends. We may want to handle | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // this in the future to notify our GUI we're | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // disconnected though. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.surfaceMessageWriter(.{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .tmux_control = .{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .event = .exit, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+431
to
+435
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normal tmux enter/exit still never reaches the embedder. Lines 431-435 only run when Possible fix .enter => {
// Setup our viewer state
assert(self.tmux_viewer == null);
const viewer = try self.alloc.create(terminal.tmux.Viewer);
errdefer self.alloc.destroy(viewer);
viewer.* = try .init(self.alloc);
errdefer viewer.deinit();
self.tmux_viewer = viewer;
+ self.surfaceMessageWriter(.{
+ .tmux_control = .{ .event = .enter },
+ });
break :tmux;
},
.exit => {
+ self.surfaceMessageWriter(.{
+ .tmux_control = .{ .event = .exit },
+ });
// Free our viewer state if we have one
if (self.tmux_viewer) |viewer| {
viewer.deinit();
self.alloc.destroy(viewer);
self.tmux_viewer = null;🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .command => |command| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -443,8 +444,37 @@ pub const StreamHandler = struct { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .windows => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .windows => |windows| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const json = serializeTmuxWindows( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Caller-owned JSON buffer from Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.alloc, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| windows, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) catch |err| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.warn("failed to serialize tmux windows: {}", .{err}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break :tmux; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.surfaceMessageWriter(.{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .tmux_control = .{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .event = .windows_changed, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .data = try apprt.surface.Message.WriteReq.init( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.alloc, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+447
to
+464
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Free the temporary windows JSON after The helper at Lines 1583-1585 returns caller-owned memory, and Possible fix .windows => |windows| {
const json = serializeTmuxWindows(
self.alloc,
viewer,
windows,
) catch |err| {
log.warn("failed to serialize tmux windows: {}", .{err});
break :tmux;
};
+ defer self.alloc.free(json);
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .windows_changed,
.data = try apprt.surface.Message.WriteReq.init(
self.alloc,
json,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+447
to
+465
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Additionally, if The fix is to always free .windows => |windows| {
const json = serializeTmuxWindows(
self.alloc,
viewer,
windows,
) catch |err| {
log.warn("failed to serialize tmux windows: {}", .{err});
break :tmux;
};
defer self.alloc.free(json);
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .windows_changed,
.data = try apprt.surface.Message.WriteReq.init(
self.alloc,
json,
),
},
});
}, |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .pane_output => |out| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.surfaceMessageWriter(.{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .tmux_control = .{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .event = .pane_output, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .id = @intCast(out.pane_id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .data = try apprt.surface.Message.WriteReq.init( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.alloc, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.data, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1549,3 +1579,46 @@ pub const StreamHandler = struct { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.surfaceMessageWriter(.{ .progress_report = report }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Serialize tmux Viewer windows state to JSON for the embedder. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Produces: {"session_id":N,"tmux_version":"X.Y","windows":[...]} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Caller owns the returned slice and must free it with `alloc`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn serializeTmuxWindows( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alloc: Allocator, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewer: *const terminal.tmux.Viewer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| windows: []const terminal.tmux.Viewer.Window, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) Allocator.Error![]const u8 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const WindowJson = struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: usize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: usize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: usize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| layout: terminal.tmux.Layout, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Payload = struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| session_id: usize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tmux_version: []const u8, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| windows: []const WindowJson, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build the JSON-friendly window array. We use a temporary allocation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // for the wrapper structs (they reference Layout by value which has | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // jsonStringify for custom serialization). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const json_windows = try alloc.alloc(WindowJson, windows.len); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer alloc.free(json_windows); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (windows, 0..) |win, i| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json_windows[i] = .{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .id = win.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .width = win.width, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .height = win.height, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .layout = win.layout, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return std.json.Stringify.valueAlloc(alloc, Payload{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .session_id = viewer.session_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .tmux_version = viewer.tmux_version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .windows = json_windows, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, .{}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: manaflow-ai/ghostty
Length of output: 6315
🏁 Script executed:
Repository: manaflow-ai/ghostty
Length of output: 7328
Add
.tmux_controlto the exhaustive switch insrc/apprt/gtk/class/application.zig:661-768before merging.The
performActionfunction uses a comptime exhaustive switch. Adding the.tmux_controlenum variant toAction.Keywithout handling it in this switch will cause a compile error. Either add an explicit handler or include it in the unimplemented catch-all case at the end of the switch.The switch currently lists unimplemented cases like
.secure_input,.close_all_windows, etc., but.tmux_controlis missing entirely.🤖 Prompt for AI Agents