Skip to content

feat(terminal): add Open with AI agent actions - #1094

Draft
spandan11106 wants to merge 2 commits into
lgse:feat/93-embedded-terminalfrom
spandan11106:feat/93-terminal-agent-actions
Draft

spandan11106 wants to merge 2 commits into
lgse:feat/93-embedded-terminalfrom
spandan11106:feat/93-terminal-agent-actions

Conversation

@spandan11106

@spandan11106 spandan11106 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Stacked on feat/93-embedded-terminal (#1089). Review the diff against that base, not main.

The feature #93 asked for: Open with AI agent. #1093, which used to sit between these two, is closed; the browser-to-terminal synchronisation it carried is deferred to #1103.

  • A configurable command, empty by default. Strata never names, ships, or requires a particular agent, and the action stays hidden until a command is set. Settings → General → Embedded terminal → AI agent command.
  • The folder background runs the agent in that folder. A selection runs it in the folder holding the selection, with every selected path passed as its own process argument — a name containing spaces, quotes or shell metacharacters is never reinterpreted, because no shell sees it.
  • The configured command is split into words once, and its program resolved before spawning: VTE spawns without G_SPAWN_SEARCH_PATH. A bare name comes from PATH, an absolute path is used directly, and a relative one resolves against the folder the agent will run in.
  • Paths and command words are only ever passed exactly. One that cannot be represented for the spawn stops the launch with a named error rather than reaching the agent as a different path.
  • An agent never replaces a live session. With one open the action asks for it to be closed first, so nothing is killed on a guess about whether a shell is idle.
  • While an agent owns the session, the directory synchronisation from feat(terminal): follow browser navigation in the embedded terminal #1093 stops, so navigating the browser never writes into the agent's input.
  • Failures are reported rather than swallowed: no command configured, a command that cannot be parsed, a program that is not on PATH, or a location with no local path.

Two things worth calling out for review:

  • The menu entries reuse the existing terminal icon rather than adding a Lucide one, since a new icon means importing exact upstream geometry. Say the word and I will add sparkles.
  • {} inside a word substitutes the paths into that single argument, which expands the original one-path-per-argument contract from Explore an embedded terminal pane for AI agent actions #93. It is there because claude and codex take a single prompt positional, so appending paths silently drops all but the first. Discussion in the review thread below.

Visual evidence

Posted in a comment below, with a correction noted underneath it: the recording predates the review fixes in a8afc8f, so its session-replacement section no longer matches the branch.

How to test

You need a command to run. Anything works; sh is enough to see the arguments arrive.

  1. Settings → General → Embedded terminal, set AI agent command to your agent, then press Enter.
  2. Right-click a folder's empty background → Open with AI agent. The panel opens and the agent starts in that folder.
  3. Close the panel with the ×. Select two or three files, right-click → Open with AI agent. It starts in their folder and receives each selected path as a separate argument.
  4. Try it on files whose names contain a space, a quote, and a ;. The names arrive intact.
  5. With a session already open, choose the action again. It refuses and asks for the session to be closed first, and the running session is untouched. Close it with the × and the action works.
  6. While an agent is running, navigate around the browser. Nothing is typed into the agent.
  7. Clear the setting. The menu entries disappear.
  8. Set it to a command that does not exist. The error names it. Set it to a file whose interpreter does not exist: the panel stays open and reports the failed launch, and F4 twice still gives a working shell.
  9. Put an executable ./agent in the folder you are browsing and configure ./agent. It resolves against that folder, not Strata's own working directory.

Expected result: the agent starts in the right folder with one argument per selected path, replaces an idle session but never a busy one, is never written into by navigation, and reports every failure instead of silently doing nothing.

Related issue

Closes #93

@spandan11106

Copy link
Copy Markdown
Collaborator Author
screenrecording-2026-09-17_19-53-35.mp4

Visual evidence for the AI agent actions.

What the video shows, in order:

  1. Hidden until configured. With no agent command set, neither the folder background menu nor the item menu offers the action. Strata never assumes a provider.
  2. Configuring it. Settings → General → Embedded terminal → AI agent command. Typing shows "Press Enter to save"; pressing Enter shows "Saved". The field is a plain command line, split into words when it runs.
  3. On a folder. Right-clicking the folder background and choosing the action opens the panel with the agent running in that folder.
  4. On a selection. Selecting main.rs and Cargo.toml and choosing the action runs the agent in src/, with each selected path passed as its own process argument after the configured flags.
  5. Quoting. Running it on it's a draft.txt inside release notes/ shows the name arriving intact. Paths are process arguments, never concatenated into a shell string, so spaces, quotes and shell metacharacters cannot be reinterpreted.
  6. A busy terminal is not disturbed. With sleep 30 running, choosing the action reports that the terminal is busy instead of killing the command. An idle session is replaced; a working one is not.
  7. Navigation does not reach the agent. Moving around the browser while an agent runs writes nothing into it; the directory synchronisation from feat(terminal): follow browser navigation in the embedded terminal #1093 only applies to a shell session.
  8. Failures are reported. A command that is not on PATH is named in an error dialog before anything is spawned. An agent that starts and then exits leaves its own output on screen with a line saying how it ended, rather than the panel disappearing.

This closes #93.

@spandan11106

Copy link
Copy Markdown
Collaborator Author
image

UI made better

@spandan11106

Copy link
Copy Markdown
Collaborator Author

Three follow-up commits from testing this by hand.

Selected paths can now be placed in the command (c94c57f). Appending paths as arguments only suits agents that take files that way, like aider. Claude Code and Codex take a single prompt instead, so selecting several files reached them as one path with the rest dropped — the selection looked like it was being ignored.

The command now says where the paths go:

Command Agent receives
aider {} /demo/a.rs and /demo/b.rs, one argument each
claude "review {}" one argument: review /demo/a.rs /demo/b.rs
claude paths appended, as before

A bare {} expands to one argument per path. A {} inside a word substitutes them into that word. A command without {} is unchanged, so nothing already configured breaks. Paths are still process arguments throughout; no shell parses them.

The agent command entry now confirms a save (d979472). It saved on Enter without changing anything on screen, so there was no way to tell whether the typed value had been taken. The row shows "Press Enter to save" while the field differs from the stored command, and "Saved" briefly once it is stored.

A failed agent stays on screen (d979472). The panel hid itself whenever the child exited, so an agent dying on a bad flag or a missing credential took its own error message with it. Only a shell you exited closes the panel now; an agent leaves its output in place with a line reporting how it ended.

The saved/unsaved notice no longer wraps mid-word (d47b203). It sat beside the entry, where the row's title column squeezed it until "Saved" broke across two lines. It now sits under the field, on one line.

Settings copy and docs/preferences.md cover the placeholder. Tests added for the placeholder forms, the save-feedback states, and the exit notice.

@guisilveira

Copy link
Copy Markdown
Contributor

I reviewed the current #1094 head as the third PR in the terminal stack, both against its actual PR2 base and cumulatively against main.

The merge-base between PR2 and PR3 is exactly the current PR2 head, so the PR3 delta is clean and attributable.

Overall, I still like the architecture here. The agent feature stays mostly provider-agnostic, uses the existing window-scoped terminal, keeps selected paths out of shell command strings, disables browser cwd synchronization while an agent owns the session, and adds the action in the places we discussed in #93.

The results surfaced five confirmed runtime issues, one coverage gap, and two contract/scope concerns.

1. Selected native paths can be silently changed before reaching the agent

The agent path handling currently converts native paths through to_string_lossy().

I reproduced this with a selected filename containing the native byte 0xff.

How to reproduce

Create a file with an invalid UTF-8 byte in its native name, configure a small argv-dumping agent, select the file and choose Open with AI agent.

The probe compared the original native bytes with what the agent actually received.

Expected native suffix:

6261642dff2e747874

Received by the agent:

6261642defbfbd2e747874

The original ff became the UTF-8 encoding of the Unicode replacement character:

ef bf bd

The agent still started successfully and no error was shown.

So the selected file path was silently changed before reaching the process.

finding-1-agent-non-utf8-path.mp4
finding-1-agent-non-utf8-path

Suggested direction

I would preserve native paths until the process boundary.

If the VTE/GLib API cannot represent a path exactly, fail safely:

path cannot be represented exactly
→ do not start the agent
→ do not destroy the existing session
→ show a clear error

The important invariant is that a selected native path must never silently become a different path.

The resolved executable path should follow the same rule.

2. A shell builtin waiting for input can be killed and replaced by the agent

This is an interaction with the PromptReady problem from PR2, but PR3 makes the consequence significantly more destructive.

PR3 uses shell_is_waiting() to determine whether the current terminal session is safe to replace.

The problem is that tcgetpgrp() == shell pid does not mean the shell is sitting at an empty command prompt.

A builtin such as read still runs inside the shell process.

How to reproduce

Open the embedded terminal and run:

echo $$ > .read-shell-pid
read -p 'waiting for input> ' value

Do not provide any input.

Once the terminal visibly shows:

waiting for input>

use the browser context menu and choose Open with AI agent.

Observed:

the agent starts
the old shell PID becomes zombie/disappears
read never completes normally

The busy-terminal guard does not activate.

The shell is simply terminated and replaced.

finding-2-busy-builtin-agent-replacement.mp4

Suggested direction

I would not use the current shell_is_waiting() predicate for destructive replacement.

Until PR2 has a reliable fresh-prompt signal, the conservative behavior would be:

existing terminal session
→ require explicit close before replacement

Longer term, safe replacement could require both:

fresh prompt confirmed
+
no external foreground process

This is inherited from PR2, but PR3 amplifies the impact from "command injected in the wrong place" to "active session can be destroyed".

3. A real asynchronous spawn failure can kill the old shell and crash Strata

This turned out to be more serious than the initial code review suggested.

The synchronous preflight catches things such as a missing executable, but a command can pass that validation and still fail when VTE actually tries to execute it.

How to reproduce

Create an executable file whose interpreter does not exist:

printf '%s\n' '#!/definitely/not/a/real/interpreter' 'echo impossible' > /tmp/strata-bad-agent
chmod +x /tmp/strata-bad-agent

Configure:

/tmp/strata-bad-agent

as the AI agent command.

Open a normal shell first and establish that it is alive.

Then choose Open with AI agent.

Observed:

OBS_MENU_CLOSE_FAILURE returncode=-11
OBS_FINDING_3_CRASH returncode=-11 old_shell_state=Z

Strata terminated with SIGSEGV.

The previous shell was already a zombie and no user-facing error dialog appeared.

The recording ends with the application gone.

As a control, starting the same invalid agent without an existing session did not crash Strata. It remained alive and only logged:

Failed to execve: No such file or directory

internally.

finding-3-agent-spawn-failure-loses-shell.mp4

Suggested direction

Agent replacement should behave transactionally.

Conceptually:

prepare replacement
        ↓
attempt spawn
        ↓
spawn confirmed
        ↓
adopt replacement session

A failed spawn should result in:

visible launch error
+
coherent TerminalPanel state
+
generic terminal still usable

Ideally, the previous session should not be irreversibly destroyed before the replacement is known to have started successfully.

At minimum, every asynchronous spawn failure must become a user-visible failure and return the panel to a valid lifecycle state.

4. The main agent process boundary still has no committed E2E test

This is not a runtime bug, but I think it should be addressed before this stack is considered ready.

I checked the actual PR2 to PR3 delta:

git diff --name-only <PR2> <PR3> | rg '^tests/e2e/'

No E2E file is added or modified.

I also searched the PR3 tree for agent-specific E2E coverage and found none.

The existing test_embedded_terminal.py belongs to the earlier terminal/cwd work. It does not exercise:

context menu
→ AgentRequest
→ TerminalPanel
→ VTE
→ real child process
→ cwd + argv

The temporary review probe exercised exactly that boundary and was able to uncover the runtime failures above, but it was removed afterward and obviously does not count as committed regression coverage.

Suggested direction

I would keep a very small fixture agent in E2E that records:

cwd
argv

and cover at least:

background action
multiple selection
argument boundaries
busy-session refusal
spawn failure
navigation while agent owns the terminal

It does not need to be a large test suite.

One real integration scenario would provide much more protection here than additional unit tests around argv construction.

5. {} inside a word combines multiple selected paths into a single argument

This behavior is confirmed, but I would classify it as a contract/spec concern rather than a runtime bug.

How to reproduce

Configure:

/tmp/strata-review-agent-probe 'review {} please'

Select:

first file.rs
second;file.rs

Observed:

ARGC=1
ARGV[1]='review /tmp/.../first file.rs /tmp/.../second;file.rs please'

Both selected paths are combined into one argument.

For comparison, using a bare placeholder:

/tmp/strata-review-agent-probe {}

produces:

ARGC=2

with one path per argument.

finding-5-placeholder-merges-paths.mp4
finding-5-placeholder-merges-paths

The implementation is doing exactly what the current PR documents, so this is not accidental behavior.

The question is whether we want that contract in this PR.

The original #93 direction was intentionally simpler:

selected paths
→ structured process arguments
→ one path per argument

The embedded-placeholder form starts introducing prompt templating semantics.

Suggested direction

If we want to preserve the original PR3 boundary, I would keep:

no {}
→ append each path as a separate argv

bare {}
→ expand to one argv per path

and leave:

"review {} please"

style prompt interpolation for a later feature.

If prompt templating is intentionally part of PR3 now, then I think the change should at least be called out explicitly as an expansion of the original contract.

6. Settings search currently contains provider-specific aliases

This is also a scope/consistency issue rather than a functional bug.

The PR says that Strata does not name or assume a specific provider.

However, the Settings search aliases currently include:

terminal embedded agent cli claude codex gemini command

I verified from the actual Settings UI that searching for:

codex
claude
gemini

all resolves to the AI agent command setting.

finding-6-provider-specific-search

Suggested direction

If strict provider-neutrality is the intended boundary, I would simply use generic aliases:

terminal
embedded
agent
cli
command

This is a very small cleanup, not something I would block the whole PR on by itself.

7. A relative configured executable is resolved against Strata's process cwd, not the agent directory

This is a confirmed runtime bug.

How to reproduce

Create:

<fixture>/relative-target/agent-probe

and make it executable.

Do not create:

<fixture>/agent-probe

Navigate Strata into:

<fixture>/relative-target

Configure:

./agent-probe

as the AI agent command.

Then choose Open with AI agent.

Observed:

Unable to start the agent
“./agent-probe” was not found on your PATH

even though ./agent-probe exists and is executable in the directory where the agent is supposed to run.

The test confirmed that the Strata process cwd was the parent fixture directory while the agent target cwd was relative-target.

finding-7-relative-agent-wrong-cwd.mp4
finding-7-relative-agent-wrong-cwd

Suggested direction

Executable resolution should distinguish:

bare program
→ PATH lookup

absolute path
→ use it directly

relative path containing /
→ resolve against AgentRequest.directory

The resolution semantics should match the cwd the child will actually receive, not Strata's administrative process cwd.

8. replacing: bool can misattribute child-exited events and leave the panel in a broken state

I originally considered this a race hypothesis.

It is reproducible.

The test used:

old shell:
  receives HUP
  delays exit by 150 ms

replacement agent:
  starts
  exits immediately

and repeatedly replaced the shell.

Ten independent Strata executions were tested.

Every execution failed on the first replacement cycle.

Observed event order:

SHELL_START
SHELL_HUP
AGENT_START

The agent started and exited while the old shell was still inside its HUP handler.

After hiding and showing the terminal with F4, no new shell was created.

Strata remained alive, but the terminal panel was visible with only the previous:

AGENT_STARTED

output and no functional prompt.

finding-8-session-replacement-race.mp4
finding-8-session-replacement-race

Why this happens

The replacement lifecycle is represented by a global boolean:

replacing = true

and whichever child-exited callback arrives while that flag is set consumes it.

There is no identity connecting an exit event to the session generation it belongs to.

Once old and new child lifetimes overlap, event ordering becomes ambiguous.

Suggested direction

I would not represent session identity with a boolean.

Use either:

session generation / session ID

so callbacks can identify which session they belong to, or serialize replacement:

terminate old session
→ observe old session exit
→ spawn new agent

depending on what VTE lifecycle semantics make easiest.

The important part is that an exit from one session must never mutate the lifecycle state of another session.

Happy-path sanity check

I also verified that a number of the intended behaviors are working correctly:

background action
→ correct cwd

multiple selection
→ correct cwd and argv boundaries

spaces, quotes and semicolons
→ preserved as process arguments

bare {}
→ one argument per selected path

browser navigation while agent runs
→ no cwd command injected into the agent

empty command
→ action hidden

The existing PR2 terminal E2Es also continue to pass, along with the selected terminal-panel and context-menu test groups used during this review.

So I don't think the basic PR3 architecture needs to be thrown away.

Relationship with PR1 and PR2

PR1's previously reported ownership/lifecycle issues have been addressed in the current stack.

The main inherited issue that matters here is the PR2 definition of an "idle" shell.

In PR2, a false shell_is_waiting() result could cause cwd synchronization to write into the wrong context.

In PR3, that same false positive is now used to authorize:

HUP current shell
→ replace session with agent

so the impact is significantly higher.

I would resolve the PromptReady model in PR2 before relying on it for destructive replacement in PR3.

Overall

I still think the layering is good:

generic embedded terminal
        ↓
browser cwd sync
        ↓
provider-agnostic agent action

The problems are concentrated in the process boundary and replacement lifecycle, not in the overall architecture.

The main blockers I would address before considering PR3 ready are:

  1. preserve native selected paths without lossy conversion;
  2. do not use the current shell_is_waiting() predicate to authorize destructive replacement;
  3. make asynchronous spawn failure safe and user-visible;
  4. fix the session replacement race by identifying session generations rather than using one global boolean;
  5. resolve relative executables against the requested agent cwd;
  6. add one real E2E test covering the agent process boundary.

The {} prompt interpolation and provider-specific Settings aliases are smaller contract/scope questions rather than runtime blockers.

All runtime reproductions were performed in the isolated canonical E2E environment. Temporary probes, helpers and logs were removed afterward, the checkout was left clean, and the retained recordings were successfully decoded with ffmpeg.

Add a provider-agnostic agent action on top of the embedded terminal. A
configurable command, empty by default, runs in the panel; Strata never
names, ships, or requires a particular agent, and the action stays
hidden until a command is set.

The folder background runs the agent in that folder. A selection runs it
in the folder that holds the selection. Where the selected paths land is
up to the command: a bare {} word becomes one argument per path, for a
tool that takes files, and {} inside a word substitutes them into that
word, for a tool that takes a single prompt. Without {} they follow the
command. They are process arguments throughout, so a name containing
spaces, quotes or shell metacharacters is never reinterpreted. The
program is resolved on PATH before spawning, because VTE spawns without
a path search.

An agent replaces the panel's session, but only when nothing is running
in it; a busy terminal reports that instead of losing the work. An agent
that exits leaves its output on screen with a line saying how it ended,
so one that fails at startup does not take its own error message away.

The command entry says whether what is typed has been saved, since
committing on Enter was otherwise invisible.

Refs lgse#93
@spandan11106
spandan11106 force-pushed the feat/93-terminal-agent-actions branch from cb23646 to e3cbb90 Compare September 17, 2026 19:39
@spandan11106
spandan11106 changed the base branch from feat/93-terminal-cwd-sync to feat/93-embedded-terminal September 17, 2026 19:39
@spandan11106

Copy link
Copy Markdown
Collaborator Author

Retargeted onto #1089 and rebuilt on top of it, now that #1093 is closed. The branch no longer carries the cwd synchronisation work, so the diff here is just the agent actions: one commit, 14 files.

Nothing in the feature itself changed — the earlier follow-up comment still describes it accurately. The {} placeholder, the save confirmation on the command entry, and an exited agent leaving its output on screen are all folded into the single commit rather than sitting in separate ones.

Selected paths went through a lossy conversion, so a native name that is
not valid UTF-8 reached the agent as a different path and the launch
still reported success. Paths and the command's own words are now only
ever passed exactly; one that cannot be is reported and stops the
launch.

A configured command containing a slash resolved against Strata's own
working directory, so ./agent next to the folder being browsed was
reported as missing. Relative commands now resolve against the folder
the agent will run in, absolute ones are used directly, and a bare name
still comes from PATH.

An agent no longer replaces a live session. Nothing here can tell a
shell waiting at its prompt from one waiting inside read, so replacing
one meant killing whatever it was doing; overlapping the old child's
exit with the new spawn could also leave the panel with no session, or
take Strata down with it. The action now asks for the session to be
closed first, which removes the kill, the race and the crash together.

A spawn that fails inside VTE arrives long after the menu has closed.
It used to hide the panel, taking the agent's own error with it; it now
leaves the panel open with the failure, and the terminal stays usable.

Adds end-to-end coverage of the whole agent boundary: the background
action's folder, a selection's arguments, the refusal, and a launch that
fails late.

Refs lgse#93
@spandan11106

Copy link
Copy Markdown
Collaborator Author

Thanks — this was a very useful pass. Findings 1, 2, 3, 4, 7 and 8 are fixed in a8afc8f. 6 is done. 5 I have kept, deliberately, and called out below.

Note the stack changed under you while you were reviewing: #1093 is closed, and this branch has been rebuilt on top of #1089. The reasoning is on that PR, but the short version is your own conclusion — without a fresh-prompt signal, and with VTE 0.76 on the current base image, there is no sound way to inject into a live shell, so the synchronisation work is deferred to #1103.

1. Lossy native paths

Fixed. Paths and the command's own words now go through Path::to_str(), never to_string_lossy(). A path that cannot be passed unchanged stops the launch with a named error instead of reaching the agent as a different file. The resolved executable follows the same rule. Unit test: a_path_that_cannot_be_passed_exactly_stops_the_launch.

2, 3 and 8. Replacement

You were right that this was the dangerous one, and the three findings share a root: replacement itself. Your suggested direction — require an explicit close — removes all three at once, so that is what it does now.

Open with AI agent no longer terminates anything. With a session open it reports that the session must be closed first, and does not spawn. That means:

  • no shell is ever killed on a guess about whether it is idle, so the read case cannot happen (2);
  • there is no old child whose exit can overlap a new spawn, so the replacing boolean is gone entirely rather than being replaced by a generation counter (8);
  • nothing has been destroyed by the time a late spawn failure arrives (3).

The idle predicate that authorised replacement is deleted, along with the rustix termios feature it needed.

3. Late spawn failures

Also fixed on its own terms. A spawn that passes validation and then fails inside VTE used to hide the panel, taking the agent's own error off screen. It now leaves the panel open with the failure reported in it, and the panel stays usable — F4 twice gives a working shell. Covered end to end with a bad-interpreter agent.

4. E2E coverage

Added tests/e2e/scenarios/test_agent_actions.py, with a fixture agent that records its cwd and argv:

  • background action → correct folder, no path arguments;
  • multiple selection → correct folder, one argument per selected path;
  • refusal with a session open → dialog shown, no agent started, and the existing shell still answers with the same pid afterwards;
  • a launch that fails late → panel stays open, Strata survives, terminal still usable.

Four scenarios, passing alongside the two from #1089.

5. {} inside a word

Kept, and you are right that it deserves to be called out rather than slipped in, so: this PR deliberately expands the original contract.

The reason is that the original one does not work for the agents most people will configure. claude --help is claude [options] [command] [prompt] — one positional, treated as a prompt. Codex is the same. Appending paths as separate arguments means the first becomes the prompt and the rest are dropped, silently, which is exactly what the first person to try it hit. aider file1 file2 is the shape the original contract assumed, and it is the minority.

So the contract is now: a bare {} expands to one argument per path, no {} appends them, and {} inside a word substitutes them into that word. The first two are the original behaviour unchanged. I would rather have the third than ship an action that quietly ignores most of a selection for two of the three agents named in #93.

Happy to split it out if you would still prefer that, but it would leave the feature broken for its most likely users.

6. Provider-specific search aliases

Fixed. The aliases are now terminal embedded agent cli command shell.

7. Relative executables

Fixed. Resolution now distinguishes the three cases you listed: an absolute path is used directly, a path containing a slash resolves against the requested agent directory, and a bare name comes from PATH. Unit test: a_relative_command_resolves_against_the_folder_the_agent_runs_in, which also asserts the negative — that Strata's own working directory is not what decides it.

@spandan11106

Copy link
Copy Markdown
Collaborator Author

Correction on the video above: it was recorded before a8afc8f, so two points in its description no longer match the branch.

Everything else in the recording still holds. I will re-record the replacement section rather than leave the stale take standing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants