diff --git a/Cargo.lock b/Cargo.lock index a096db6b..ade573b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2242,7 +2242,7 @@ dependencies = [ [[package]] name = "harness-gateway-client" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "async-trait", "bytes", @@ -2258,7 +2258,7 @@ dependencies = [ [[package]] name = "harness-plugin-loader" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "libloading 0.8.9", "runvil-plugin", @@ -2269,7 +2269,7 @@ dependencies = [ [[package]] name = "harness-runner" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "futures-util", "rand 0.9.5", @@ -4064,7 +4064,7 @@ dependencies = [ [[package]] name = "plugin-mcp" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "reqwest", "rmcp", @@ -4088,7 +4088,7 @@ dependencies = [ [[package]] name = "plugin-web" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "async-trait", "encoding_rs", @@ -4734,7 +4734,7 @@ dependencies = [ [[package]] name = "runvil" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "harness-plugin-loader", "harness-runner", @@ -4745,7 +4745,7 @@ dependencies = [ [[package]] name = "runvil-engine" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "runvil-lua", "runvil-model-client", @@ -4758,7 +4758,7 @@ dependencies = [ [[package]] name = "runvil-lua" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "mlua", "runvil-markdown", @@ -4772,7 +4772,7 @@ dependencies = [ [[package]] name = "runvil-markdown" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "pulldown-cmark", "serde_json", @@ -4782,7 +4782,7 @@ dependencies = [ [[package]] name = "runvil-model-client" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "runvil-types", "serde", @@ -4792,7 +4792,7 @@ dependencies = [ [[package]] name = "runvil-plugin" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "runvil-types", "runvil-vfs", @@ -4815,19 +4815,20 @@ source = "git+https://github.com/cppalliance/runvil-plugin-api?rev=03a9fa37d2fe6 [[package]] name = "runvil-prompt" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "pulldown-cmark", "runvil-lua", "runvil-types", "serde", + "serde_json", "serde_yaml_ng", ] [[package]] name = "runvil-runtime" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "mlua", "regex", @@ -4845,7 +4846,7 @@ dependencies = [ [[package]] name = "runvil-types" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" dependencies = [ "serde", "serde_json", @@ -4855,7 +4856,7 @@ dependencies = [ [[package]] name = "runvil-vfs" version = "0.3.0" -source = "git+https://github.com/cppalliance/runvil-harness?rev=7147e709f603b81786fb2d702bf50c4d9a88d62b#7147e709f603b81786fb2d702bf50c4d9a88d62b" +source = "git+https://github.com/cppalliance/runvil-harness?rev=0d938885abc7fb293ae449a787c50e7c7eb60e92#0d938885abc7fb293ae449a787c50e7c7eb60e92" [[package]] name = "rust-embed" diff --git a/Cargo.toml b/Cargo.toml index e4cfdb31..8a4144c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,10 @@ repository = "https://github.com/cppalliance/runvil-workshop" # in. All four name one commit, because two commits would be two copies # of every type the crates share. `version` beside `git` keeps cargo-deny's # wildcard check quiet. -runvil = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "7147e709f603b81786fb2d702bf50c4d9a88d62b" } -harness-gateway-client = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "7147e709f603b81786fb2d702bf50c4d9a88d62b" } -plugin-web = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "7147e709f603b81786fb2d702bf50c4d9a88d62b" } -plugin-mcp = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "7147e709f603b81786fb2d702bf50c4d9a88d62b" } +runvil = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "0d938885abc7fb293ae449a787c50e7c7eb60e92" } +harness-gateway-client = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "0d938885abc7fb293ae449a787c50e7c7eb60e92" } +plugin-web = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "0d938885abc7fb293ae449a787c50e7c7eb60e92" } +plugin-mcp = { version = "0.3.0", git = "https://github.com/cppalliance/runvil-harness", rev = "0d938885abc7fb293ae449a787c50e7c7eb60e92" } # The safe layer over the C Plugin contract, version 0.4, from its own # repository, pinned by commit. Only the library Plugin crates under # `crates/workshop/` name it, for `export_plugin!`. The commit is the one diff --git a/crates/workshop/server/src/routes/prompts-tests.rs b/crates/workshop/server/src/routes/prompts-tests.rs index cdd70a0c..482a7c65 100644 --- a/crates/workshop/server/src/routes/prompts-tests.rs +++ b/crates/workshop/server/src/routes/prompts-tests.rs @@ -39,9 +39,6 @@ name: full description: does everything runvil: 0 max_tool_iterations: 5 -input: - path: paper.md - description: the paper output: path: out.md description: the result @@ -50,13 +47,16 @@ uses: - fs - acme/poop args: - topic: - type: string - description: the topic - count: - type: integer - optional: true - default: 3 + type: object + properties: + topic: + type: string + description: the topic + count: + type: integer + default: 3 + required: + - topic models: strong: min_context: 200000 @@ -78,10 +78,6 @@ async fn a_full_frontmatter_prompt_answers_every_contract_section() { assert_eq!(json["description"], "does everything"); assert_eq!(json["runvil"], 0); assert_eq!(json["max_tool_iterations"], 5); - assert_eq!( - json["input"], - serde_json::json!({ "path": "paper.md", "description": "the paper" }) - ); assert_eq!( json["output"], serde_json::json!({ "path": "out.md", "description": "the result" }) @@ -96,14 +92,19 @@ async fn a_full_frontmatter_prompt_answers_every_contract_section() { { "id": "poop" }, ]) ); + // The declaration is sent as JSON Schema: every object node writes + // `additionalProperties`, and `required` lists the fields a caller + // must send. assert_eq!( json["args"], serde_json::json!({ - "implicit": false, - "fields": [ - { "name": "count", "type": "integer", "optional": true, "default": 3, "description": null }, - { "name": "topic", "type": "string", "optional": false, "default": null, "description": "the topic" }, - ], + "type": "object", + "properties": { + "count": { "type": "integer", "default": 3 }, + "topic": { "type": "string", "description": "the topic" }, + }, + "required": ["topic"], + "additionalProperties": false, }) ); assert_eq!( @@ -147,7 +148,7 @@ fn an_internal_parse_failure_names_no_prompt_line() { } #[tokio::test] -async fn a_prompt_without_args_yields_the_implicit_prose_field() { +async fn a_prompt_without_args_yields_the_default_declaration() { let text = "---\nname: plain\ndescription: no args\n---\n\n## S\n\np\n"; let body = serde_json::json!({ "name": "plain", "text": text }); let (status, json) = post_contract(body).await; @@ -155,10 +156,11 @@ async fn a_prompt_without_args_yields_the_implicit_prose_field() { assert_eq!( json["args"], serde_json::json!({ - "implicit": true, - "fields": [ - { "name": "prose", "type": "string", "optional": true, "default": null, "description": "Freeform input for this prompt" }, - ], + "type": "object", + "properties": { + "text": { "type": "string", "description": "Freeform text for this prompt" }, + }, + "additionalProperties": false, }) ); assert_eq!( @@ -195,7 +197,6 @@ async fn the_dto_serializes_absent_declarations_as_nulls() { let (status, json) = post_contract(body).await; assert_eq!(status, StatusCode::OK); // Absent declarations serialize as explicit nulls, not missing keys. - assert!(json["input"].is_null()); assert!(json["output"].is_null()); assert!(json["max_tool_iterations"].is_null()); assert!(json["runvil"].is_null()); diff --git a/crates/workshop/server/src/routes/prompts.rs b/crates/workshop/server/src/routes/prompts.rs index 65028f9d..6ca8f818 100644 --- a/crates/workshop/server/src/routes/prompts.rs +++ b/crates/workshop/server/src/routes/prompts.rs @@ -5,7 +5,9 @@ //! posted markdown into the contract the Run window renders, or a `422` //! envelope naming the parse failure kind and line. The parser's own //! types are `Deserialize`-only, so the wire DTO is hand-written -//! `Serialize` structs built from [`Frontmatter`] accessors. +//! `Serialize` structs built from [`Frontmatter`] accessors. Its `args` +//! field is the JSON Schema that +//! [`ArgsSchema::as_json`](runvil::prompt::ArgsSchema::as_json) gives. use axum::Json; use axum::http::StatusCode; @@ -16,7 +18,7 @@ use serde::{Deserialize, Serialize}; use runvil::Prompt; use runvil::model::ModelRole; use runvil::packs::PackRef; -use runvil::prompt::{ArgDecl, ArgsDecl, FileDecl, Frontmatter, RoleRequirements}; +use runvil::prompt::{FileDecl, Frontmatter, RoleRequirements}; use crate::error::AppError; @@ -48,19 +50,18 @@ struct ContractResponse { runvil: Option, /// The declared tool-loop cap; `null` for the runtime default. max_tool_iterations: Option, - /// The declared input file; `null` when absent. - input: Option, /// The declared output file; `null` when absent. output: Option, /// The declared Packs, in declaration order. uses: Vec, - /// The typed args declaration. - args: ArgsDto, + /// The `args` declaration as JSON Schema. The root is an object node, + /// and its `properties` are the prompt's arguments by name. + args: serde_json::Value, /// The declared model roles, sorted by label. models: Vec, } -/// A declared input or output file. +/// The declared output file. #[derive(Debug, Serialize)] struct FileDto { /// The store-internal path. @@ -93,60 +94,6 @@ impl From<&PackRef> for PackDto { } } -/// The typed args declaration. -#[derive(Debug, Serialize)] -struct ArgsDto { - /// True when the declaration is the implicit default (no `args:` key). - implicit: bool, - /// The declared fields, sorted by name. - fields: Vec, -} - -impl From<&ArgsDecl> for ArgsDto { - fn from(decl: &ArgsDecl) -> Self { - Self { - implicit: decl.is_default(), - fields: decl - .iter() - .map(|(name, arg)| ArgDto::new(name, arg)) - .collect(), - } - } -} - -/// One declared arg. -#[derive(Debug, Serialize)] -struct ArgDto { - /// The arg name. - name: String, - /// The declared type (`string`, `boolean`, `integer`, `number`). - #[serde(rename = "type")] - kind: String, - /// Whether a call may omit the field. - optional: bool, - /// The declared default; `null` when absent. - default: Option, - /// The human-readable description; `null` when absent. - description: Option, -} - -impl ArgDto { - /// Builds the DTO for the arg declared under `name`. - fn new(name: &str, decl: &ArgDecl) -> Self { - Self { - name: name.to_owned(), - kind: decl.kind().to_string(), - optional: decl.is_optional(), - // A declared default is validated against the declared type - // at parse, so it is always a JSON-representable scalar. - default: decl - .default() - .and_then(|value| serde_json::to_value(value).ok()), - description: decl.description().map(str::to_owned), - } - } -} - /// The requirements a prompt declares for one model role. #[derive(Debug, Serialize)] struct ModelDto { @@ -178,10 +125,9 @@ impl From<&Frontmatter> for ContractResponse { max_tool_iterations: frontmatter .max_tool_iterations() .map(std::num::NonZeroU32::get), - input: frontmatter.input().map(FileDto::from), output: frontmatter.output().map(FileDto::from), uses: frontmatter.uses().iter().map(PackDto::from).collect(), - args: ArgsDto::from(frontmatter.args()), + args: frontmatter.args().as_json().clone(), models: ModelRole::ALL .into_iter() .filter_map(|role| { diff --git a/crates/workshop/ui/src/parts/run/run-panel.ts b/crates/workshop/ui/src/parts/run/run-panel.ts index 387b88f0..97d668ab 100644 --- a/crates/workshop/ui/src/parts/run/run-panel.ts +++ b/crates/workshop/ui/src/parts/run/run-panel.ts @@ -11,8 +11,7 @@ // "Prompt" label is for screen readers only, the empty and error states // are text lines with Browse... and Choose Prompt as inline links, a // droppable drag marks the panel with a drag-over overlay class, and the -// typed-path dialogs (Choose Prompt, Choose Input) are Cursor's form -// modal, Cancel first. +// typed-path dialog (Choose Prompt) is Cursor's form modal, Cancel first. // // The panel never reads disk: prompt text arrives through the confined // GET /workspace/file and is posted to POST /prompts/contract. Prompts @@ -83,7 +82,7 @@ export class RunPanel extends WorkshopPart { private contract: RunContract | null = null; private failure: string | null = null; private promptPath: string | null = null; - // The open Choose Prompt / input Browse dialog, dismissed with the panel. + // The open Choose Prompt dialog, dismissed with the panel. private dialog: { dispose(): void } | null = null; // Holds the toolbar, body, and footer. It is the width container the rows @@ -328,39 +327,6 @@ export class RunPanel extends WorkshopPart { await this.load(path); } - /** The input row's Browse: fills the field, reading nothing. */ - private browseInput(field: HTMLInputElement): void { - if (window.__TAURI_INTERNALS__ !== undefined) { - void open({ directory: false, title: "Choose Input" }).then((picked) => { - if (typeof picked === "string") { - field.value = picked; - } - }); - return; - } - this.dialog?.dispose(); - this.dialog = showPanelDialog({ - container: this.element, - classPrefix: "ws-run-input", - titleId: "run-input-title", - title: "Choose Input", - message: "Enter the full path of the input file.", - skin: "form", - field: { id: "run-input-path", label: "Input path" }, - buttons: [ - { label: "Cancel", run: () => undefined }, - { - label: "Choose", - primary: true, - requiresValue: true, - run: (value) => { - field.value = value; - }, - }, - ], - }); - } - /** Paints the body's state-dependent content and the Run button. */ private render(): void { this.promptField.value = this.promptPath ?? ""; @@ -398,9 +364,7 @@ export class RunPanel extends WorkshopPart { return; } if (this.contract !== null) { - renderContractRows(this.body, this.contract, { - browseInput: (field) => this.browseInput(field), - }); + renderContractRows(this.body, this.contract); } } } diff --git a/crates/workshop/ui/src/parts/run/run-rows.ts b/crates/workshop/ui/src/parts/run/run-rows.ts index 0c4c05f3..4afbd049 100644 --- a/crates/workshop/ui/src/parts/run/run-rows.ts +++ b/crates/workshop/ui/src/parts/run/run-rows.ts @@ -2,9 +2,9 @@ // pure DOM building over the narrowed RunContract. Every frontmatter // key present in the prompt except `uses`, which has nothing to // bind, appears as exactly one row: name, description, and runvil -// read-only; input a text field plus Browse; output a text field; one -// control per arg by type with required markers and defaults (the -// implicit declaration is the single prose box); model roles +// read-only; output a text field; one row for each top-level property +// of the `args` schema, its control chosen by the property's type, with +// the required marker and the default prefilled; model roles // read-only; max_tool_iterations numeric with the runtime-default // placeholder. No bind-time validation runs here - validation is the // parser's alone. @@ -15,18 +15,15 @@ // side. A boolean arg is a `.switch` button, as Cursor's settings toggles // are. -import type { RunContract, RunContractArg } from "../../services/run-api"; +import type { RunContract, RunContractSchema } from "../../services/run-api"; -/** The panel's hooks the rows need. */ -export interface RunRowsOptions { - /** Fills the input row's field through the panel's file-pick flow. */ - readonly browseInput: (field: HTMLInputElement) => void; -} +/** The Engine's name for an argument that holds free text. */ +const TEXT_ARG = "text"; /** What a row shows beside its label. */ interface RowDetails { /** The second line under the label; blank or absent draws none. */ - readonly description?: string | null; + readonly description?: string; /** Draws the required marker beside the label. */ readonly required?: boolean; } @@ -51,7 +48,7 @@ function row(label: string, controls: readonly Node[], details: RowDetails = {}) title.appendChild(requiredMarker()); } text.appendChild(title); - if (details.description !== undefined && details.description !== null && details.description.length > 0) { + if (details.description !== undefined && details.description.length > 0) { const description = document.createElement("div"); description.className = "ws-run-panel__row-description"; description.textContent = details.description; @@ -83,7 +80,16 @@ function textField(value: string, placeholder = ""): HTMLInputElement { return field; } -/** The required marker on a non-optional arg. */ +/** A multiline box with the shared control skin. */ +function multilineBox(value: string, placeholder = ""): HTMLTextAreaElement { + const box = document.createElement("textarea"); + box.className = "input ws-run-panel__prose"; + box.value = value; + box.placeholder = placeholder; + return box; +} + +/** The required marker on an arg the schema's `required` list names. */ function requiredMarker(): HTMLElement { const marker = document.createElement("span"); marker.className = "ws-run-panel__required"; @@ -96,12 +102,12 @@ function requiredMarker(): HTMLElement { * A boolean arg's control: Cursor's switch, a `