diff --git a/extensions/cate.aisession/src/_kit/cate-host.d.ts b/extensions/cate.aisession/src/_kit/cate-host.d.ts index 6bffc19..1cf2d47 100644 --- a/extensions/cate.aisession/src/_kit/cate-host.d.ts +++ b/extensions/cate.aisession/src/_kit/cate-host.d.ts @@ -42,6 +42,46 @@ export interface CateHostStorage { export interface CatePanel { readonly id: string setTitle(title: string): Promise + /** List panels across this workspace's windows (requires the `panel` scope). + * THE single enumeration surface: the + * focused entry answers "what is the user looking at", and browser panels + * carry their `url` (there is no separate browser list). */ + list(): Promise + /** Reveal/focus a panel by id (requires the `panel` scope). */ + focus(panelId: string): Promise + /** Close a panel through its normal dirty/running confirmation path. Does not + * reveal or focus the panel first (requires the `panel` scope). */ + close(panelId: string): Promise +} + +/** One open panel, as reported by `cate.panel.list()`. `filePath` is the bare + * runtime path (same form as `workspace.get().rootPath`), present for panels + * backed by a file (editors, documents). `url` is present for browser panels + * (empty while on the start page). */ +export interface CatePanelInfo { + panelId: string + type: string + title: string + focused: boolean + filePath?: string + url?: string +} + +/** One interactable element in an accessibility `snapshot()`. `ref` is an opaque + * handle to pass back to `click`/`type`/`press`; it is only valid for the + * snapshot it came from (re-snapshot after a navigation or mutation). */ +export interface CateBrowserRef { + ref: string + role: string + name: string + value?: string +} + +/** Accessibility snapshot of a browser panel, from `cate.browser.snapshot()`. */ +export interface CateBrowserSnapshot { + url: string + title: string + refs: CateBrowserRef[] } /** A file the user dragged onto this panel, delivered to `cate.files.onDrop`. */ @@ -86,8 +126,29 @@ export interface CateHost { open(opts?: { resume?: string }): Promise<{ sessionId: string } | { error: string }> send(sessionId: string, prompt: string): Promise dispose(sessionId: string): Promise - run(prompt: string): Promise cancel(): Promise } + /** Drive Cate's browser panels (requires the `browser` scope + first-use user + * consent). These panels hold the user's real, logged-in session — treat it + * accordingly. `panelId` picks a target; omitted, the focused (or only) + * browser panel is used. */ + browser: { + /** To enumerate open browser panels, use `cate.panel.list()`. */ + open(opts: { url: string; panelId?: string }): Promise<{ panelId: string; url: string }> + reload(opts?: { panelId?: string }): Promise<{ ok: true }> + /** Capture a screenshot; returns a host filesystem path in the OS temp dir + * (a webview guest can't read it directly; a server-backed extension can). */ + screenshot(opts?: { panelId?: string }): Promise<{ path: string }> + snapshot(opts?: { panelId?: string }): Promise + click(opts: { ref: string; panelId?: string }): Promise<{ ok: true }> + type(opts: { ref: string; text: string; panelId?: string }): Promise<{ ok: true }> + /** Resolve once the panel stops loading (`timeoutMs` defaults to 5000, + * capped at 8000). Rejects in-band with `still-loading`. */ + wait(opts?: { panelId?: string; timeoutMs?: number }): Promise<{ url: string; title: string; loading: false }> + /** Press a named key (Enter, Tab, Escape, Backspace, Delete, Space, arrows, + * PageUp/PageDown, Home, End) as TRUSTED input, so Enter submits forms. + * With `ref` the element is focused first. */ + press(opts: { key: string; ref?: string; panelId?: string }): Promise<{ ok: true }> + } storage: CateHostStorage } diff --git a/extensions/cate.kitchensink/src/public/cate-host.d.ts b/extensions/cate.kitchensink/src/public/cate-host.d.ts index f371b44..f5b4693 100644 --- a/extensions/cate.kitchensink/src/public/cate-host.d.ts +++ b/extensions/cate.kitchensink/src/public/cate-host.d.ts @@ -15,7 +15,7 @@ interface CateHostTheme { terminal: Record } -/** Result of one agent turn (`cate.agent.send` / `cate.agent.run`): the flattened +/** Result of one agent turn (`cate.agent.send`): the flattened * `text` for convenience plus the raw final assistant `message` from pi (its role * and content blocks — text, tool calls, etc.), or null if the turn produced none. */ interface AgentTurnResult { @@ -92,7 +92,6 @@ interface CateHost { /** Tear down the live session (pi's jsonl stays; reopen via `resume`). */ dispose(sessionId: string): Promise /** One-shot sugar over open -> send -> dispose. */ - run(prompt: string): Promise /** Abort the in-flight turn of this extension's session. */ cancel(): Promise } diff --git a/extensions/cate.kitchensink/src/server.ts b/extensions/cate.kitchensink/src/server.ts index b5e0d62..da67c39 100644 --- a/extensions/cate.kitchensink/src/server.ts +++ b/extensions/cate.kitchensink/src/server.ts @@ -13,7 +13,7 @@ // POST /api/echo echoes the JSON body // GET /ws WebSocket echo // POST /api/cate-roundtrip server calls back into Cate over CATE_API -// POST /api/agent-run server runs one agent turn via cate.agent.run +// POST /api/agent-run server runs one agent turn (agent.open -> send -> dispose) import http from 'http' import crypto from 'crypto' @@ -259,8 +259,9 @@ const server = http.createServer(async (req, res) => { return } - // The server runs one agent turn via cate.agent.run (needs the `agent` scope - // and first-use consent). It resolves only when the agent finishes. + // The server runs one agent turn by composing open -> send -> dispose (needs + // the `agent` scope and first-use consent; there is no one-shot host method). + // It resolves only when the agent finishes. if (pathname === '/api/agent-run' && req.method === 'POST') { const raw = await readBody(req) let prompt = '' @@ -274,14 +275,30 @@ const server = http.createServer(async (req, res) => { return } try { - const result = unwrap(await callCateApi('cate.agent.run', { prompt })) as - | { text?: string; error?: string } + const opened = unwrap(await callCateApi('cate.agent.open', {})) as + | { sessionId?: string; error?: string } | undefined - if (result && typeof result.error === 'string') { - sendJson(res, 200, { ok: false, error: result.error }) + if (!opened || typeof opened.sessionId !== 'string') { + sendJson(res, 200, { ok: false, error: opened?.error ?? 'open-failed' }) return } - sendJson(res, 200, { ok: true, text: result?.text ?? '' }) + try { + const result = unwrap( + await callCateApi('cate.agent.send', { sessionId: opened.sessionId, prompt }), + ) as { text?: string; error?: string } | undefined + if (result && typeof result.error === 'string') { + sendJson(res, 200, { ok: false, error: result.error }) + return + } + sendJson(res, 200, { ok: true, text: result?.text ?? '' }) + } finally { + // Best effort: the session must not outlive the one-shot turn. + try { + await callCateApi('cate.agent.dispose', { sessionId: opened.sessionId }) + } catch { + /* noop */ + } + } } catch (err) { const message = err instanceof Error ? err.message : String(err) sendJson(res, 500, { ok: false, error: message }) diff --git a/extensions/cate.mcp/src/_kit/cate-host.d.ts b/extensions/cate.mcp/src/_kit/cate-host.d.ts index 6bffc19..1cf2d47 100644 --- a/extensions/cate.mcp/src/_kit/cate-host.d.ts +++ b/extensions/cate.mcp/src/_kit/cate-host.d.ts @@ -42,6 +42,46 @@ export interface CateHostStorage { export interface CatePanel { readonly id: string setTitle(title: string): Promise + /** List panels across this workspace's windows (requires the `panel` scope). + * THE single enumeration surface: the + * focused entry answers "what is the user looking at", and browser panels + * carry their `url` (there is no separate browser list). */ + list(): Promise + /** Reveal/focus a panel by id (requires the `panel` scope). */ + focus(panelId: string): Promise + /** Close a panel through its normal dirty/running confirmation path. Does not + * reveal or focus the panel first (requires the `panel` scope). */ + close(panelId: string): Promise +} + +/** One open panel, as reported by `cate.panel.list()`. `filePath` is the bare + * runtime path (same form as `workspace.get().rootPath`), present for panels + * backed by a file (editors, documents). `url` is present for browser panels + * (empty while on the start page). */ +export interface CatePanelInfo { + panelId: string + type: string + title: string + focused: boolean + filePath?: string + url?: string +} + +/** One interactable element in an accessibility `snapshot()`. `ref` is an opaque + * handle to pass back to `click`/`type`/`press`; it is only valid for the + * snapshot it came from (re-snapshot after a navigation or mutation). */ +export interface CateBrowserRef { + ref: string + role: string + name: string + value?: string +} + +/** Accessibility snapshot of a browser panel, from `cate.browser.snapshot()`. */ +export interface CateBrowserSnapshot { + url: string + title: string + refs: CateBrowserRef[] } /** A file the user dragged onto this panel, delivered to `cate.files.onDrop`. */ @@ -86,8 +126,29 @@ export interface CateHost { open(opts?: { resume?: string }): Promise<{ sessionId: string } | { error: string }> send(sessionId: string, prompt: string): Promise dispose(sessionId: string): Promise - run(prompt: string): Promise cancel(): Promise } + /** Drive Cate's browser panels (requires the `browser` scope + first-use user + * consent). These panels hold the user's real, logged-in session — treat it + * accordingly. `panelId` picks a target; omitted, the focused (or only) + * browser panel is used. */ + browser: { + /** To enumerate open browser panels, use `cate.panel.list()`. */ + open(opts: { url: string; panelId?: string }): Promise<{ panelId: string; url: string }> + reload(opts?: { panelId?: string }): Promise<{ ok: true }> + /** Capture a screenshot; returns a host filesystem path in the OS temp dir + * (a webview guest can't read it directly; a server-backed extension can). */ + screenshot(opts?: { panelId?: string }): Promise<{ path: string }> + snapshot(opts?: { panelId?: string }): Promise + click(opts: { ref: string; panelId?: string }): Promise<{ ok: true }> + type(opts: { ref: string; text: string; panelId?: string }): Promise<{ ok: true }> + /** Resolve once the panel stops loading (`timeoutMs` defaults to 5000, + * capped at 8000). Rejects in-band with `still-loading`. */ + wait(opts?: { panelId?: string; timeoutMs?: number }): Promise<{ url: string; title: string; loading: false }> + /** Press a named key (Enter, Tab, Escape, Backspace, Delete, Space, arrows, + * PageUp/PageDown, Home, End) as TRUSTED input, so Enter submits forms. + * With `ref` the element is focused first. */ + press(opts: { key: string; ref?: string; panelId?: string }): Promise<{ ok: true }> + } storage: CateHostStorage } diff --git a/extensions/cate.mermaid/src/_kit/cate-host.d.ts b/extensions/cate.mermaid/src/_kit/cate-host.d.ts index 6bffc19..1cf2d47 100644 --- a/extensions/cate.mermaid/src/_kit/cate-host.d.ts +++ b/extensions/cate.mermaid/src/_kit/cate-host.d.ts @@ -42,6 +42,46 @@ export interface CateHostStorage { export interface CatePanel { readonly id: string setTitle(title: string): Promise + /** List panels across this workspace's windows (requires the `panel` scope). + * THE single enumeration surface: the + * focused entry answers "what is the user looking at", and browser panels + * carry their `url` (there is no separate browser list). */ + list(): Promise + /** Reveal/focus a panel by id (requires the `panel` scope). */ + focus(panelId: string): Promise + /** Close a panel through its normal dirty/running confirmation path. Does not + * reveal or focus the panel first (requires the `panel` scope). */ + close(panelId: string): Promise +} + +/** One open panel, as reported by `cate.panel.list()`. `filePath` is the bare + * runtime path (same form as `workspace.get().rootPath`), present for panels + * backed by a file (editors, documents). `url` is present for browser panels + * (empty while on the start page). */ +export interface CatePanelInfo { + panelId: string + type: string + title: string + focused: boolean + filePath?: string + url?: string +} + +/** One interactable element in an accessibility `snapshot()`. `ref` is an opaque + * handle to pass back to `click`/`type`/`press`; it is only valid for the + * snapshot it came from (re-snapshot after a navigation or mutation). */ +export interface CateBrowserRef { + ref: string + role: string + name: string + value?: string +} + +/** Accessibility snapshot of a browser panel, from `cate.browser.snapshot()`. */ +export interface CateBrowserSnapshot { + url: string + title: string + refs: CateBrowserRef[] } /** A file the user dragged onto this panel, delivered to `cate.files.onDrop`. */ @@ -86,8 +126,29 @@ export interface CateHost { open(opts?: { resume?: string }): Promise<{ sessionId: string } | { error: string }> send(sessionId: string, prompt: string): Promise dispose(sessionId: string): Promise - run(prompt: string): Promise cancel(): Promise } + /** Drive Cate's browser panels (requires the `browser` scope + first-use user + * consent). These panels hold the user's real, logged-in session — treat it + * accordingly. `panelId` picks a target; omitted, the focused (or only) + * browser panel is used. */ + browser: { + /** To enumerate open browser panels, use `cate.panel.list()`. */ + open(opts: { url: string; panelId?: string }): Promise<{ panelId: string; url: string }> + reload(opts?: { panelId?: string }): Promise<{ ok: true }> + /** Capture a screenshot; returns a host filesystem path in the OS temp dir + * (a webview guest can't read it directly; a server-backed extension can). */ + screenshot(opts?: { panelId?: string }): Promise<{ path: string }> + snapshot(opts?: { panelId?: string }): Promise + click(opts: { ref: string; panelId?: string }): Promise<{ ok: true }> + type(opts: { ref: string; text: string; panelId?: string }): Promise<{ ok: true }> + /** Resolve once the panel stops loading (`timeoutMs` defaults to 5000, + * capped at 8000). Rejects in-band with `still-loading`. */ + wait(opts?: { panelId?: string; timeoutMs?: number }): Promise<{ url: string; title: string; loading: false }> + /** Press a named key (Enter, Tab, Escape, Backspace, Delete, Space, arrows, + * PageUp/PageDown, Home, End) as TRUSTED input, so Enter submits forms. + * With `ref` the element is focused first. */ + press(opts: { key: string; ref?: string; panelId?: string }): Promise<{ ok: true }> + } storage: CateHostStorage } diff --git a/extensions/cate.sqlite/src/_kit/cate-host.d.ts b/extensions/cate.sqlite/src/_kit/cate-host.d.ts index 6bffc19..1cf2d47 100644 --- a/extensions/cate.sqlite/src/_kit/cate-host.d.ts +++ b/extensions/cate.sqlite/src/_kit/cate-host.d.ts @@ -42,6 +42,46 @@ export interface CateHostStorage { export interface CatePanel { readonly id: string setTitle(title: string): Promise + /** List panels across this workspace's windows (requires the `panel` scope). + * THE single enumeration surface: the + * focused entry answers "what is the user looking at", and browser panels + * carry their `url` (there is no separate browser list). */ + list(): Promise + /** Reveal/focus a panel by id (requires the `panel` scope). */ + focus(panelId: string): Promise + /** Close a panel through its normal dirty/running confirmation path. Does not + * reveal or focus the panel first (requires the `panel` scope). */ + close(panelId: string): Promise +} + +/** One open panel, as reported by `cate.panel.list()`. `filePath` is the bare + * runtime path (same form as `workspace.get().rootPath`), present for panels + * backed by a file (editors, documents). `url` is present for browser panels + * (empty while on the start page). */ +export interface CatePanelInfo { + panelId: string + type: string + title: string + focused: boolean + filePath?: string + url?: string +} + +/** One interactable element in an accessibility `snapshot()`. `ref` is an opaque + * handle to pass back to `click`/`type`/`press`; it is only valid for the + * snapshot it came from (re-snapshot after a navigation or mutation). */ +export interface CateBrowserRef { + ref: string + role: string + name: string + value?: string +} + +/** Accessibility snapshot of a browser panel, from `cate.browser.snapshot()`. */ +export interface CateBrowserSnapshot { + url: string + title: string + refs: CateBrowserRef[] } /** A file the user dragged onto this panel, delivered to `cate.files.onDrop`. */ @@ -86,8 +126,29 @@ export interface CateHost { open(opts?: { resume?: string }): Promise<{ sessionId: string } | { error: string }> send(sessionId: string, prompt: string): Promise dispose(sessionId: string): Promise - run(prompt: string): Promise cancel(): Promise } + /** Drive Cate's browser panels (requires the `browser` scope + first-use user + * consent). These panels hold the user's real, logged-in session — treat it + * accordingly. `panelId` picks a target; omitted, the focused (or only) + * browser panel is used. */ + browser: { + /** To enumerate open browser panels, use `cate.panel.list()`. */ + open(opts: { url: string; panelId?: string }): Promise<{ panelId: string; url: string }> + reload(opts?: { panelId?: string }): Promise<{ ok: true }> + /** Capture a screenshot; returns a host filesystem path in the OS temp dir + * (a webview guest can't read it directly; a server-backed extension can). */ + screenshot(opts?: { panelId?: string }): Promise<{ path: string }> + snapshot(opts?: { panelId?: string }): Promise + click(opts: { ref: string; panelId?: string }): Promise<{ ok: true }> + type(opts: { ref: string; text: string; panelId?: string }): Promise<{ ok: true }> + /** Resolve once the panel stops loading (`timeoutMs` defaults to 5000, + * capped at 8000). Rejects in-band with `still-loading`. */ + wait(opts?: { panelId?: string; timeoutMs?: number }): Promise<{ url: string; title: string; loading: false }> + /** Press a named key (Enter, Tab, Escape, Backspace, Delete, Space, arrows, + * PageUp/PageDown, Home, End) as TRUSTED input, so Enter submits forms. + * With `ref` the element is focused first. */ + press(opts: { key: string; ref?: string; panelId?: string }): Promise<{ ok: true }> + } storage: CateHostStorage } diff --git a/extensions/cate.usage/src/_kit/cate-host.d.ts b/extensions/cate.usage/src/_kit/cate-host.d.ts index 6bffc19..1cf2d47 100644 --- a/extensions/cate.usage/src/_kit/cate-host.d.ts +++ b/extensions/cate.usage/src/_kit/cate-host.d.ts @@ -42,6 +42,46 @@ export interface CateHostStorage { export interface CatePanel { readonly id: string setTitle(title: string): Promise + /** List panels across this workspace's windows (requires the `panel` scope). + * THE single enumeration surface: the + * focused entry answers "what is the user looking at", and browser panels + * carry their `url` (there is no separate browser list). */ + list(): Promise + /** Reveal/focus a panel by id (requires the `panel` scope). */ + focus(panelId: string): Promise + /** Close a panel through its normal dirty/running confirmation path. Does not + * reveal or focus the panel first (requires the `panel` scope). */ + close(panelId: string): Promise +} + +/** One open panel, as reported by `cate.panel.list()`. `filePath` is the bare + * runtime path (same form as `workspace.get().rootPath`), present for panels + * backed by a file (editors, documents). `url` is present for browser panels + * (empty while on the start page). */ +export interface CatePanelInfo { + panelId: string + type: string + title: string + focused: boolean + filePath?: string + url?: string +} + +/** One interactable element in an accessibility `snapshot()`. `ref` is an opaque + * handle to pass back to `click`/`type`/`press`; it is only valid for the + * snapshot it came from (re-snapshot after a navigation or mutation). */ +export interface CateBrowserRef { + ref: string + role: string + name: string + value?: string +} + +/** Accessibility snapshot of a browser panel, from `cate.browser.snapshot()`. */ +export interface CateBrowserSnapshot { + url: string + title: string + refs: CateBrowserRef[] } /** A file the user dragged onto this panel, delivered to `cate.files.onDrop`. */ @@ -86,8 +126,29 @@ export interface CateHost { open(opts?: { resume?: string }): Promise<{ sessionId: string } | { error: string }> send(sessionId: string, prompt: string): Promise dispose(sessionId: string): Promise - run(prompt: string): Promise cancel(): Promise } + /** Drive Cate's browser panels (requires the `browser` scope + first-use user + * consent). These panels hold the user's real, logged-in session — treat it + * accordingly. `panelId` picks a target; omitted, the focused (or only) + * browser panel is used. */ + browser: { + /** To enumerate open browser panels, use `cate.panel.list()`. */ + open(opts: { url: string; panelId?: string }): Promise<{ panelId: string; url: string }> + reload(opts?: { panelId?: string }): Promise<{ ok: true }> + /** Capture a screenshot; returns a host filesystem path in the OS temp dir + * (a webview guest can't read it directly; a server-backed extension can). */ + screenshot(opts?: { panelId?: string }): Promise<{ path: string }> + snapshot(opts?: { panelId?: string }): Promise + click(opts: { ref: string; panelId?: string }): Promise<{ ok: true }> + type(opts: { ref: string; text: string; panelId?: string }): Promise<{ ok: true }> + /** Resolve once the panel stops loading (`timeoutMs` defaults to 5000, + * capped at 8000). Rejects in-band with `still-loading`. */ + wait(opts?: { panelId?: string; timeoutMs?: number }): Promise<{ url: string; title: string; loading: false }> + /** Press a named key (Enter, Tab, Escape, Backspace, Delete, Space, arrows, + * PageUp/PageDown, Home, End) as TRUSTED input, so Enter submits forms. + * With `ref` the element is focused first. */ + press(opts: { key: string; ref?: string; panelId?: string }): Promise<{ ok: true }> + } storage: CateHostStorage } diff --git a/kit/cate-host.d.ts b/kit/cate-host.d.ts index e2c231c..36f7039 100644 --- a/kit/cate-host.d.ts +++ b/kit/cate-host.d.ts @@ -41,6 +41,46 @@ export interface CateHostStorage { export interface CatePanel { readonly id: string setTitle(title: string): Promise + /** List panels across this workspace's windows (requires the `panel` scope). + * THE single enumeration surface: the + * focused entry answers "what is the user looking at", and browser panels + * carry their `url` (there is no separate browser list). */ + list(): Promise + /** Reveal/focus a panel by id (requires the `panel` scope). */ + focus(panelId: string): Promise + /** Close a panel through its normal dirty/running confirmation path. Does not + * reveal or focus the panel first (requires the `panel` scope). */ + close(panelId: string): Promise +} + +/** One open panel, as reported by `cate.panel.list()`. `filePath` is the bare + * runtime path (same form as `workspace.get().rootPath`), present for panels + * backed by a file (editors, documents). `url` is present for browser panels + * (empty while on the start page). */ +export interface CatePanelInfo { + panelId: string + type: string + title: string + focused: boolean + filePath?: string + url?: string +} + +/** One interactable element in an accessibility `snapshot()`. `ref` is an opaque + * handle to pass back to `click`/`type`/`press`; it is only valid for the + * snapshot it came from (re-snapshot after a navigation or mutation). */ +export interface CateBrowserRef { + ref: string + role: string + name: string + value?: string +} + +/** Accessibility snapshot of a browser panel, from `cate.browser.snapshot()`. */ +export interface CateBrowserSnapshot { + url: string + title: string + refs: CateBrowserRef[] } /** A file the user dragged onto this panel, delivered to `cate.files.onDrop`. */ @@ -85,8 +125,29 @@ export interface CateHost { open(opts?: { resume?: string }): Promise<{ sessionId: string } | { error: string }> send(sessionId: string, prompt: string): Promise dispose(sessionId: string): Promise - run(prompt: string): Promise cancel(): Promise } + /** Drive Cate's browser panels (requires the `browser` scope + first-use user + * consent). These panels hold the user's real, logged-in session — treat it + * accordingly. `panelId` picks a target; omitted, the focused (or only) + * browser panel is used. */ + browser: { + /** To enumerate open browser panels, use `cate.panel.list()`. */ + open(opts: { url: string; panelId?: string }): Promise<{ panelId: string; url: string }> + reload(opts?: { panelId?: string }): Promise<{ ok: true }> + /** Capture a screenshot; returns a host filesystem path in the OS temp dir + * (a webview guest can't read it directly; a server-backed extension can). */ + screenshot(opts?: { panelId?: string }): Promise<{ path: string }> + snapshot(opts?: { panelId?: string }): Promise + click(opts: { ref: string; panelId?: string }): Promise<{ ok: true }> + type(opts: { ref: string; text: string; panelId?: string }): Promise<{ ok: true }> + /** Resolve once the panel stops loading (`timeoutMs` defaults to 5000, + * capped at 8000). Rejects in-band with `still-loading`. */ + wait(opts?: { panelId?: string; timeoutMs?: number }): Promise<{ url: string; title: string; loading: false }> + /** Press a named key (Enter, Tab, Escape, Backspace, Delete, Space, arrows, + * PageUp/PageDown, Home, End) as TRUSTED input, so Enter submits forms. + * With `ref` the element is focused first. */ + press(opts: { key: string; ref?: string; panelId?: string }): Promise<{ ok: true }> + } storage: CateHostStorage }