Skip to content

feat(deployments): Browse container files and bring them onto the host#81

Merged
nfebe merged 4 commits into
mainfrom
feat/container-files-browser
Jul 15, 2026
Merged

feat(deployments): Browse container files and bring them onto the host#81
nfebe merged 4 commits into
mainfrom
feat/container-files-browser

Conversation

@nfebe

@nfebe nfebe commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

The UI side of flatrun/agent#139. Files could browse and edit a deployment's own directory but not what the running container holds, which is usually where the configuration a user wants lives. The only way to it was a terminal.

Files now switches between the host and a running service. A path brought out of the container lands back in the host view where the existing editor already works, so there is no second editor.

The opposite direction already existed, mounting a host path into a container, which hides whatever the container had there. That asymmetry is what made the container side hard to reach.

Mounted paths carry a tag naming what reads them and where. Their warnings follow what happens: deleting a mounted path takes it from the running container, because the host copy is what the container reads; unmounting first returns the service to its image's content and leaves the copy on the host, where deleting it is harmless.

Browsing needs the service running and an image that ships a shell, since the engine cannot list a directory.

Depends on flatrun/agent#172 for its endpoints.

nfebe added 4 commits July 15, 2026 15:41
A deployment's own directory is browsable and editable, but whatever the
running service keeps to itself is not, and that is usually where the
configuration a user wants to read lives. Today the only way to it is a
terminal.

The new tab lists what a running service holds and can bring any path onto
the host, after which the existing Files tab edits it and the container
reads the same content. This is the opposite direction to mounting a host
path into a container, which hides whatever was there.

Browsing needs the service running and an image that ships a shell, since
the engine cannot list a directory. That case is explained rather than
left as a bare failure.
Container files were their own tab, which split browsing across two
places when the two are the same task: find a path, then read or edit it.
Files now switches between the host and a running service, and bringing a
path out of the container drops straight back to the host view where it
can be edited.

The panel had also styled itself, so it sat flush against the tab's edges
and drew colours the theme does not set, which left it looking unlike the
rest of the app in either theme. It uses the design tokens now, and scrolls
a long directory inside the pane rather than stretching the tab.
The switcher had space above it and none below, so it sat high in its row
rather than centred, and its row did not line up with the file browser's
toolbar directly beneath it.
A path a container reads through a bind mount looked like any other file,
so there was nothing to say that editing or deleting it reaches inside the
running container, and no way to disconnect it short of editing the compose
file by hand.

Mounted paths now carry a tag naming what reads them and where, and can be
unmounted from the row menu. The warnings follow what actually happens:
deleting a mounted path takes it from the container too, because the host
copy is what the container reads; unmounting first sends the service back
to its image's content and leaves the copy on the host, where deleting it
is then harmless.
@sourceant

sourceant Bot commented Jul 15, 2026

Copy link
Copy Markdown

Code Review Summary

This PR introduces a significant feature allowing users to browse files inside running containers and 'materialize' them onto the host for editing. It includes a new ContainerFilesPanel component, updates to FileBrowser to display mount status, and corresponding API integrations.

🚀 Key Improvements

  • Ability to edit container-native configuration files by bringing them to the host.
  • Visual indicators (mount tags) in the file browser to show which paths are active bind mounts.
  • Safety warnings when deleting host files that are currently mounted into containers.

💡 Minor Suggestions

  • Extract formatBytes to a shared utility.
  • Improve error reporting when unmounting from multiple services simultaneously.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

}
};

const formatBytes = (bytes: number) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Math.log(0) case is mathematically undefined and Math.log(bytes) will return -Infinity if bytes is 0. While the if (!bytes) check at the start handles 0, it won't handle cases where bytes is less than 1 (though rare for file sizes). A more robust approach ensures the index is at least 0.

Suggested change
const formatBytes = (bytes: number) => {
const formatBytes = (bytes: number) => {
if (bytes <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
return `${(bytes / Math.pow(1024, i)).toFixed(i ? 1 : 0)} ${units[i]}`;
};

try {
// A path can be mounted into more than one service, and it stays mounted
// until the last one lets go.
for (const mount of mounts) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When unmounting a path that is used by multiple services, perform the deletions sequentially with better error handling, or ideally use Promise.allSettled if you want to attempt all even if one fails. Currently, a single failure stops the loop, potentially leaving the UI in an inconsistent state regarding what is actually mounted.

Suggested change
for (const mount of mounts) {
const results = await Promise.allSettled(
mounts.map((mount) =>
deploymentsApi.removeComposeMount(props.deploymentName, {
source_path: mount.source,
target_path: mount.target,
service_name: mount.service,
})
)
);
const failed = results.filter((r) => r.status === "rejected");
if (failed.length > 0) throw new Error(`Failed to remove ${failed.length} mount(s)`);

@nfebe
nfebe merged commit 816abc1 into main Jul 15, 2026
5 checks passed
@nfebe
nfebe deleted the feat/container-files-browser branch July 15, 2026 16:11
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.

1 participant