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
12 changes: 12 additions & 0 deletions apps/backend/src/computer-use/computer-use.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ describe('Settings › Computer use', () => {
expect(mod.requestPermissions).not.toHaveBeenCalled();
});

it('says whether Linux is Hyprland, and only there', async () => {
const { service } = setup({ enabled: true });
service.platform = 'linux';
service.env = { HYPRLAND_INSTANCE_SIGNATURE: 'abc' };
expect((await service.state()).hyprland).toBe(true);
service.env = {};
expect((await service.state()).hyprland).toBe(false);
service.platform = 'darwin';
service.env = { HYPRLAND_INSTANCE_SIGNATURE: 'abc' };
expect((await service.state()).hyprland).toBeUndefined();
});

it('refuses to turn on where Cua Driver does not run', async () => {
const { service, mod } = setup({ enabled: false });
service.platform = 'freebsd';
Expand Down
6 changes: 4 additions & 2 deletions apps/backend/src/computer-use/computer-use.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const run = promisify(execFile);
const SUPPORTED_PLATFORMS: readonly NodeJS.Platform[] = ['darwin', 'win32', 'linux'];

/**
* Settings › Computer use: the on/off switch (the same saved choice as `/computer-use on | off`), which app macOS gives the permissions to, and on macOS what it has
* granted, with the System Settings page of each. The driver's native library is loaded only while computer use is on, as in the extension. A change of the
* Settings › Computer use: the on/off switch (the same saved choice as `/computer-use on | off`), which app macOS gives the permissions to, on macOS what it has
* granted, with the System Settings page of each, and on Linux whether this is Hyprland, where computer use is read-only. The driver's native library is loaded only while computer use is on, as in the extension. A change of the
* switch reloads the idle sessions, so their computer tools appear or go.
*/
@Injectable()
Expand All @@ -23,6 +23,7 @@ export class ComputerUseService {
private adHoc: Promise<boolean | undefined> | undefined;
/** Overridable for the tests. */
platform: NodeJS.Platform = process.platform;
env: NodeJS.ProcessEnv = process.env;
openUrl = (url: string): Promise<unknown> => run('open', [url]);
codesignInfo = async (bundle: string): Promise<string> => (await run('codesign', ['-dv', bundle])).stderr;

Expand Down Expand Up @@ -55,6 +56,7 @@ export class ComputerUseService {
host: { name: host.name, kind: host.kind, ...(adHoc === undefined ? {} : { adHoc }) },
permissions,
needsRelaunch: this.platform === 'darwin' && enabled && this.screenChanged,
...(this.platform === 'linux' ? { hyprland: Boolean(this.env['HYPRLAND_INSTANCE_SIGNATURE']) } : {}),
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/score/score.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const scoreConsentSentence = (model: string): string =>
`This sends the thread’s transcript (prompts, tool calls, review comments, the final diff and the files you changed by hand, with secrets masked) to ${model} to attribute each change to what you did. The attendant is off, so nothing else is ever sent for this thread.`;

/**
* The Meat Proxy Score: the host side of the plugin events the package needs beyond `learn`'s (`baselines`, `outside-edits`, `annotate`, the outcome's stored
* The Meat Proxy Score: the host side of the plugin events the package needs beyond `learn`'s (`baselines`, `annotate`, the outcome's stored
* annotations, the attendant's `score` task), the attendant's fourth job (scoring a thread after its outcome lands, under its cap and per-project opt-out), and what the app asks
* for: the score of a thread, scoring it now, relabelling a hunk, and the per-project distribution. Core stores and draws; the labelling and the arithmetic are the package's
* (`lib/pipeline.mjs`), and nothing here is drawn while the package is not installed in pi.
Expand Down
2 changes: 2 additions & 0 deletions doc/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9852,6 +9852,8 @@ components:
- type: 'null'
needsRelaunch:
type: boolean
hyprland:
type: boolean
required:
- installed
- supported
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,11 @@ describe('Settings › Computer use', () => {
expect(q(linux.el, 'computer-use-permissions')).toBeNull();
expect(linux.el.textContent).toContain('Linux needs no permissions');
});

it('tells a Hyprland session that computer use is read-only', () => {
state.set({ ...base, platform: 'linux', enabled: true, hyprland: true });
expect(q(render().el, 'computer-use-hyprland')).not.toBeNull();
state.set({ ...base, platform: 'linux', enabled: true, hyprland: false });
expect(q(render().el, 'computer-use-hyprland')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const POLL_MS = 1500;
/**
* Settings › Computer use (`@pi-kit/computer-use`): the switch that `/computer-use on | off` also writes, and on macOS the two permissions with their state, a button that
* asks macOS and opens the right page of System Settings, which app macOS gives them to (pi-kit, Electron for a development build, or a terminal) and, once Screen
* Recording was asked for, Relaunch, because macOS applies it only to an app that starts again. The state follows System Settings by itself: the page reads it every
* 1.5 s while something is missing and when the window gets the focus back.
* Recording was asked for, Relaunch, because macOS applies it only to an app that starts again. On Hyprland, that computer use is read-only. The state follows
* System Settings by itself: the page reads it every 1.5 s while something is missing and when the window gets the focus back.
*/
@Component({
selector: 'pk-computer-use-section',
Expand Down Expand Up @@ -150,6 +150,11 @@ const POLL_MS = 1500;
} @else if (s.installed && s.platform === 'win32') {
<p class="muted t-sm">{{ 'settings.computerUseSection.windowsNoPermissions' | transloco }}</p>
} @else if (s.installed && s.platform === 'linux') {
@if (s.hyprland) {
<div role="status" class="alert alert-soft alert-warning text-sm" data-testid="computer-use-hyprland">
<span>{{ 'settings.computerUseSection.hyprlandReadOnly' | transloco }}</span>
</div>
}
<p class="muted t-sm">{{ 'settings.computerUseSection.linuxNoPermissions' | transloco }}</p>
}
} @else if (cu.error()) {
Expand Down
1 change: 1 addition & 0 deletions libs/shared/api/src/generated/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ export type ComputerUseState = {
screenRecording: boolean;
} | null;
needsRelaunch: boolean;
hyprland?: boolean;
};

export type Connector = {
Expand Down
3 changes: 2 additions & 1 deletion libs/shared/i18n/src/lib/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4122,7 +4122,8 @@
"adHocBuild": "Dieser Build ist ad-hoc signiert: macOS vergisst seine Berechtigungen bei jedem neuen Build. Ein Developer-ID-Build behält sie über Updates hinweg.",
"terminalHost": "Das Backend von pi-kit wurde aus {{value1}} gestartet, daher fragt macOS {{value1}}, nicht pi-kit.",
"windowsNoPermissions": "Windows braucht für die Computer-Steuerung keine Berechtigungen.",
"linuxNoPermissions": "Linux braucht für die Computer-Steuerung keine Berechtigungen. Es braucht eine Desktop-Sitzung (X11, oder Wayland mit XWayland oder den Wayland-Wegen des Treibers, die pi-kit einschaltet) und den AT-SPI-Bus für Barrierefreiheit (at-spi2-core). Unter Hyprland (Omarchy) braucht die Eingabe zusätzlich Cuas cua-hyprland-plugin, im Compositor geladen."
"linuxNoPermissions": "Linux braucht für die Computer-Steuerung keine Berechtigungen. Es braucht eine Desktop-Sitzung und den AT-SPI-Bus für Barrierefreiheit (at-spi2-core). Unter Hyprland ist die Computer-Steuerung read-only.",
"hyprlandReadOnly": "Unter Hyprland ist die Computer-Steuerung read-only: pi kann den Bildschirm sehen, aber nicht klicken oder tippen. Cua Driver unterstützt Eingaben unter Hyprland noch nicht."
}
},
"agents": {
Expand Down
3 changes: 2 additions & 1 deletion libs/shared/i18n/src/lib/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4122,7 +4122,8 @@
"adHocBuild": "This build is ad-hoc signed: macOS forgets its permissions whenever it is rebuilt. A Developer ID build keeps them across updates.",
"terminalHost": "pi-kit’s backend was started from {{value1}}, so macOS asks {{value1}}, not pi-kit.",
"windowsNoPermissions": "Windows needs no permissions for computer use.",
"linuxNoPermissions": "Linux needs no permissions for computer use. It needs a desktop session (X11, or Wayland with XWayland or the driver's Wayland routes, which pi-kit turns on) and the AT-SPI accessibility bus (at-spi2-core). On Hyprland (Omarchy), input also needs Cua's cua-hyprland-plugin loaded in the compositor."
"linuxNoPermissions": "Linux needs no permissions for computer use. It needs a desktop session and the AT-SPI accessibility bus (at-spi2-core). On Hyprland, computer use is read-only.",
"hyprlandReadOnly": "On Hyprland, computer use is read-only: pi can see your screen but cannot click or type. Cua Driver does not support input on Hyprland yet."
}
},
"agents": {
Expand Down
4 changes: 3 additions & 1 deletion libs/shared/protocol/src/computer-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { z } from 'zod';
export const ComputerUsePermissionSchema = z.enum(['accessibility', 'screenRecording']).meta({ id: 'ComputerUsePermission' });
export type ComputerUsePermission = z.infer<typeof ComputerUsePermissionSchema>;

/** Settings › Computer use: whether it is on, which app macOS asks, and (macOS, while it is on) what macOS has granted. */
/** Settings › Computer use: whether it is on, which app macOS asks, (macOS, while it is on) what macOS has granted, and (Linux) whether this is Hyprland. */
export const ComputerUseStateSchema = z
.object({
/** pi has the `@pi-kit/computer-use` package (pi-kit installs it once; `pi remove` takes it away). */
Expand All @@ -25,6 +25,8 @@ export const ComputerUseStateSchema = z
permissions: z.object({ accessibility: z.boolean(), screenRecording: z.boolean() }).nullable(),
/** Screen Recording was asked for since this app started: macOS applies it only after a relaunch. */
needsRelaunch: z.boolean(),
/** Linux: a Hyprland session, where computer use is read-only (Cua Driver cannot click or type there yet). */
hyprland: z.boolean().optional(),
})
.meta({ id: 'ComputerUseState' });
export type ComputerUseState = z.infer<typeof ComputerUseStateSchema>;
Expand Down
8 changes: 3 additions & 5 deletions libs/shared/protocol/src/score.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { AnnotateRequestSchema, AttendantRunRequestSchema, BaselineFileSchema, OutcomeRequestSchema, OutsideEditSchema, PLUGIN_CHANNELS, PLUGIN_CHANNEL_LIST, TranscriptTurnSchema } from './plugins';
import { AnnotateRequestSchema, AttendantRunRequestSchema, BaselineFileSchema, OutcomeRequestSchema, PLUGIN_CHANNELS, PLUGIN_CHANNEL_LIST, TranscriptTurnSchema } from './plugins';
import { type HunkLabelEntry, HunkLabelEntrySchema, HunkLabelsStoreSchema, OutcomeScoresSchema, ProjectScoresSchema, RelabelHunkSchema, ThreadScoreBadgeSchema, effectiveHunks, hunkId } from './score';

const entry = (id: string, label: HunkLabelEntry['label'], over: Partial<HunkLabelEntry> = {}): HunkLabelEntry => ({ id, file: id.split('@')[0], header: '@@ -1 +1 @@', label, input: label === 'autonomous' ? 'none' : 'first-prompt', trace: 'because', lines: 3, by: 'model', ...over });
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('the stored shapes', () => {

describe('the events 10e added to the plugin API', () => {
it('are on the list a session’s bus subscribes to', () => {
for (const channel of ['pi-kit:thread:baselines', 'pi-kit:thread:outside-edits', 'pi-kit:thread:annotate']) expect(PLUGIN_CHANNEL_LIST).toContain(channel);
for (const channel of ['pi-kit:thread:baselines', 'pi-kit:thread:annotate']) expect(PLUGIN_CHANNEL_LIST).toContain(channel);
expect(PLUGIN_CHANNELS.annotate).toBe('pi-kit:thread:annotate');
});

Expand All @@ -74,10 +74,8 @@ describe('the events 10e added to the plugin API', () => {
expect(AttendantRunRequestSchema.safeParse({ requestId: 'r', task: 'wander', system: 's', user: 'u' }).success).toBe(false);
});

it('baselines carry paths and times, never content; outside edits carry paths and counts', () => {
it('baselines carry paths and times, never content', () => {
expect(BaselineFileSchema.safeParse({ path: 'a.ts', existed: false, at: 1, turn: 1, edits: 2 }).success).toBe(true);
expect(OutsideEditSchema.safeParse({ path: 'a.ts', firstAt: 1, lastAt: 2, count: 2 }).success).toBe(true);
expect(OutsideEditSchema.safeParse({ path: 'a.ts', firstAt: 1, lastAt: 2, count: 0 }).success).toBe(false);
});

it('a turn is a message with its timing, tool calls, chips, review comments and answers', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/pi-computer-use/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Installing does nothing yet. Computer use is **off until you turn it on**:
|---|---|
| `/computer-use on` | asks once, then gives pi the tools |
| `/computer-use off` | takes them away |
| `/computer-use setup` | macOS: opens the permission pages one at a time (Windows and Linux need none) |
| `/computer-use status` | on or off, and whether permissions are ready |
| `/computer-use setup` | macOS: opens the permission pages one at a time (Windows and Linux need none; Hyprland is told it is read-only) |
| `/computer-use status` | on or off, whether permissions are ready, and on Linux the session type (Hyprland: read-only) and the driver's own check while it runs |

## Linux

Expand All @@ -26,7 +26,7 @@ Nothing to grant, but more to have in place than on the other platforms. The dri
- **X11** is the best supported: capture, input and the accessibility tree.
- **Wayland** is compositor by compositor. In a Wayland session this package turns on the driver's native Wayland routes (`CUA_DRIVER_RS_ENABLE_WAYLAND=1`, unless you set it yourself; `0` turns them off). Without them only apps running under XWayland are reached, and with neither XWayland nor the native routes there is nothing to drive.
- **The accessibility tree** (reading a window, acting on an element) needs the AT-SPI bus: install `at-spi2-core`. Desktops without a full GNOME or KDE session may not start it; Qt apps also need `QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1`.
- **Hyprland (Omarchy)** is experimental in Cua Driver. Window capture works through Hyprland's own protocol, but input needs Cua's compositor plugin, `cua-hyprland-plugin`, which is off by default and not part of the driver: install it (`pacman -S cua-hyprland-plugin`), load it into Hyprland (`hyprctl plugin load /usr/lib/cua/hyprland/cua-hyprland-plugin.so`, then enable `plugin.cua.enabled` in your Hyprland config) and check it with `hyprctl -j cua:status`. The plugin is built against one exact Hyprland version, so it may not install on Omarchy's stable channel. Even with it, Cua Driver covers background input for a short list of apps only (LibreOffice Calc and Inkscape at the time of writing); Chromium, Electron and XWayland apps, Unicode and input methods are not covered. See Cua's [platform support](https://cua.ai/docs/reference/cua-driver/platform-support).
- **Hyprland (Omarchy)** is read-only. Looking works: windows, screenshots and the accessibility tree come through Hyprland's own protocols, and an element the driver has read can be acted on through AT-SPI where the app exposes an action (an `element_token` from `computer_get_window_state`). Clicking at a point, pressing keys and bringing a window to the front do not: the driver answers `foreground_unavailable: production Hyprland input plugin is unavailable` or `Background delivery is not available`. This package says so (`/computer-use status`, `on` and `setup`, a refused input's error, and in pi-kit Settings › Computer use). *Experimental, untested:* that input goes through Cua's compositor plugin, `cua-hyprland-plugin`, which is not part of the driver. Omarchy's repository packages it against the exact Hyprland it ships (`sudo pacman -S cua-hyprland-plugin`, then `hyprctl plugin load /usr/lib/cua/hyprland/cua-hyprland-plugin.so`, a `plugin = …` line in `hyprland.conf` to load it at login, `hyprctl -j cua:status` to check), but the build it carries at the time of writing (0.33.2) reports itself `discovery_only` and speaks a newer socket protocol than this driver (0.36.0), so it is not expected to deliver input. Even where it does, Cua covers a short list of apps only (LibreOffice Calc and Inkscape at the time of writing); Chromium, Electron and XWayland apps are not covered. See Cua's [platform support](https://cua.ai/docs/reference/cua-driver/platform-support).

## macOS permissions

Expand Down
Loading
Loading