mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-15 05:02:08 +00:00
runtime contract enforcement (#6)
This commit is contained in:
parent
61f4738311
commit
543d41c3a2
7 changed files with 958 additions and 157 deletions
|
|
@ -21,6 +21,7 @@ pnpm --dir site install
|
|||
- `src/` holds production code and unit tests
|
||||
- `tests/` holds integration tests
|
||||
- `tests/support/` holds shared X11 and daemon helpers for integration coverage
|
||||
- `docs/runtime-output.md` is the stable-vs-best-effort runtime output contract for agent-facing CLI work
|
||||
|
||||
Keep integration-only helpers out of `src/`.
|
||||
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -100,6 +100,7 @@ deskctl doctor
|
|||
- `@wN` refs are short-lived handles assigned by `snapshot` and `list-windows`
|
||||
- `--json` output includes a stable `window_id` for programmatic targeting within the current daemon session
|
||||
- `list-windows` is a cheap read-only operation and does not capture or write a screenshot
|
||||
- the stable runtime JSON/error contract is documented in [docs/runtime-output.md](docs/runtime-output.md)
|
||||
|
||||
## Read and Wait Surface
|
||||
|
||||
|
|
@ -147,6 +148,16 @@ Successful `get active-window`, `wait window`, and `wait focus` responses return
|
|||
|
||||
Wait timeout and selector failures are structured in `--json` mode so agents can recover without string parsing.
|
||||
|
||||
## Output Policy
|
||||
|
||||
Text mode is compact and follow-up-oriented, but JSON is the parsing contract.
|
||||
|
||||
- use `--json` when an agent needs strict parsing
|
||||
- rely on `window_id`, selector-related fields, grouped read payloads, and structured error `kind` values for stable automation
|
||||
- treat monitor naming, incidental whitespace, and default screenshot file names as best-effort
|
||||
|
||||
See [docs/runtime-output.md](docs/runtime-output.md) for the exact stable-vs-best-effort breakdown.
|
||||
|
||||
## Selector Contract
|
||||
|
||||
Explicit selector modes:
|
||||
|
|
|
|||
178
docs/runtime-output.md
Normal file
178
docs/runtime-output.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# Runtime Output Contract
|
||||
|
||||
This document defines the current output contract for `deskctl`.
|
||||
|
||||
It is intentionally scoped to the current Linux X11 runtime surface.
|
||||
It does not promise stability for future Wayland or window-manager-specific features.
|
||||
|
||||
## Goals
|
||||
|
||||
- Keep `deskctl` fully non-interactive
|
||||
- Make text output actionable for quick terminal and agent loops
|
||||
- Make `--json` safe for agent consumption without depending on incidental formatting
|
||||
|
||||
## JSON Envelope
|
||||
|
||||
Every runtime command uses the same top-level JSON envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
Stable top-level fields:
|
||||
|
||||
- `success`
|
||||
- `data`
|
||||
- `error`
|
||||
|
||||
`success` is always the authoritative success/failure bit.
|
||||
When `success` is `false`, the CLI exits non-zero in both text mode and `--json` mode.
|
||||
|
||||
## Stable Fields
|
||||
|
||||
These fields are stable for agent consumption in the current Phase 1 runtime contract.
|
||||
|
||||
### Window Identity
|
||||
|
||||
Whenever a runtime response includes a window payload, these fields are stable:
|
||||
|
||||
- `ref_id`
|
||||
- `window_id`
|
||||
- `title`
|
||||
- `app_name`
|
||||
- `x`
|
||||
- `y`
|
||||
- `width`
|
||||
- `height`
|
||||
- `focused`
|
||||
- `minimized`
|
||||
|
||||
`window_id` is the stable public identifier for a live daemon session.
|
||||
`ref_id` is a short-lived convenience handle for the current window snapshot/ref map.
|
||||
|
||||
### Grouped Reads
|
||||
|
||||
`deskctl get active-window`
|
||||
|
||||
- stable: `data.window`
|
||||
|
||||
`deskctl get monitors`
|
||||
|
||||
- stable: `data.count`
|
||||
- stable: `data.monitors`
|
||||
- stable per monitor:
|
||||
- `name`
|
||||
- `x`
|
||||
- `y`
|
||||
- `width`
|
||||
- `height`
|
||||
- `width_mm`
|
||||
- `height_mm`
|
||||
- `primary`
|
||||
- `automatic`
|
||||
|
||||
`deskctl get version`
|
||||
|
||||
- stable: `data.version`
|
||||
- stable: `data.backend`
|
||||
|
||||
`deskctl get systeminfo`
|
||||
|
||||
- stable: `data.backend`
|
||||
- stable: `data.display`
|
||||
- stable: `data.session_type`
|
||||
- stable: `data.session`
|
||||
- stable: `data.socket_path`
|
||||
- stable: `data.screen`
|
||||
- stable: `data.monitor_count`
|
||||
- stable: `data.monitors`
|
||||
|
||||
### Waits
|
||||
|
||||
`deskctl wait window`
|
||||
`deskctl wait focus`
|
||||
|
||||
- stable: `data.wait`
|
||||
- stable: `data.selector`
|
||||
- stable: `data.elapsed_ms`
|
||||
- stable: `data.window`
|
||||
|
||||
### Selector-Driven Action Success
|
||||
|
||||
For selector-driven action commands that resolve a window target, these identifiers are stable when present:
|
||||
|
||||
- `data.ref_id`
|
||||
- `data.window_id`
|
||||
- `data.title`
|
||||
- `data.selector`
|
||||
|
||||
This applies to:
|
||||
|
||||
- `click`
|
||||
- `dblclick`
|
||||
- `focus`
|
||||
- `close`
|
||||
- `move-window`
|
||||
- `resize-window`
|
||||
|
||||
The exact human-readable text rendering of those commands is not part of the JSON contract.
|
||||
|
||||
### Artifact-Producing Commands
|
||||
|
||||
`snapshot`
|
||||
`screenshot`
|
||||
|
||||
- stable: `data.screenshot`
|
||||
|
||||
When the command also returns windows, `data.windows` uses the stable window payload documented above.
|
||||
|
||||
## Stable Structured Error Kinds
|
||||
|
||||
When a runtime command returns structured JSON failure data, these error kinds are stable:
|
||||
|
||||
- `selector_not_found`
|
||||
- `selector_ambiguous`
|
||||
- `selector_invalid`
|
||||
- `timeout`
|
||||
- `not_found`
|
||||
- `window_not_focused` as `data.last_observation.kind` or equivalent observation payload
|
||||
|
||||
Stable structured failure fields include:
|
||||
|
||||
- `data.kind`
|
||||
- `data.selector` when selector-related
|
||||
- `data.mode` when selector-related
|
||||
- `data.candidates` for ambiguous selector failures
|
||||
- `data.message` for invalid selector failures
|
||||
- `data.wait`
|
||||
- `data.timeout_ms`
|
||||
- `data.poll_ms`
|
||||
- `data.last_observation`
|
||||
|
||||
## Best-Effort Fields
|
||||
|
||||
These values are useful but environment-dependent and should be treated as best-effort:
|
||||
|
||||
- exact monitor naming conventions
|
||||
- EWMH/window-manager-dependent window ordering details
|
||||
- cosmetic text formatting in non-JSON mode
|
||||
- screenshot file names when the caller did not provide an explicit path
|
||||
- command stderr wording outside the structured `kind` classifications above
|
||||
|
||||
## Text Mode Expectations
|
||||
|
||||
Text mode is intended to stay compact and follow-up-useful.
|
||||
|
||||
The exact whitespace/alignment of text output is not stable.
|
||||
The following expectations are stable at the behavioral level:
|
||||
|
||||
- important runtime reads print actionable identifiers or geometry
|
||||
- selector failures print enough detail to recover without `--json`
|
||||
- artifact-producing commands print the artifact path
|
||||
- window listings print both `@wN` refs and `window_id` values
|
||||
|
||||
If an agent needs strict parsing, it should use `--json`.
|
||||
|
|
@ -90,6 +90,14 @@ deskctl daemon status # Check daemon status
|
|||
- `--session NAME` : Session name for multiple daemon instances (default: "default")
|
||||
- `--socket PATH` : Custom Unix socket path
|
||||
|
||||
## Output Contract
|
||||
|
||||
- Prefer `--json` when an agent needs strict parsing.
|
||||
- Use `window_id` for stable targeting inside a live daemon session.
|
||||
- Use `ref_id` / `@wN` for quick short-lived follow-up actions after `snapshot` or `list-windows`.
|
||||
- Structured JSON failures expose machine-usable `kind` values for selector and wait failures.
|
||||
- The exact text formatting is intentionally compact but not the parsing contract. See `docs/runtime-output.md` for the stable field policy.
|
||||
|
||||
## Window Refs
|
||||
|
||||
After `snapshot` or `list-windows`, windows are assigned short refs:
|
||||
|
|
|
|||
872
src/cli/mod.rs
872
src/cli/mod.rs
File diff suppressed because it is too large
Load diff
|
|
@ -88,9 +88,21 @@ impl std::fmt::Display for WindowInfo {
|
|||
|
||||
#[allow(dead_code)]
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
if s.len() <= max {
|
||||
if s.chars().count() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}...", &s[..max - 3])
|
||||
let truncated: String = s.chars().take(max.saturating_sub(3)).collect();
|
||||
format!("{truncated}...")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::truncate;
|
||||
|
||||
#[test]
|
||||
fn truncate_is_char_safe() {
|
||||
let input = format!("fire{}fox", '\u{00E9}');
|
||||
assert_eq!(truncate(&input, 7), "fire...");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,9 +81,13 @@ async fn handle_click(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Res
|
|||
ResolveResult::Match(entry) => {
|
||||
let (x, y) = entry.center();
|
||||
match state.backend.click(x, y) {
|
||||
Ok(()) => Response::ok(
|
||||
serde_json::json!({"clicked": {"x": x, "y": y, "selector": selector, "window_id": entry.window_id}}),
|
||||
),
|
||||
Ok(()) => Response::ok(serde_json::json!({
|
||||
"clicked": {"x": x, "y": y},
|
||||
"selector": selector,
|
||||
"ref_id": entry.ref_id,
|
||||
"window_id": entry.window_id,
|
||||
"title": entry.title,
|
||||
})),
|
||||
Err(error) => Response::err(format!("Click failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -117,9 +121,13 @@ async fn handle_dblclick(request: &Request, state: &Arc<Mutex<DaemonState>>) ->
|
|||
ResolveResult::Match(entry) => {
|
||||
let (x, y) = entry.center();
|
||||
match state.backend.dblclick(x, y) {
|
||||
Ok(()) => Response::ok(
|
||||
serde_json::json!({"double_clicked": {"x": x, "y": y, "selector": selector, "window_id": entry.window_id}}),
|
||||
),
|
||||
Ok(()) => Response::ok(serde_json::json!({
|
||||
"double_clicked": {"x": x, "y": y},
|
||||
"selector": selector,
|
||||
"ref_id": entry.ref_id,
|
||||
"window_id": entry.window_id,
|
||||
"title": entry.title,
|
||||
})),
|
||||
Err(error) => Response::err(format!("Double-click failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +275,10 @@ async fn handle_window_action(
|
|||
Ok(()) => Response::ok(serde_json::json!({
|
||||
"action": action,
|
||||
"window": entry.title,
|
||||
"title": entry.title,
|
||||
"ref_id": entry.ref_id,
|
||||
"window_id": entry.window_id,
|
||||
"selector": selector,
|
||||
})),
|
||||
Err(error) => Response::err(format!("{action} failed: {error}")),
|
||||
}
|
||||
|
|
@ -296,7 +307,10 @@ async fn handle_move_window(request: &Request, state: &Arc<Mutex<DaemonState>>)
|
|||
match state.backend.move_window(entry.backend_window_id, x, y) {
|
||||
Ok(()) => Response::ok(serde_json::json!({
|
||||
"moved": entry.title,
|
||||
"title": entry.title,
|
||||
"ref_id": entry.ref_id,
|
||||
"window_id": entry.window_id,
|
||||
"selector": selector,
|
||||
"x": x,
|
||||
"y": y,
|
||||
})),
|
||||
|
|
@ -338,7 +352,10 @@ async fn handle_resize_window(request: &Request, state: &Arc<Mutex<DaemonState>>
|
|||
{
|
||||
Ok(()) => Response::ok(serde_json::json!({
|
||||
"resized": entry.title,
|
||||
"title": entry.title,
|
||||
"ref_id": entry.ref_id,
|
||||
"window_id": entry.window_id,
|
||||
"selector": selector,
|
||||
"width": width,
|
||||
"height": height,
|
||||
})),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue