Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/developer/AGENT_RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ The runtime keeps provider-specific code out of the core app.

Adapters know how to talk to a specific backend or protocol.

The composed system prompt includes the live registry catalog plus bounded
session context: cwd, space name, active editor file, selected diff file, open
browser tabs, terminal status, running dev-server URLs, recent browser console
errors, current git summary, and project instruction files discovered from the
session cwd (`AGENTS.md`, `.cursorrules`, `CLAUDE.md`, and GitHub Copilot
instructions). Prompt text also states precedence: app safety and tool schemas
win first, then the latest user request, then project instruction files with
more-specific files overriding broader ones.

## Session Model

An agent session contains:
Expand Down
10 changes: 6 additions & 4 deletions docs/developer/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ use, discovery data lives under `~/.meith`:
protocol version, user data path, instance path, and managed CLI launcher.
- `~/.meith/instances/<pid>.json` records live runtime instances so the CLI can
list, target, or kill them.
- `<userData>/state.json` stores spaces, projects, browser tabs, workspace tabs,
file edit events, plugins, and app settings.
- `<userData>/state.json` stores spaces, projects, browser tabs, workspace tabs
(including editor file focus and diff-tab selected file), file edit events,
plugins, and app settings.
- `<userData>/logs.jsonl` stores app logs.
- `<userData>/audit.jsonl` stores tool authorization/audit records.
- `<userData>/artifacts/` stores screenshot and bug-report artifacts.
Expand Down Expand Up @@ -176,8 +177,9 @@ Major surfaces:
- `TerminalView` for PTY sessions.
- `AgentView` for session list, transcript, composer, stop button, and
permission cards.
- `DiffView` for a working-tree diff tab with a folder tree, cached summary
counts, and lazy patch loading for the selected file.
- `DiffView` for a working-tree diff tab with a folder tree, refreshed summary
counts, persisted selected-file state, and lazy patch loading for the selected
file.
- `SettingsView` for app preferences, per-project run commands, agent config,
plugin management, and about info.
- `DebugPanel` for tool runner, state, logs, and output diagnostics.
Expand Down
3 changes: 2 additions & 1 deletion docs/developer/TOOL_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ and unstaged changes against `HEAD` plus untracked files:

`includePatches: false` returns file status and line-count summaries without
patch bodies. The renderer uses that cheap summary for the top-bar diff chip and
polling. The diff tab then requests a selected file on demand:
visible diff tab, refreshing while they are shown. The diff tab then requests a
selected file on demand:

```json
{
Expand Down
6 changes: 6 additions & 0 deletions docs/user/SPACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Within each workspace you can have:

- browser tabs for previewing localhost or research,
- editor tabs for project files,
- diff tabs for reviewing git working-tree changes,
- terminal tabs,
- agent chat tabs,
- run commands and environment configurations,
Expand All @@ -33,6 +34,11 @@ Split panes to arrange your browser, editor, terminal, or agent next to each
other. A common layout is an agent chat on the left with a live localhost preview
on the right, so you can watch changes land as the agent works.

Clicking inside a pane makes that pane active and highlights its active tab in
orange. Opening the git diff from the top bar defaults to the right pane,
creating split view from a single-pane layout; you can still drag the diff tab
elsewhere.

## Run commands & preview

Start and stop your project's run command from the top bar, then preview the
Expand Down
4 changes: 2 additions & 2 deletions docs/user/TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ they can request immediately.
| --- | --- | --- |
| App diagnostics | Inspect app state, logs, health, instances, screenshots, debug mode, and bug reports. | `app_get_state`, `app_get_logs`, `app_list_instances`, `app_health`, `app_screenshot`, `app_set_debug_mode`, `app_export_bug_report` |
| Browser control | Open, focus, navigate, inspect, automate, screenshot, and debug browser tabs. | `get_tabs`, `get_active_tab`, `open_browser_tab`, `navigate`, `go_back`, `go_forward`, `refresh`, `focus_browser_tab`, `close_browser_tab`, `browser_use_start`, `browser_use_end`, `get_browser_state`, `click_element`, `type_text`, `scroll_page`, `send_keys`, `cdp_command`, `get_console_logs`, `get_network_logs`, `take_screenshot` |
| Spaces & workspace tabs | Manage spaces plus editor, terminal, agent, and preview tabs inside them. | `list_spaces`, `create_space`, `update_space`, `switch_space`, `close_space`, `open_workspace_tab`, `set_workspace_tab_file`, `set_workspace_tab_terminal`, `focus_workspace_tab`, `close_workspace_tab` |
| Spaces & workspace tabs | Manage spaces plus editor, terminal, agent, preview, and diff tabs inside them. `set_workspace_tab_file` tracks editor focus/open files and the selected file in a diff tab. | `list_spaces`, `create_space`, `update_space`, `switch_space`, `close_space`, `open_workspace_tab`, `set_workspace_tab_file`, `set_workspace_tab_terminal`, `focus_workspace_tab`, `close_workspace_tab` |
| Projects | Detect, open, generate, prewarm, allocate, configure, run, and stop projects. | `project_list`, `project_detect`, `project_open`, `project_start_dev_server`, `project_stop_dev_server`, `project_run`, `project_set_run_config`, `project_list_templates`, `project_create`, `project_create_plugin`, `project_prewarm`, `project_prewarm_status`, `project_allocate` |
| Files & diagnostics | List, read, search, edit, undo, and inspect TypeScript or JavaScript diagnostics. | `workspace_list_files`, `workspace_read_file`, `workspace_search`, `workspace_write_file`, `workspace_apply_patch`, `workspace_undo`, `get_diagnostics` |
| Git | Inspect a project's working-tree changes as cached summaries, per-file counts, and lazy-loaded patches. | `git_diff` |
| Git | Inspect a project's working-tree changes as refreshed summaries, per-file counts, and lazy-loaded patches. | `git_diff` |
| Processes & terminals | Create terminals, manage dev servers, inspect process trees, and read or stream logs. | `create_terminal`, `list_terminals`, `write_terminal`, `resize_terminal`, `kill_terminal`, `close_terminal`, `get_terminal_snapshot`, `start_dev_server`, `list_dev_servers`, `stop_dev_server`, `get_process_tree`, `get_process_logs`, `attach_process_logs` |
| Storage | Inspect durable storage collections and export persisted state for backup or debugging. | `storage_list_collections`, `storage_read_collection`, `storage_export_state` |
| Plugins | Install, grant, enable, open, list, and uninstall controlled plugin tabs. | `list_plugins`, `install_plugin`, `approve_plugin_grants`, `set_plugin_enabled`, `open_plugin_tab`, `uninstall_plugin` |
Expand Down
8 changes: 7 additions & 1 deletion docs/user/USING_MEITH.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ outside in, the window is made of a few stable regions:
scopes every tab you open to that project.
2. Split the workspace so you can see more than one thing at once. A common
layout is an agent chat on the left and a live `localhost` preview on the
right.
right. Clicking inside either pane makes that pane active; its active tab is
highlighted in orange.
3. Start your project's run command from the title bar. The status bar shows the
running server and its port, and the embedded browser tab can point straight
at it.
Expand All @@ -55,6 +56,11 @@ project's working-tree changes, including staged, unstaged, and brand-new files,
and presents them as per-file diffs with added and removed line counts. You can
see exactly what was rewritten before you keep it.

The git diff view opens on the right by default. From a single-pane layout,
opening the diff starts split view; from an existing split layout, it focuses or
moves the diff tab into the right pane. The tab remains draggable if you want a
different arrangement.

> **Edits are reversible**
>
> File edits land as reviewable diffs, and a workspace-level undo lets you roll
Expand Down
10 changes: 10 additions & 0 deletions docs/user/WORKING_WITH_AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ project. It can read and edit your app's code, run the dev server, drive an
embedded browser to check the live preview, and inspect dev-server logs. All of
that happens through the same shared tool registry the rest of the app uses.

Before each run, meith also gives the agent a bounded snapshot of the workbench:
the active editor file, selected diff file, open browser tabs, terminal and dev
server status, recent browser console errors, the current git summary, and
project-specific instruction files such as `AGENTS.md` when they exist.

## Provider-agnostic by design

meith does not lock you into one AI provider. The agent runtime uses an adapter
Expand Down Expand Up @@ -47,6 +52,11 @@ provider-native helpers cannot bypass the shared registry.
> When an agent edits a file, the change lands as an inline diff with a gutter
> marker, so you can see exactly what was rewritten and undo it.

The top-bar diff chip and the diff tab refresh while visible, so changes made by
agents, tools, or terminal commands should appear without waiting for a manual
refresh. Opening the git diff from the top bar opens it on the right side by
default, creating a split pane when needed.

## Learn more

For how permissions and tool calls work under the hood, see
Expand Down
116 changes: 116 additions & 0 deletions packages/desktop/src/main/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { buildSystemPrompt, renderToolCatalog } from "../agent/systemPrompt.js";
import type { AgentAdapter, AgentSession, AgentStreamChunk } from "../agent/types.js";
import { AgentConfigStore } from "../services/AgentConfigStore.js";
import { AgentService } from "../services/AgentService.js";
import { AppStateService } from "../services/AppStateService.js";
import { Logger } from "../services/Logger.js";
import { ToolRegistry } from "../tools/registry.js";

Expand Down Expand Up @@ -83,6 +84,63 @@ describe("system prompt builder", () => {
it("handles an empty registry gracefully", () => {
expect(renderToolCatalog([])).toContain("No tools are currently registered");
});

it("renders live IDE context, instructions, and precedence rules", () => {
const prompt = buildSystemPrompt([], {
cwd: "/repo",
spaceName: "Project",
activeEditorFile: { tabTitle: "Editor", cwd: "/repo", path: "src/app.ts" },
selectedDiffFile: { tabTitle: "Diff", cwd: "/repo", path: "src/app.ts" },
openTabs: [{ title: "Local app", url: "http://localhost:3000" }],
terminals: [
{
id: "term_1",
tabTitle: "Terminal",
cwd: "/repo",
status: "running",
pid: 123,
exitCode: null,
active: true,
},
],
devServers: [
{
id: "dev_1",
cwd: "/repo",
status: "running",
command: "pnpm dev",
url: "http://localhost:3000",
pid: 456,
},
],
consoleErrors: [
{
tabTitle: "Local app",
url: "http://localhost:3000",
text: "Uncaught Error: boom",
},
],
git: {
branch: "main",
status: "changes",
summary: "1 changed file(s)",
files: ["M src/app.ts"],
},
instructionFiles: [
{ path: "/repo/AGENTS.md", content: "Use pnpm.", truncated: false },
],
});

expect(prompt).toContain("## Instruction precedence");
expect(prompt).toContain("latest user request defines the task");
expect(prompt).toContain("## Project instructions");
expect(prompt).toContain("Use pnpm.");
expect(prompt).toContain("Active editor file: `src/app.ts`");
expect(prompt).toContain("Selected diff file: `src/app.ts`");
expect(prompt).toContain("http://localhost:3000");
expect(prompt).toContain("Uncaught Error: boom");
expect(prompt).toContain("Git: changes on main");
});
});

describe("ACP adapter prompt", () => {
Expand Down Expand Up @@ -473,6 +531,64 @@ describe("AgentService host context", () => {
// The host exposes a registry-derived prompt to the adapter.
expect(captured.systemPrompt).toContain("__probe");
});

it("builds prompt context from app state and project instruction files", async () => {
const { mkdtempSync, writeFileSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const cwd = mkdtempSync(join(tmpdir(), "meith-agent-context-"));
writeFileSync(join(cwd, "AGENTS.md"), "Use pnpm for this project.\n", "utf8");

const logger = new Logger();
const appState = new AppStateService(join(cwd, "state.json"), logger, 0);
const spaceId = appState.getState().activeSpaceId ?? appState.getState().spaces[0].id;
appState.update((draft) => {
draft.workspaceTabs.push(
{
id: "w_editor",
spaceId,
title: "Editor",
cwd,
kind: "editor",
active: false,
activeFilePath: "src/app.ts",
createdAt: 1,
},
{
id: "w_diff",
spaceId,
title: "Diff",
cwd,
kind: "diff",
active: true,
selectedDiffFilePath: "src/app.ts",
createdAt: 2,
},
);
});
const registry = new ToolRegistry();
registry.register(
defineTool({
name: "__probe",
description: "captures call context",
inputSchema: z.object({}),
execute: () => ({}),
}),
);
const service = new AgentService(registry, logger, { appState });
const { adapter, captured } = makeCapturingAdapter();
service.registerAdapter(adapter);
const session = service.createSession({ cwd, spaceId });

for await (const _chunk of service.run(session.id)) {
void _chunk;
}

expect(captured.systemPrompt).toContain("Use pnpm for this project.");
expect(captured.systemPrompt).toContain("Active editor file: `src/app.ts`");
expect(captured.systemPrompt).toContain("Selected diff file: `src/app.ts`");
expect(captured.systemPrompt).toContain("Git: not a git repository");
});
});

describe("AgentService permission model", () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/src/main/__tests__/electronHardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ describe("ElectronBrowserViewHost Electron permission hardening", () => {
permissionHandler = null;
windowOpenHandler = null;
// Reset the mock factory so each test gets its own view.
const { WebContentsView } = vi.mocked(
await import("electron"),
) as { WebContentsView: ReturnType<typeof vi.fn> };
const { WebContentsView } = vi.mocked(await import("electron")) as unknown as {
WebContentsView: ReturnType<typeof vi.fn>;
};
WebContentsView.mockImplementation(() => makeMockView());
});

Expand Down
7 changes: 4 additions & 3 deletions packages/desktop/src/main/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join } from "node:path";
import { ToolClient } from "@meith/cli/client";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { type ServiceContainer, bootstrap } from "../bootstrap.js";
import { CURRENT_STATE_VERSION } from "../storage/migrations.js";

function sendRawFrame(
socketPath: string,
Expand Down Expand Up @@ -322,8 +323,8 @@ describe("socket integration", () => {
dataDirectory: string;
state: { version: number };
};
expect(snapshot.stateVersion).toBe(4);
expect(snapshot.state.version).toBe(4);
expect(snapshot.stateVersion).toBe(CURRENT_STATE_VERSION);
expect(snapshot.state.version).toBe(CURRENT_STATE_VERSION);
});

it("returns app health and live instances over the socket", async () => {
Expand Down Expand Up @@ -389,7 +390,7 @@ describe("socket integration", () => {
};
expect(content.path).toBeTruthy();
expect(content.report.schema).toBe("meith-bug-report/v1");
expect(content.report.stateSummary.version).toBe(4);
expect(content.report.stateSummary.version).toBe(CURRENT_STATE_VERSION);
expect(content.report.toolRegistry.some((t) => t.name === "app_health")).toBe(true);
});
});
Expand Down
15 changes: 15 additions & 0 deletions packages/desktop/src/main/__tests__/spaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ describe("BrowserTabService workspace tabs", () => {
expect(cleared.terminalId).toBeUndefined();
});

it("persists the selected file on diff workspace tabs", () => {
const tab = ctx.tabs.openWorkspaceTab({
title: "Diff",
cwd: "/tmp/project",
kind: "diff",
});

const updated = ctx.tabs.setWorkspaceTabFile(tab.id, {
selectedDiffFilePath: "src/app.ts",
});

expect(updated.selectedDiffFilePath).toBe("src/app.ts");
expect(ctx.tabs.listWorkspaceTabs()[0].selectedDiffFilePath).toBe("src/app.ts");
});

it("rejects terminal session ids on non-terminal workspace tabs", () => {
const tab = ctx.tabs.openWorkspaceTab({ title: "Editor", cwd: "/tmp/project" });
expect(() => ctx.tabs.setWorkspaceTabTerminal(tab.id, "term_123")).toThrow(
Expand Down
51 changes: 49 additions & 2 deletions packages/desktop/src/main/__tests__/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,22 @@ describe("migrations", () => {

it("passes through a valid current-version state", () => {
const valid = {
version: 3,
spaces: [{ id: "s1", name: "S", createdAt: 1 }],
version: CURRENT_STATE_VERSION,
spaces: [{ id: "s1", name: "S", projectId: null, createdAt: 1 }],
activeSpaceId: "s1",
browserTabs: [],
workspaceTabs: [],
projects: [],
workspaceFileEvents: [],
plugins: [],
settings: {
autoRunOnOpen: false,
confirmOnClose: true,
stopServersOnClose: true,
showOutputOnRun: true,
defaultPackageManager: "unknown",
debugMode: false,
},
};
expect(migrateAppState(valid).activeSpaceId).toBe("s1");
});
Expand Down Expand Up @@ -116,6 +126,43 @@ describe("migrations", () => {
});
});

it("migrates v4 -> v5 by preserving selected diff file state", () => {
const v4 = {
version: 4,
spaces: [{ id: "s1", name: "S", createdAt: 1, projectId: null }],
activeSpaceId: "s1",
browserTabs: [],
workspaceTabs: [
{
id: "w1",
spaceId: "s1",
title: "Diff",
cwd: "/tmp/proj",
kind: "diff",
selectedDiffFilePath: "src/app.ts",
active: true,
createdAt: 1,
},
],
projects: [],
workspaceFileEvents: [],
plugins: [],
settings: {
autoRunOnOpen: false,
confirmOnClose: true,
stopServersOnClose: true,
showOutputOnRun: true,
defaultPackageManager: "unknown",
debugMode: false,
},
};

const migrated = migrateAppState(v4);

expect(migrated.version).toBe(CURRENT_STATE_VERSION);
expect(migrated.workspaceTabs[0].selectedDiffFilePath).toBe("src/app.ts");
});

it("throws on a newer-than-supported version", () => {
expect(() => migrateAppState({ version: 99 })).toThrow();
});
Expand Down
Loading
Loading