I keep saying "Window 1, Shell 5" when I talk about my desktop. But what is a window here? Is it an AwesomeWM workspace, an X11 window, a Terminator tab, or a split terminal? Those are four different things. Once I started trying to send voice requests to one agent from another shell, that distinction stopped being academic.

This is a tour of the actual layers I am using. I run AwesomeWM on X11, put many shells inside Terminator, and use LinuxUtilities to discover and address a pane. CKE does the speech work. The window-routing part is separate: D-Bus tells my script what Terminator knows, while X11 and accessibility APIs help it reach the right pane. None of this means the agent has completed the request. The companion article, From My Microphone to an Agent and Back, shows the human voice loop; here I am opening the desktop plumbing underneath it.

Before the window manager: what Linux is doing for me

When I press a key, I am not talking directly to a shell. The kernel handles the physical device and exposes input events. In my X11 session, Xorg receives input, tracks focus, and normally delivers key events to the focused client. AwesomeWM is another X client with window-manager authority: it decides which top-level window is tiled, visible, or focused, and it can handle configured shortcuts. It does not forward every ordinary character. When Terminator receives a key, its focused VTE widget writes bytes to a pseudo-terminal. The shell or agent CLI reads those bytes. Output travels back through the PTY to the terminal. This distinction matters because each layer has a different identifier and a different API.

From one key press to one agent processA keyboard enters the Linux kernel input subsystem. Xorg sends ordinary key events to the focused Terminator window. AwesomeWM separately controls window focus and handles its configured shortcuts as another X client. Terminator's VTE widget writes input to the master end of a pseudo-terminal, and a shell or agent reads from the slave end.One key, six boundariesKeyboardphysical deviceKernelinput eventsX serverfocus + dispatchAwesomeWMwindow policyTerminatorVTE + PTY masterShell / agentPTY slaveordinary focused inputfocus policy / shortcuts, not every keyXorg routes ordinary keys; AwesomeWM sets policy and may intercept configured shortcuts.The agent only sees terminal input after the correct window and pane receive it.
The X server handles input delivery; AwesomeWM supplies window placement and focus policy. The PTY is the byte stream between terminal emulator and process, not an AwesomeWM object. See the Linux PTY manual.

The PTY has a master end held by the terminal emulator and a slave end presented to the shell as a terminal, often /dev/pts/N. That is why reading /proc/<pid>/fd/0 can help correlate a local agent process with a pane's shell: they can share a TTY. It is a clue about a local process, not proof of what a remote SSH process is doing. The PTY manual explains the master/slave contract in detail.

Where the distribution ends and the desktop begins

I casually say "my Ubuntu machine," but this laptop currently reports Linux Mint 22.3, an Ubuntu/Debian-derived distribution. That distinction helps: the distribution assembles packages and defaults; it is not the X11 protocol. The kernel provides devices, processes, permissions, sockets, and PTYs. A login/display manager starts a graphical session. On my current session that is LightDM launching Xorg and AwesomeWM. PipeWire handles my audio graph. A user-session D-Bus service lets desktop processes exchange structured messages. Terminator and my Python scripts run as applications in that session. I checked the session type with loginctl, rather than inferring it from a wallpaper.

The desktop is assembled above the kernelLinux Mint packages the system. The kernel owns hardware and process primitives. LightDM starts an X11 login session with Xorg and AwesomeWM. The user session also hosts D-Bus and PipeWire. Terminator and LinuxUtilities are applications inside that session. This is the observed setup on this laptop, not every Ubuntu or Mint installation.Deployment view: what starts what on this laptop?Linux Mint packages + configuration (Ubuntu-derived)Linux kernel: device input, process isolation, sockets, PTYs, display driversLightDM / loginstarts this X11 sessionXorg + AwesomeWMdisplay + window policyD-Bus + PipeWiremessages + audio, parallel servicesTerminator, LinuxUtilities, browser, agent CLIs
This is a deployment map of the observed session, not a claim that every key traverses every box. D-Bus and PipeWire are parallel user-space services; neither is the window manager.

Another Linux desktop might use GNOME on Wayland, KDE on X11, or a different display manager. The kernel still supplies core primitives, but the display protocol and routing tools change. My wmctrl/xdotool path is specifically an X11 choice, not a universal Linux command surface.

First, the Linux desktop underneath the shells

On this machine, the kernel and Xorg handle the input path; AwesomeWM manages the X11 clients and focus policy. Terminator is one of those clients. It draws terminals using VTE, and each pane hosts a shell with a pseudo-terminal (PTY). The shell may run Codex, Qwen, SSH, or nothing at all. A remote SSH agent does not magically become visible to my local process scanner.

The display stack on my X11 desktopKeyboard and microphone feed separate input paths. The X server handles windows and keyboard focus; AwesomeWM manages screens, tags, and window placement. Terminator draws tabs and pane terminals, each connected to a PTY, shell, and possibly an agent. PipeWire audio reaches LinuxUtilities and CKE separately.C4 / Context: what is actually running?Keyboard + headsetphysical inputX server + AwesomeWMwindows, focus, screens, tagsTerminatortabs + split terminal panesPTY + shell + agent CLIone process chain per panePipeWire + LinuxUtilitiesrecord, identify, confirmCKE Whisper / Kokoro
The X server and AwesomeWM do not know which blog an agent is writing. That is application state above the window layer. The audio path is also separate from window management.

The EWMH window-manager hints give X11 clients common properties such as the active top-level window. AwesomeWM's client API can focus or move that window, and its tag API organizes workspaces. But AwesomeWM does not have a first-class object for "Terminator Shell 5." That object belongs to Terminator.

My naming trap: tag 3 is not Window 3

AwesomeWM tags are the numbered workspaces I move among, often on two screens. A Terminator window is a top-level X11 window living on one or more of those tags. Inside it, Terminator has tabs and split terminals. The LinuxUtilities script currently prints "Window 1" after sorting Terminator's D-Bus window identifiers, then "Shell 5" after sorting the pane UUIDs in that window. Those numbers are a current list position, not AwesomeWM tag numbers and not stable identities. Opening or closing a pane can renumber the list. The UUID is a stronger address while the pane lives.

The hierarchy from screen to an agentTwo screens contain AwesomeWM tags. A tag displays top-level X11 windows. One Terminator window contains tabs; a tab can contain multiple split panes. One pane has a UUID, a PTY, a shell, and perhaps an agent. Window and Shell numbers in the script are list positions, not AwesomeWM tag numbers.C4 / Container: where does "Shell 5" live?Screen Atag 1 / tag 2tag 3 / ...X11 windowTerminator processXID: top-level IDTabone layoutmany splitsTerminator pane UUIDVTE terminal + PTYshell + optional agent processthis is the delivery targetAnother screen can have its own tags. A window can move tags; its pane UUID stays until that pane is destroyed.
Do not read "Window 1, Shell 5" as "AwesomeWM workspace 1, fifth terminal on the desktop." It is only the script's current enumeration.

I can have two monitors and tag 3 selected on one of them, while my script prints Window 1, Shell 5. That does not contradict anything. The tag belongs to AwesomeWM. The window number is a temporary sorted list of Terminator windows discovered over D-Bus. The shell number is a temporary sorted list of panes inside that Terminator window. The pane's UUID is the useful identifier for this session; even that disappears when the pane closes.

Four names for four different thingsAwesomeWM has screen and tag identifiers, X11 has a top-level window XID, Terminator D-Bus has a window UUID and a terminal pane UUID, and the script displays ephemeral Window and Shell list numbers. These are not interchangeable identifiers.The address book: labels are not identitiesAwesomeWMscreen + selected tagexample: screen 2 / tag 3X11 / EWMHtop-level window XIDexample: 0x00a5b4e9Terminator D-Buswindow + pane UUIDsexample: urn:uuid:...LinuxUtilities labelWindow 1, Shell 5list positions; can changeRouting joins these identities at runtime. There is no universal integer called “window 3.”
At the X11 layer, EWMH defines the active top-level window. Terminator's own D-Bus service returns different UUIDs. The script must join them.

Where D-Bus fits

D-Bus is a message protocol and a local bus for processes to find and call one another. A method call is not a magic Python function across the desktop. It has an address: a bus name (which service), an object path (which object), an interface (which API), and a method with typed arguments. Replies return values or errors; signals announce events. The user-session bus is the relevant one here, not the system bus used for machine-wide services.

Terminator's IPC source registers a service whose name starts with net.tenshu.Terminator2 at /net/tenshu/Terminator2. In my running version the name may include a display-derived suffix, so the script discovers matching names instead of hard-coding one. It asks get_terminals() for pane UUIDs, then get_window(uuid), get_tab_title(uuid), and get_focused_terminal(). Terminator's remotinator is another client of this interface. None of these calls proves an agent is attached to a pane.

What is a D-Bus call in plain English? My process sends a message on the session bus: "To this named Terminator service, at this object path, on this interface, call this method with this UUID." The bus routes the message; Terminator answers or reports an error. The four coordinates are not interchangeable. The service name locates a process, the object path locates an exported object, the interface groups methods, and the method is the operation. Signals are the reverse pattern: a service announces an event without a caller waiting for a direct return value. The D-Bus specification defines that message model.

One D-Bus method call has four addressesLinuxUtilities sends a method call through the user-session bus to the Terminator service. The bus name identifies the service, the object path identifies the exported object, the interface names the API, and get_window with a pane UUID is the method and argument. Terminator replies with a window UUID, not an X11 window ID.A D-Bus call is an addressed messageLinuxUtilitiesthe callerSession busroutes the messageTerminator servicename: net.tenshu.Terminator2...path: /net/tenshu/Terminator2interface: matching service APImethod: get_window(pane_uuid)Reply: Terminator window UUID. It is not the X11 XID and it is not an agent acknowledgment.
To inspect a running session without sending anything, list bus names with busctl --user list, then inspect the Terminator service. Do not guess a service name from a screenshot.
A small version of the discovery pathpython
bus = dbus.SessionBus()
for name in bus.list_names():
    if not name.startswith("net.tenshu.Terminator"):
        continue
    api = dbus.Interface(
        bus.get_object(name, "/net/tenshu/Terminator2"), name
    )
    for pane_uuid in api.get_terminals():
        window_uuid = api.get_window(pane_uuid)
        tab_title = api.get_tab_title(pane_uuid)

That is adapted from scripts/voice_terminator_targets.py in my local LinuxUtilities checkout. Try python3 scripts/voice_terminator_targets.py --focused-window from a Terminator pane. Add --scan-titles to read the pane headers you actually see. That extra scan uses AT-SPI accessibility information and briefly cycles panes, then restores focus. It is not the same thing as the D-Bus tab title. If the focused-pane or accessibility check fails, it reports uncertainty rather than inventing a name. Publication note: these newest pane-targeting scripts are still local changes, not files you can download from GitHub's main branch yet. I will link the exact files when they are merged.

Not every desktop message is a D-Bus message

This was the part I kept mentally flattening into "Linux APIs." There is no single desktop post office. When xdotool asks for an active X window or sends input, that goes through X11. When my script asks Terminator for pane UUIDs, it calls Terminator on the user D-Bus. When it reads a visible pane title, AT-SPI exposes the accessibility tree, backed by its own bus machinery. When Terminator sends text to a shell, those are bytes on a PTY, not an RPC. When my microphone records, PipeWire moves audio buffers through its graph. When the reader asks P3 to speak, SSH carries a command to that machine, where a local HTTP request reaches CKE Kokoro. Each protocol has its own trust boundary and failure mode.

Six different communication paths in one desktop workflowLinuxUtilities communicates with Xorg using X11 for window focus and keystrokes, Terminator through user D-Bus for pane UUIDs, AT-SPI for visible labels, the shell via Terminator's PTY for byte input, PipeWire for microphone audio, and P3 over SSH for speech synthesis. These are separate protocols with different data and authority.One desktop, six message highwaysLinuxUtilitiescontroller + checksX11 / EWMH → Xorg + AwesomeWM: window XID, focus, inputD-Bus session → Terminator: pane/window UUID, focused paneAT-SPI → accessibility tree: visible pane headerPTY → shell / agent: bytes and terminal outputPipeWire → recorder: microphone samplesSSH → P3, then HTTP loopback: Kokoro speech requestThe arrows name data paths, not a sequence. One request may use several of them at different times.
X11 requests/events and D-Bus method calls have different semantics. The X protocol describes requests and events; the D-Bus specification describes calls, replies, errors, and signals.

That map also explains why I do not use D-Bus to move raw microphone samples or pretend an AT-SPI label is an authenticated agent identity. D-Bus is good for structured local control calls. X11 is good at the window protocol it was designed for. A PTY is good at terminal bytes. My script has to bridge the boundaries deliberately, and it should stop whenever it cannot prove the join it is about to make.

From a D-Bus UUID to a real focused pane

This is the awkward part. The Terminator window UUID returned by D-Bus is not the X11 window ID (XID) used by wmctrl and xdotool. And the Terminator D-Bus API this script uses does not provide a direct "send text to this existing pane" call. So the local scripts/voice_send_pane.py matches the layers: enumerate X11 Terminator windows, activate one, then ask Terminator which pane UUID is focused. Only one X11 window may match the target. Within that window it cycles panes with the configured Ctrl+Tab binding, checking the UUID after each step. It types only when both the active XID and focused UUID agree.

A checked route to one pane, not a blind keystrokeLinuxUtilities uses the Terminator session D-Bus API for pane UUIDs, wmctrl and xdotool to discover and focus an X11 window, AT-SPI for visible pane headers, and procfs plus TTY to identify known local agents. It rechecks window and pane focus before typing. The agent must separately write a request-matched summary.C4 / Component: the script's four evidence sourcesD-Buspane + window UUIDX11 / EWMHXID + active focusAT-SPIvisible pane header/proc + TTYknown local agentLinuxUtilities controllerselect, cross-check, confirm, typeexact pane or stop with an error
These are separate evidence sources. A pane title is a useful label, not proof of process identity. A local TTY match can identify a local Codex or Qwen process, but it cannot certify a remote SSH session.

What the script actually checks before it types

The local voice_send_pane.py first resolves the requested pane from a fresh Terminator D-Bus listing. It checks that pane still belongs to the same Terminator window UUID. It then enumerates top-level X11 windows with wmctrl -lx and activates each candidate using wmctrl and xdotool. After activation it asks Terminator for the focused pane UUID. Exactly one X11 window must map back to the requested Terminator window. Then it cycles panes with Ctrl+Tab, checking the focused UUID until it reaches the exact target. Finally it checks the active XID and pane UUID immediately before typing and again before sending Return. If a window closes or focus changes, the script stops rather than choosing a likely-looking pane.

That is also why a pretty title is not enough. A title such as "CKE serving agent" comes from the terminal's visible header and helps me recognize it. The script's stronger local evidence is the pane UUID and, when possible, a process on the same /dev/pts/N as the shell. It knows how to recognize specific local Codex and Qwen command lines; it does not claim to discover every agent and cannot identify a remote process just because an SSH pane title looks convincing.

The checked delivery sequenceThe user selects a pane. LinuxUtilities resolves its D-Bus UUID, maps the Terminator window to one X11 XID, focuses the selected pane, verifies both IDs, types the draft, verifies focus again, and only then optionally sends Return. Any ambiguity stops delivery.One request, with a stop line at every boundaryYOULINUXUTILITIESTERMINATOR / D-BUSX11 / AWESOMEWMselect pane UUIDresolve fresh window/pane UUIDsmap exactly one top-level XIDcycle to focused pane UUIDverify active XID + pane UUIDtype one-line draftverify again; optional ReturnMismatch or ambiguity at any check: stop, inspect the pane, never auto-resend.
This is X11-specific focus-based delivery, not an agent API. A successful Return proves only that a key was sent, not that the agent accepted or finished the instruction.

The send routine caps the message at 4,096 UTF-8 bytes and rejects embedded newlines. It confirms the destination and text, types with xdotool, and checks focus again. If you choose to submit, it then sends Return. That is a stronger boundary than "type into whichever window was last focused," but it is still keystroke injection. If the target changes during delivery, the state is uncertain and the safe response is to inspect the pane, not automatically resend. A terminal displaying text is not an agent acknowledgment.

Fail closed when the target stops matchingThe script checks that a selected pane UUID still exists, that exactly one X11 window maps to its Terminator window, and that both the active XID and focused pane UUID match before and after typing. A no or uncertain result at any stage stops delivery and requires inspection; it never blindly retries a possibly accepted request.The useful answer is sometimes “do not send”Pane UUID exists?fresh D-Bus listingOne XID maps?activate + verifyFocus exact?XID + pane UUIDType draftrecheck; maybe ReturnNo, ambiguous, or focus changed?Stop. Keep transcript. Inspect the target. Do not auto-resend.Typed text is not an acknowledgment. A timeout is not permission to send the same instruction twice.
The actual script checks focus again after typing. If it cannot establish what happened, it reports uncertainty; it cannot erase text that another process may already have received.

A smaller C4 map: who owns which decision?

The earlier diagrams show the whole desktop. Here is the part that matters when I select a single pane. My Python controller does not own Terminator's tabs or AwesomeWM's tags. It asks the process that owns each fact, then joins the answers. The arrows below name the actual communication mechanism, not a generic "API" arrow.

Component map for one pane requestLinuxUtilities calls the session D-Bus to discover Terminator pane and window UUIDs. It calls wmctrl and xdotool through X11 to find and activate the corresponding top-level window. Terminator's focused VTE pane then sends typed input to its PTY and the agent CLI. AwesomeWM controls top-level window policy but has no method in this route for sending text to a Terminator pane.C4 / Component: one request, six partsLinuxUtilitiesselect + verifySession D-Busroutes callsTerminator IPCpane UUID + focusX11 tools + XorgXID, focus, keysVTE + PTYterminal bytesShell / agentreads inputAwesomeWM manages X11 window policy; it does not own the pane's text-input API.D-BuscallX11keysPTY
A component view, not a claim that one protocol does everything. Terminator IPC exposes identity and focus queries; X11 tools supply the present delivery path.

Part one: what does “select Shell 5” actually call?

voice_note.py --list-panes calls discover_terminals(). It opens the session D-Bus, lists names beginning net.tenshu.Terminator, then asks each service for its pane UUIDs. For every pane it asks for the parent window UUID and window or tab title. LinuxUtilities sorts the UUIDs and prints friendly positions such as Window 1, Shell 5. Those numbers are a snapshot; the UUID is the address to carry into a later send.

These methods have different answers: get_terminals() gives pane UUIDs; get_window(pane_uuid) gives a Terminator window UUID, not an X11 XID; get_focused_terminal() gives Terminator's last-focused pane UUID; get_window_title() and get_tab_title() give labels. A split pane may not have a useful tab title. None confirms which agent accepted a prompt or returns the terminal's output text. This is the contract of the Terminator IPC implementation, not a capability inferred from the diagram.

D-Bus discovery and its repliesTime flows downward. LinuxUtilities opens the session bus, lists Terminator service names, gets the Terminator object, calls get_terminals, then for each pane calls get_window and title methods. Terminator replies with UUIDs or titles. LinuxUtilities sorts those results and displays Window and Shell numbers. Listing does not change focus or type into a terminal.PYTHONSESSION BUSTERMINATOR IPClist_names()matching service namesget_terminals()pane UUID listget_window(pane UUID)window UUIDget_*_title(pane UUID)title or unavailableSort UUIDs, display “Window 1, Shell 5”. No X11 XID yet.No focus change, no input, no agent acknowledgment.
These are ordered method calls and replies, not benchmark timings. The user-session bus routes calls; Terminator owns the answers.

What each interface actually gives me

Call or commandUseful answerLimit
get_terminals() on Terminator D-BusPane UUIDsNo XID, text, or agent state
get_window(pane_uuid)Parent Terminator window UUIDNot an X11 window ID
get_focused_terminal()Last-focused pane UUIDCan change between calls
wmctrl -lxTop-level XIDs and classesNo internal tabs or panes
xdotool getactivewindowActive X11 window IDNo agent delivery receipt
xdotool type / key ReturnSynthetic X key eventsFocus-sensitive, no acknowledgment
/proc + pane TTYKnown local agent process clueCannot verify a remote SSH agent
LinuxUtilities composes these partial answers. It cannot ask any one of them, “did the agent finish?”

To inspect rather than guess, run busctl --user list to find the current Terminator bus name and busctl --user introspect NAME /net/tenshu/Terminator2 to see its exported methods. NAME is the discovered name, not a literal service. That inspection is read-only; it does not select a pane.

Part two: bridge Terminator's UUID to an X11 window

At send time the script resolves the pane again. It checks that its UUID still belongs to the expected Terminator window and that exactly one Terminator D-Bus service owns it. Then it lists top-level X11 windows with wmctrl -lx and filters for Terminator's window class. It cannot compare an XID with a D-Bus UUID: different systems issued them. Instead it activates each candidate window, asks Terminator for the focused pane, maps that pane back to its Terminator window UUID, and requires one unambiguous match. This scan can briefly change visible focus.

Once it has the XID, the script activates that window and cycles panes with Ctrl+Tab, asking get_focused_terminal() after each step. It first checks that Terminator's configured cycle_next binding really is <Control>Tab; otherwise it refuses to guess. The loop is bounded by the number of panes in that window. A final check requires both the active X11 window and focused pane UUID to match.

The cross-protocol bridge before typingTime runs downward. LinuxUtilities re-resolves the pane through D-Bus, lists Terminator X11 candidates with wmctrl, asks Xorg through xdotool to activate each candidate, queries Terminator D-Bus for the focused pane and its parent window, requires exactly one UUID-to-XID match, then cycles Ctrl+Tab until the exact pane UUID is focused. It checks active XID and pane UUID before typing. Window activation is not agent acknowledgment.PYTHONXORG / X11TERMINATOR IPCget_window(pane UUID)window UUIDwmctrl -lxcandidate XIDsactivate candidate XIDactive XIDget_focused_terminal()focused pane UUIDCtrl+Tab if neededX11 focus eventget_focused_terminal()exact pane UUID or STOPGate: one XID to one window UUID; active XID and focused pane UUID must match before typing.
After X activation, the script waits briefly for Terminator's focus state and reads it twice to avoid a transient answer. Those waits are synchronization heuristics, not proof that focus can never change.

Part three: what makes words appear in that pane?

Only after that gate does xdotool type send synthetic key events through X11. In this invocation it has no explicit target window, so xdotool uses XTEST to inject keys toward the current focus. Xorg delivers them to Terminator's focused VTE widget; VTE turns the characters into bytes on the PTY; the shell or agent CLI reads them as terminal input. This is the apparent "automatic typing." It is not an accessibility-tree write, not a D-Bus message containing the prompt, and not a direct call into the agent. If I submit, xdotool key Return sends one more key event. A draft stops before Return.

The actual X11 commands in voice_send_pane.pyshell
wmctrl -lx                         # list top-level XIDs and WM_CLASS
wmctrl -ia 0x00a5b4e9             # activate one numeric XID
xdotool windowactivate --sync 0x00a5b4e9
xdotool windowfocus --sync 0x00a5b4e9
xdotool getactivewindow            # verify the active XID
xdotool key --clearmodifiers ctrl+Tab  # move to another pane
xdotool type --clearmodifiers --delay 35 -- "review the tests"
xdotool key --clearmodifiers Return # only after confirmed submission

-l lists windows, -x includes their class, and -i -a activates by numeric XID. --sync waits for the X window operation; it does not wait for an agent answer. --clearmodifiers avoids a held Shift/Ctrl changing the simulated input. --delay 35 spaces synthetic keystrokes in milliseconds. The -- marks the end of options so the message is treated as text. These are X11 controls: AwesomeWM can still change focus according to its policy, and another action can race this script.

Synthetic keys become PTY bytes, not an agent RPCTime runs downward. LinuxUtilities checks X11 active window and Terminator focused pane UUID, asks xdotool to type synthetic keys, Xorg dispatches them to Terminator's focused VTE widget, VTE writes bytes to the PTY master, and the shell or agent reads the PTY slave. The script checks focus again after typing and sends Return only if the caller confirmed submission and focus still matches. A focus change during typing can leave an unsafe partial draft; Return is withheld but typed text cannot be recalled.SCRIPTXORGVTEPTYAGENTtype --delay 35key eventswrite bytesread inputrecheck focusReturn, if confirmedThis proves where keys were aimed, not that an agent accepted, understood, or finished.
The post-type check protects the final Return, but it cannot retract characters if focus changed midway. The script reports uncertain delivery and requires inspection rather than automatically retrying.

The delivery contract is narrow: one line, at most 4,096 UTF-8 bytes; no newline, carriage return, or NUL. voice_note.py keeps the WAV and transcript even if targeting fails. In its normal confirmation prompt, Enter types and submits; SEND types a draft without Return; CANCEL leaves the transcript unsent. With --summary-return, it appends a request-ID-matched file instruction, but that is not a transport-level agent acknowledgment. If focus changes after typing, the script withholds Return and marks delivery uncertain; it must not silently resend a possibly accepted instruction.

One real request, step by step

  1. Discover. voice_terminator_targets.py --focused-window --scan-titles shows panes in the invoking Terminator window. --agents-only filters to locally verified Codex or Qwen processes when possible.
  2. Record. voice_note.py selects a microphone, writes a WAV, calls CKE Whisper, and saves a transcript and JSON report. I can correct the text before sending.
  3. Address. I select a pane UUID using --pane-id, or the current window/shell number. The UUID is safer across list reordering, but only for that pane's lifetime.
  4. Confirm. Enter submits the request; SEND leaves a draft without Return. The script records a request ID and asks the agent to write a summary to a unique path.
  5. Read back. The summary ledger recognizes the exact request file. A text-following Kokoro reader can speak it. That return path is separate from Terminator and still needs the agent to honor the file instruction.

The speech loop is two routes today, not one imaginary server

Here is the detail I do not want to blur. In the current voice_note.py, pw-record records 16 kHz mono PCM to a private voice-note directory on my laptop. The script calls voice_agent.transcribe(), which invokes the local CKE run_whisper_v8.py with a prepared Whisper Base encoder and decoder. That is a local runner, not the P3 HTTP Whisper service. It saves recording.wav, whisper.json, and transcript.txt. I can inspect or edit that transcript, and --send-file lets me send corrected text without recording again.

The spoken return is different. agent_summary_inbox.py polls a local ledger for a completed summary with the same request ID. cke_summary_reader.py sends that text over SSH to a Kokoro endpoint on P3's loopback address, checks the returned source hash, chunk IDs, text spans, audio samples, and WAV format, then builds a local HTML reader beside the WAV. The browser is a captioned player here, not the agent transport. The local reader can highlight the part of the summary being spoken. If P3 is unavailable or a chunk fails validation, it does not pretend an older WAV is the new answer.

The current request and spoken reply pathOn the laptop, PipeWire records a WAV and the local CKE Whisper runner transcribes it. LinuxUtilities confirms the transcript and addresses one Terminator pane. The agent may write a request-ID-matched summary file. The inbox checks that file and sends the text over SSH to P3's Kokoro speech endpoint. The resulting WAV and checked cues appear in a local captioned browser reader.The round trip I can use todayLAPTOP / my X11 sessionP3 / speech hostMic + pw-recordrecording.wavlocal CKE Whispertranscript.txtconfirm + pane routeUUID and XID checkedagent summary filerequest ID + statusinbox + cue checkshash + text spanscaptioned readertext + local WAVCKE KokoroHTTP on 127.0.0.1reached via SSHCurrent input: local Whisper runner. Current spoken return: P3 Kokoro via SSH. Future service paths may differ.
The speech paths are intentionally labelled by where they run. "CKE-powered" does not by itself tell us whether inference happened on the laptop or P3.

That little ledger matters more than it sounds. A summary is not "the latest text file I can find." The expected path includes the target's pane UUID and a unique voice-... request ID. A completed file must start with matching Request ID and Status: complete lines. The agent writes a temporary file and atomically renames it when done. The inbox checks age, size, and the envelope before it builds speech. I can ask for --list to see which request has a new summary instead of listening to a stale answer from another shell.

A request and reply in causal orderTime runs downward. The user records on the laptop, local Whisper returns a transcript, LinuxUtilities resolves and verifies a Terminator pane, X11 delivers the draft, the agent may write a request-ID-matched summary, the inbox validates it, and a separate SSH call to P3 Kokoro returns audio and text cues. This diagram shows ordering, not measured durations or guaranteed agent acknowledgment.A timing diagram without invented millisecondsYOUVOICE / CKE LOCALX11 + TERMINATORAGENT + FILEP3 KOKOROrecord WAVtranscript returnedconfirm destination + wordsverify UUID + XID; typeReturn may reach CLImatching summary file appearsvalidated text to P3 via SSHchecked WAV + cues to readerThe gap between “Return sent” and “summary file appears” is agent work, not a guaranteed transport reply.
Time runs downward; these are ordered transitions, not benchmark numbers. The D-Bus call and X11 events happen at different points in the same human task.
Find the answer and open the captioned readershell
cd ~/Workspace/LinuxUtilities
python3 scripts/agent_summary_inbox.py --list
python3 scripts/agent_summary_inbox.py \
  --id 'voice-PASTE-THE-REQUEST-ID' --wait 120 --open
Try the local, deliberate versionshell
cd ~/Workspace/LinuxUtilities
python3 scripts/voice_terminator_targets.py --focused-window --scan-titles
python3 scripts/voice_note.py --list-panes
python3 scripts/voice_note.py \
  --mic-target "$(pactl get-default-source)" \
  --pane-id 'urn:uuid:PASTE-THE-SELECTED-PANE' \
  --summary-return

My laptop's built-in microphone and Sony headset do not behave identically, so I name the source explicitly when testing. These commands describe my X11/Terminator setup, not a generic cross-desktop installer. The repository is where the scripts and tests belong; check the current tree before expecting a local-only piece from my screenshot to be available there.

Why not just ask AwesomeWM to do everything?

AwesomeWM does expose a Lua API, and awesome-client can execute Lua through D-Bus when remote support is enabled. That is useful for choosing a tag, finding a client, moving a window, or building a predictable project map. It does not replace Terminator's pane model or tell me whether an AI agent understood a request. I could write a tiny AwesomeWM helper that returns the selected tag and top-level XID; I would still have to join that to Terminator's pane UUID and the agent's own status.

When I want to see the layers without sending anything, I start with read-only commands. wmctrl -lx lists top-level X11 windows with XIDs and classes. xdotool getactivewindow reports the currently active XID. busctl --user list shows names on my user session bus. The LinuxUtilities discovery command adds Terminator's window and pane UUIDs and, optionally, visible headers. I can correlate these views, but I should never compare the literal number in "tag 3" with the literal number in "Window 3."

Read-only inspection from my X11 sessionshell
wmctrl -lx
xdotool getactivewindow
busctl --user list | grep net.tenshu.Terminator
python3 ~/Workspace/LinuxUtilities/scripts/voice_terminator_targets.py \
  --focused-window --scan-titles

These commands have scope limits. The AwesomeWM API can see its clients and tags, but not the semantic state of an agent. Terminator's IPC can report its own windows and terminal UUIDs, but it does not own my spoken request ledger. AT-SPI can expose accessible widget labels, but a label can be stale. /proc can reveal local process details, but not a remote agent behind SSH. That is why my controller combines them and still asks for confirmation.

This is also why I would not call the current script Wayland-compatible. Wayland's protocol does not give ordinary clients the same global view of other clients' windows. The X11 focus and keystroke parts would need a compositor-approved adapter or an application-owned delivery API. That is a future port, not a flag we can flip.

X11 today, a different integration boundary under WaylandThe left column shows the current implemented route: LinuxUtilities asks X11 tools for window IDs and focus, Terminator D-Bus for pane UUIDs, and verifies both before typing. The right column is a future conceptual Wayland route where a compositor-approved interface or application-owned agent adapter replaces global X11 window inspection and keystroke injection. The Wayland route is not implemented.Same goal, different display boundaryX11 / AwesomeWM: working todaywmctrl + xdotool → XID and focusTerminator D-Bus → pane UUIDcross-check → type into exact paneRisk: focus can move; no agent ACKWayland: conceptual, not builtcompositor controls client visibilityno generic global XID / focus scanneed approved adapter or agent APIDo not rename the X11 script “portable”The PTY, D-Bus, and speech ideas may still apply. The global window-control mechanism does not transfer unchanged.
Wayland clients do not have the same global position and other-client visibility as X11 clients. A future adapter should be designed against the actual compositor and agent interface, not simulated by silently falling back to the wrong pane.

The next interface I actually want

The current route is useful because it makes my existing desktop addressable. It is not the final architecture. I want a stable agent registry above windows: project name, agent name, current pane UUID, transport, verified availability, last request ID, and last completed summary. Then my voice command can say "CKE serving agent" instead of "Window 3, Shell 2." The application can show or speak the resolved destination before sending. Terminator remains a place to see the work, but not the only source of truth.

A native Terminator plugin or a supported agent-session adapter might eventually avoid focus-based typing. Until then, the reliable habits are simple: resolve one exact pane, confirm the transcript, fail closed on ambiguity, keep the original text, and only call a request complete when its matching answer arrives. That is how this memory palace becomes a usable interface rather than a wall of windows.

Sources and code