Files
IdeA/frontend/src/ports/index.ts
Blomios 9d6d0fbdf1 feat(tickets): affiche le créateur d'un ticket et filtre la liste par créateur
createdBy était déjà présent dans le frontmatter des tickets mais ni exposé
ni exploitable côté UI. Ajout du DTO (domain/ports/mock), affichage dans
TicketDetail, colonne/filtre créateur dans TicketsPanel, et persistance du
filtre. ticketActor.ts introduit pour porter la logique de filtrage côté
frontend, sans changement backend nécessaire (lot frontend pur).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 10:45:58 +02:00

1362 lines
53 KiB
TypeScript

/**
* UI ports (gateways) — interfaces describing *what the UI needs*, independent
* of transport (ARCHITECTURE §1.3). React components depend on these, never on
* `@tauri-apps/api` directly. Implemented by the Tauri adapters and by mocks.
*
* Signatures are intentionally minimal/skeletal for L1; later lots flesh out
* each gateway as their use cases land. They are aligned with the planned use
* cases (ARCHITECTURE §6) so the shape is stable.
*/
import type {
Agent,
AgentDrift,
AgentProfile,
AppExitWorkGuardState,
CustomProviderConfig,
DomainEvent,
EmbedderEngines,
EmbedderProfile,
EmbeddedServerStatus,
FirstRunState,
GitBranches,
GitCommit,
GitFileStatus,
GraphCommit,
HealthReport,
LayoutKind,
LayoutList,
LayoutOperation,
LayoutTree,
LocalModelServerConfig,
ModelServerCommandPreview,
Memory,
MemoryIndexEntry,
MemoryLink,
MemoryType,
McpToolPolicy,
OpenCodeConfig,
OpenCodeProviderCatalogEntry,
ProfileModelCatalog,
EffectivePermissions,
PairedDevice,
PairingCode,
PermissionSet,
PluginAdmin,
PluginInstallResult,
PluginReview,
PluginRuntimeContributionCatalog,
PluginSourceKind,
PluginUninstallResult,
ProjectMcpToolPermissions,
PageDirection,
Project,
ProjectPermissions,
ProjectWorkState,
ProjectSystemPermissions,
ProfileAvailability,
ResumableAgent,
ResolvedAgentSystemPermissions,
ReplyChunk,
ServerExposurePreview,
ServerExposureSettings,
Skill,
SkillScope,
SystemPermissionSet,
Sprint,
Template,
TerminalSession,
Ticket,
TicketBulkResult,
TicketChat,
TicketCarnet,
TicketLinkKind,
TicketList,
TicketPriority,
TicketStatus,
TurnPage,
Unsubscribe,
} from "@/domain";
/** System-level gateway: health/ping + global domain-event subscription. */
export interface SystemGateway {
/** Calls the backend `health` command (smoke test of the whole pipeline). */
health(note?: string): Promise<HealthReport>;
/** Subscribes to relayed domain events. */
onDomainEvent(handler: (event: DomainEvent) => void): Promise<Unsubscribe>;
/**
* Opens a native folder picker and returns the chosen path, or `null` if the
* user cancelled. This is the only sanctioned way to pick a folder — all call
* sites go through this port; the Tauri plugin is only imported in the adapter.
*/
pickFolder(): Promise<string | null>;
/**
* Opens a native file picker for a single local archive (plugin install from
* archive, carnet §1.4/§9) and returns the chosen path, or `null` if the user
* cancelled. Same sanctioned-picker rule as {@link pickFolder}.
*/
pickArchiveFile(): Promise<string | null>;
/**
* Subscribes to the app-exit work-in-progress guard (ticket #83): fired when
* closing the main window is intercepted because it would interrupt active
* agents/background tasks. Desktop-only — the web transport returns an inert
* unsubscribe (never fires; there is no interceptable window to guard).
*/
onAppExitWorkGuard(
handler: (state: AppExitWorkGuardState) => void,
): Promise<Unsubscribe>;
/**
* Bypasses the guard once and requests the main window to close for real
* (ticket #83) — the user chose "Quitter quand même".
*/
confirmAppExit(): Promise<void>;
}
/** Input for {@link AgentGateway.createAgent}. */
export interface CreateAgentInput {
name: string;
profileId: string;
initialContent?: string;
}
/**
* Best-effort enriched details about a CLI conversation (T7), used to enrich the
* resume popup. Both fields are optional: a missing inspector or a missing
* transcript yields an empty object — the popup degrades to the status alone.
*/
export interface ConversationDetails {
/** A short, best-effort label for the conversation (last topic). */
lastTopic?: string;
/** A best-effort cumulative token count. */
tokenCount?: number;
}
/** Agents: create, list, read/update context, delete, launch (L6). */
export interface AgentGateway {
/** Lists all agents belonging to the given project. */
listAgents(projectId: string): Promise<Agent[]>;
/**
* Lists the agents that can be **resumed** when the project is (re)opened
* (ARCHITECTURE §15.2): a read-only inventory of agent cells that were running
* and/or carry a persisted conversation id at close time. Drives the
* `ResumeProjectPanel` shown once on open; an empty list ⇒ no panel. Pure
* inventory: no PTY is spawned by this call.
*/
listResumableAgents(projectId: string): Promise<ResumableAgent[]>;
/**
* Lists the agents that currently own a live session and the cell hosting
* each. Used to disable an agent already running in another cell (it cannot be
* launched a second time — one live session per agent).
*/
listLiveAgents(projectId: string): Promise<LiveAgent[]>;
/**
* Rebinds an already-live agent session to another visible layout cell without
* respawning the CLI process. Used when a background agent is opened in a cell,
* or when a live session is moved from a now-detached/closed cell.
*
* Backends that do not yet support agent-session rebinding may omit this
* method; the UI will show the session as running elsewhere but cannot attach
* it from a different cell.
*/
attachLiveAgent?(
projectId: string,
agentId: string,
nodeId: string,
): Promise<LiveAgent>;
/**
* Stops an already-live agent session without going through terminal teardown
* commands from the Work panel. The backend owns the live-agent lifecycle.
*/
stopLiveAgent?(projectId: string, agentId: string): Promise<StoppedLiveAgent>;
/** Creates a new agent from scratch; returns the created agent. */
createAgent(projectId: string, input: CreateAgentInput): Promise<Agent>;
/**
* Hot-swaps an agent's runtime AI profile (Chantier A). The conversation
* history is abandoned (the product decision is "start fresh"); the agent's
* context `.md` and project memory are preserved. When the agent has a live
* session it is relaunched on the new profile and returned as
* {@link TerminalSession} so the hosting cell can rebind; otherwise
* `relaunchedSession` is absent.
*/
changeAgentProfile(
projectId: string,
agentId: string,
profileId: string,
rows: number,
cols: number,
): Promise<{ agent: Agent; relaunchedSession?: TerminalSession }>;
/** Reads an agent's `.md` context by agent id. */
readContext(projectId: string, agentId: string): Promise<string>;
/** Overwrites an agent's `.md` context. */
updateContext(projectId: string, agentId: string, content: string): Promise<void>;
/** Removes an agent from the project. */
deleteAgent(projectId: string, agentId: string): Promise<void>;
/**
* Launches the agent: opens a PTY, spawns the CLI, and wires the output stream.
* Mirrors {@link TerminalGateway.openTerminal} but invokes `launch_agent`.
*
* The returned handle carries an optional {@link TerminalHandle.assignedConversationId}:
* when the agent's profile assigns a fresh CLI conversation id on this first
* launch (the cell had none yet), it is surfaced here so the caller can persist
* it on the hosting leaf (`setCellConversation`) and resume on the next open.
* Pass the leaf's current `conversationId` via {@link OpenTerminalOptions} to
* resume an existing conversation instead.
*/
launchAgent(
projectId: string,
agentId: string,
options: OpenTerminalOptions,
onData: (bytes: Uint8Array) => void,
): Promise<TerminalHandle>;
/**
* Re-attaches to an agent's already-running PTY (same backend mechanism as
* {@link TerminalGateway.reattach}; agent sessions share the session-based
* terminal commands). Used when an agent cell's view re-mounts after a
* navigation/layout change, so the agent is never killed.
*/
reattach(
sessionId: string,
onData: (bytes: Uint8Array) => void,
): Promise<ReattachResult>;
/**
* Reads best-effort {@link ConversationDetails} for an agent's conversation
* (T7), to enrich the resume popup with the last topic + a token indicator.
* Best-effort by contract: a missing/unsupported inspector or a missing
* transcript resolves to an empty object (never rejects for "no details").
*/
inspectConversation(
projectId: string,
agentId: string,
conversationId: string,
): Promise<ConversationDetails>;
}
/** Options for opening a terminal. */
export interface OpenTerminalOptions {
/** Working directory (typically the project root). */
cwd: string;
/** Initial terminal height in rows. */
rows: number;
/** Initial terminal width in columns. */
cols: number;
/**
* Persistent CLI conversation id recorded on the hosting cell, if any. When
* present, an agent launch **resumes** that conversation; when absent the
* launch may *assign* a fresh id (surfaced on the returned handle). Only
* meaningful for {@link AgentGateway.launchAgent}; ignored for plain terminals.
*/
conversationId?: string;
/**
* The layout leaf (node) hosting this launch. Drives the "one live session per
* agent" invariant backend-side: launching an agent already running in a
* *different* node is refused (`AGENT_ALREADY_RUNNING`); the same node is
* idempotent. Only meaningful for {@link AgentGateway.launchAgent}.
*/
nodeId?: string;
}
/** One currently-live agent and the cell hosting it (see {@link AgentGateway.listLiveAgents}). */
export interface LiveAgent {
/** The live agent's id. */
agentId: string;
/** The node (layout leaf) hosting the agent's live session. */
nodeId: string;
/**
* The live PTY session id, when the backend exposes it. Required for opening
* a background/running agent in a new visible cell without respawning it.
*/
sessionId: string;
/** Runtime kind backing the live agent. */
kind: "pty" | "structured";
}
/** Result returned when the backend stops a live agent. */
export interface StoppedLiveAgent {
agentId: string;
sessionId: string;
kind: "pty" | "structured";
}
/**
* A live terminal handle returned by {@link TerminalGateway.openTerminal}.
*
* The output stream is delivered to the `onData` callback passed at open time
* (over a Tauri Channel in the real adapter); the handle exposes the input/
* control operations and a `close` that also tears the stream down.
*/
export interface TerminalHandle {
/** Stable session id (UUID) used by the backend for this PTY. */
readonly sessionId: string;
/**
* Conversation id **assigned** by an agent launch when the profile minted a
* fresh one (the cell had none yet). Present only on handles returned by
* {@link AgentGateway.launchAgent}; `undefined` for plain terminals, resumes,
* or profiles without a session block. The caller persists it on the hosting
* leaf so the next open resumes instead of re-assigning.
*/
readonly assignedConversationId?: string;
/** Sends bytes (xterm keystrokes) to the PTY. */
write(data: Uint8Array): Promise<void>;
/** Resizes the PTY. */
resize(rows: number, cols: number): Promise<void>;
/**
* Detaches the **view** from the PTY without killing it: stops the local
* output subscription so a torn-down view (navigation / layout change) stops
* receiving bytes, while the backend PTY keeps running. The session can later
* be re-attached via {@link TerminalGateway.reattach}.
*
* This is the lifecycle the view's cleanup must use — never {@link close}.
*/
detach(): void;
/**
* Kills the PTY and stops the output stream. Reserved for an **explicit** user
* action (closing the terminal); navigation must never call this.
*/
close(): Promise<void>;
}
/**
* The write-portal contract an agent-cell terminal talks to (ARCHITECTURE §20).
* The portal owns the *human line* counter, the suspension flag (raised while it
* injects a delegation) and — once the view has a live PTY — the handle it
* writes through. The terminal view is the only effective PTY writer
* (single-writer invariant): it relays human keystrokes and the portal injects
* via the same handle, never a second physical writer.
*/
export interface WritePortal {
/**
* Reports a raw human keystroke chunk (`term.onData`) so the portal can keep
* its line counter. Called for **every** keystroke in agent mode, including
* while suspended (the portal decides what to count).
*/
onHumanData(data: string): void;
/**
* Whether the keystroke relay to the PTY is currently suspended (the portal
* is injecting a delegation). When `true`, the view drops keystrokes so they
* do not race the injected text.
*/
isSuspended(): boolean;
/** Binds the live PTY handle so the portal can inject through it. */
bindHandle(handle: TerminalHandle): void;
/** Drops the handle reference (view torn down). */
unbindHandle(): void;
}
/**
* Terminals (L3): open a PTY with a per-session output stream, then write/
* resize/close it through the returned {@link TerminalHandle}.
*/
export interface TerminalGateway {
/**
* Opens a terminal. `onData` receives every chunk of PTY output (bytes) as it
* arrives. Resolves once the PTY is spawned and the stream is wired.
*/
openTerminal(
options: OpenTerminalOptions,
onData: (bytes: Uint8Array) => void,
): Promise<TerminalHandle>;
/**
* Re-attaches to an **already-running** PTY identified by `sessionId` (after a
* view was torn down by navigation/layout change). Returns the live handle and
* the retained scrollback, which the caller repaints into xterm before the new
* output stream (`onData`) starts delivering subsequent bytes. Does NOT
* re-spawn the process.
*
* Rejects if the session is no longer alive (the caller then opens fresh).
*/
reattach(
sessionId: string,
onData: (bytes: Uint8Array) => void,
): Promise<ReattachResult>;
/**
* Kills a live PTY by its session id, independently of any view-held handle.
* Used when a cell's agent changes: the old PTY must be torn down even though
* the owning {@link TerminalHandle} is private to its (unmounting) view, whose
* cleanup only ever {@link TerminalHandle.detach}es. Best-effort: resolves even
* if the session is already gone.
*
* This is the only sanctioned way to kill a PTY outside
* {@link TerminalHandle.close}.
*/
closeTerminal(sessionId: string): Promise<void>;
}
/** The outcome of {@link TerminalGateway.reattach}. */
export interface ReattachResult {
/** The live terminal handle for the re-attached session. */
handle: TerminalHandle;
/** The retained scrollback bytes to repaint before the live stream resumes. */
scrollback: Uint8Array;
}
/** A UI subscription attached to one background task output stream. */
export interface BackgroundTaskAttachment {
/** Attached task id, echoed by the backend. */
taskId: string;
/** Retained bytes to repaint immediately before live chunks arrive. */
scrollback: Uint8Array;
/** Whether subsequent live bytes are expected on the supplied callback. */
live: boolean;
/** Detaches the local UI subscriber without cancelling the task. */
detach(): void;
}
/** Projects: create/open/close/list (L2). */
export interface ProjectGateway {
/** Lists the projects known to the registry. */
listProjects(): Promise<Project[]>;
/** Creates a project from a root; returns the created project. */
createProject(name: string, root: string): Promise<Project>;
/** Opens a project by id; returns the opened project. */
openProject(projectId: string): Promise<Project>;
/** Closes a project by id. */
closeProject(projectId: string): Promise<void>;
/** Reads the shared project context stored at `.ideai/CONTEXT.md`. */
readProjectContext(projectId: string): Promise<string>;
/** Overwrites the shared project context stored at `.ideai/CONTEXT.md`. */
updateProjectContext(projectId: string, content: string): Promise<void>;
}
/** Layout: load the terminal grid tree and apply mutating operations (L4). */
export interface LayoutGateway {
/** Loads a project's layout tree (defaults to a single cell if none persisted).
* When `layoutId` is omitted, the active layout for the project is used. */
loadLayout(projectId: string, layoutId?: string): Promise<LayoutTree>;
/**
* Applies a split/merge/resize/move/setSession/setCellAgent operation; the backend persists
* `.ideai/layout.json` and returns the resulting tree.
* When `layoutId` is omitted, the active layout for the project is used.
*/
mutateLayout(
projectId: string,
operation: LayoutOperation,
layoutId?: string,
): Promise<LayoutTree>;
/** Lists all named layouts for a project, with the current active id. */
listLayouts(projectId: string): Promise<LayoutList>;
/** Creates a new named layout for a project; returns the new layout id. */
createLayout(projectId: string, name: string, kind?: LayoutKind): Promise<{ layoutId: string }>;
/** Renames a layout. */
renameLayout(projectId: string, layoutId: string, name: string): Promise<void>;
/** Deletes a layout; returns the new active layout id. */
deleteLayout(projectId: string, layoutId: string): Promise<{ activeId: string }>;
/**
* Sets the active layout for a project. Returns the layout id the backend
* *actually* activated — authoritative (invariant I4): it equals the requested
* id when valid, else the unchanged current active id (self-healing fallback
* when the requested id was stale, e.g. after an external overwrite of
* `layouts.json`). Callers must adopt this id rather than the one they asked for.
*/
setActiveLayout(projectId: string, layoutId: string): Promise<{ activeId: string }>;
}
/** Git: status/commit/checkout/… (L8). */
export interface GitGateway {
status(projectId: string): Promise<GitFileStatus[]>;
stage(projectId: string, path: string): Promise<void>;
unstage(projectId: string, path: string): Promise<void>;
commit(projectId: string, message: string): Promise<GitCommit>;
branches(projectId: string): Promise<GitBranches>;
checkout(projectId: string, branch: string): Promise<void>;
log(projectId: string, limit: number): Promise<GitCommit[]>;
init(projectId: string): Promise<void>;
/** Returns the full commit DAG for the git-graph layout. */
graph(projectId: string, limit: number): Promise<GraphCommit[]>;
}
/** Remote (SSH/WSL) connection management (L9). */
export interface RemoteGateway {
connect(projectId: string): Promise<void>;
}
/** Input for {@link TemplateGateway.createTemplate}. */
export interface CreateTemplateInput {
name: string;
content: string;
defaultProfileId: string;
}
/**
* Templates (L7): CRUD for agent templates, creation of agents from templates,
* drift detection and synchronisation.
*/
export interface TemplateGateway {
/** Lists all templates. */
listTemplates(): Promise<Template[]>;
/** Creates a new template; returns the created template. */
createTemplate(input: CreateTemplateInput): Promise<Template>;
/** Updates a template's content; increments its version; returns the updated template. */
updateTemplate(templateId: string, content: string): Promise<Template>;
/** Deletes a template by id. */
deleteTemplate(templateId: string): Promise<void>;
/**
* Creates an agent in `projectId` based on the given template.
* The agent's origin will be `fromTemplate`; its context will be the template's `contentMd`.
*/
createAgentFromTemplate(
projectId: string,
templateId: string,
opts?: { name?: string; synchronized?: boolean },
): Promise<Agent>;
/** Returns the list of synchronized agents in `projectId` whose template has been updated. */
detectDrift(projectId: string): Promise<AgentDrift[]>;
/**
* Syncs a single agent to the current version of its template.
* Returns `{ synced: true, version }` on success, `{ synced: false, version: null }` for
* scratch / non-synchronized agents.
*/
syncAgent(
projectId: string,
agentId: string,
): Promise<{ synced: boolean; version: number | null }>;
}
/** Input for {@link SkillGateway.createSkill}. */
export interface CreateSkillInput {
/** Owning project (resolved to a root; ignored on disk for `global`). */
projectId: string;
name: string;
content: string;
scope: SkillScope;
}
/**
* Skills (L12): CRUD for reusable, model-agnostic workflows in either scope, and
* agent↔skill assignment. Assigned skills are injected into the agent's
* convention file at activation (handled backend-side, ARCHITECTURE §14.2).
*/
export interface SkillGateway {
/** Lists the skills in one scope for the given project. */
listSkills(projectId: string, scope: SkillScope): Promise<Skill[]>;
/** Creates a new skill; returns the created skill. */
createSkill(input: CreateSkillInput): Promise<Skill>;
/** Updates a skill's content; returns the updated skill. */
updateSkill(
projectId: string,
scope: SkillScope,
skillId: string,
content: string,
): Promise<Skill>;
/** Deletes a skill by id from its scope's store. */
deleteSkill(
projectId: string,
scope: SkillScope,
skillId: string,
): Promise<void>;
/** Assigns a skill to an agent (idempotent). */
assignSkill(
projectId: string,
agentId: string,
skillId: string,
scope: SkillScope,
): Promise<void>;
/** Unassigns a skill from an agent (idempotent). */
unassignSkill(
projectId: string,
agentId: string,
skillId: string,
): Promise<void>;
}
/** Input for {@link MemoryGateway.createMemory}. */
export interface CreateMemoryInput {
/** Owning project (resolved to its `.ideai/` memory store). */
projectId: string;
/** Human title; also the source of the note's slug identity. */
name: string;
description: string;
type: MemoryType;
content: string;
}
/**
* Memory (L14): CRUD over project memory notes + the recall-oriented index and
* `[[wikilink]]` resolution. Identity is the **slug** (kebab-case), not a UUID;
* a note's slug is immutable, so `updateMemory` never changes it. Mirrors the
* seven backend memory commands (+ optional `recall`).
*/
export interface MemoryGateway {
/** Lists every memory note of a project (full payloads). */
listMemories(projectId: string): Promise<Memory[]>;
/** Reads a single note by slug. */
getMemory(projectId: string, slug: string): Promise<Memory>;
/** Creates a new note; returns the created note. */
createMemory(input: CreateMemoryInput): Promise<Memory>;
/** Updates a note's mutable fields (slug is immutable); returns the updated note. */
updateMemory(
projectId: string,
slug: string,
description: string,
type: MemoryType,
content: string,
): Promise<Memory>;
/** Deletes a note by slug. */
deleteMemory(projectId: string, slug: string): Promise<void>;
/** Reads the recall-oriented memory index. */
readIndex(projectId: string): Promise<MemoryIndexEntry[]>;
/** Resolves a note's `[[wikilinks]]` to the slugs of existing target notes. */
resolveLinks(projectId: string, slug: string): Promise<MemoryLink[]>;
/** Best-effort recall: the index entries most relevant to `text`, within a token budget. */
recall(
projectId: string,
text: string,
tokenBudget: number,
): Promise<MemoryIndexEntry[]>;
}
/**
* Agent input control (ARCHITECTURE §20). The mediated-input strip is gone: the
* agent cell is a **native terminal** and human keystrokes (Enter included) go
* straight to the PTY. This port only carries the two out-of-band controls:
*
* - **Interrompre** → `interrupt` (preempt the current turn — a control byte,
* not an enqueue).
* - **Ack** → `delegationDelivered`: the cell's write-portal confirms it has
* effectively written a delegation's text into the native PTY (closes the
* "the turn was delivered" loop for observability; the `ask` wake-up still
* rides on `idea_reply`).
*
* The component talks to this gateway via DI, never to `invoke()` directly.
*/
export interface InputGateway {
/** Interrompre = preempt: signals the current turn to stop (not an enqueue). */
interrupt(projectId: string, agentId: string): Promise<void>;
/**
* Ack: the cell's write-portal has written the delegation `ticket` into the
* agent's native PTY (ARCHITECTURE §20.3). Best-effort; never changes the
* ticket correlation.
*/
delegationDelivered(
projectId: string,
agentId: string,
ticket: string,
): Promise<void>;
/**
* Reports whether a **frontend terminal cell** is mounted for `agentId` (`true`
* when the cell's write-portal binds its PTY handle, `false` on unmount). The
* backend mediator needs this to deliver a turn to a **headless** agent — one
* delegated in the background with no cell, where nobody would consume
* `delegationReady`: it then writes the task into the PTY itself. An agent with a
* mounted cell keeps the write-portal path. Best-effort; never throws into the UI.
*/
setFrontAttached(agentId: string, attached: boolean): Promise<void>;
/**
* Cancels the **auto-resume** armed for a rate-limited agent (ARCHITECTURE §21):
* the user clicked "Annuler la reprise" during the cancellable window. Disarms
* the scheduled wake-up. Resolves to `true` iff a resume was effectively
* cancelled (a wake-up was still pending); `false` if none was armed or it had
* already fired (the resume then runs its course). The backend also emits
* `agentResumeCancelled` on success, which clears the countdown in the UI.
*/
cancelResume(agentId: string): Promise<boolean>;
/**
* Human net (level 3, ARCHITECTURE §21.1): arms a resume at a user-chosen
* instant for an agent whose limit was *suspected* without a reliable reset
* time. `resetsAtMs` is the chosen wake-up in epoch-milliseconds; the backend
* clamps a past instant to "now" (⇒ immediate resume). This arms the same
* cancellable resume as the automatic path and re-emits `agentResumeScheduled`,
* so the UI flips on its own from "heure inconnue" to the nominal countdown.
*/
setResumeAt(agentId: string, resetsAtMs: number): Promise<void>;
}
/**
* AI profiles & first-run (L5). Drives the first-run wizard and profile
* management: the pre-filled reference catalogue, detection of installed CLIs,
* and CRUD/batch persistence of the chosen/edited/custom profiles.
*/
export interface ProfileGateway {
/** First-run state: whether to show the wizard + the reference catalogue. */
firstRunState(): Promise<FirstRunState>;
/** The pre-filled, editable reference catalogue (Claude/Codex/Gemini/Aider). */
referenceProfiles(): Promise<AgentProfile[]>;
/** Probes each candidate's detection command; returns availability (✓/✗). */
detectProfiles(candidates: AgentProfile[]): Promise<ProfileAvailability[]>;
/** Lists the configured profiles. */
listProfiles(): Promise<AgentProfile[]>;
/** Creates or replaces (by id) a single profile; returns the saved profile. */
saveProfile(profile: AgentProfile): Promise<AgentProfile>;
/** Deletes a profile by id. */
deleteProfile(profileId: string): Promise<void>;
/**
* Clones a persisted or reference profile seed and saves the fresh profile.
* Used by Settings duplication for Codex/Claude/OpenCode identity copies.
*/
cloneProfileFromSeed(input: CloneProfileFromSeedInput): Promise<AgentProfile>;
/** Enriched Claude Code model catalogue. Manual model entry remains supported. */
listClaudeModels(): Promise<ProfileModelCatalog>;
/** Enriched Codex CLI model catalogue. Manual model entry remains supported. */
listCodexModels(): Promise<ProfileModelCatalog>;
/** Persists the batch of chosen profiles, closing the first run. */
configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]>;
/**
* Mints a new OpenCode profile from the canonical `opencode-llamacpp` seed
* (F36 — several local OpenCode profiles per project, identity = id). `name`
* overrides the seed display name; `opencode` overrides the seed config (used
* by "Duplicate" to carry an existing row's endpoint). Returns the new,
* freshly-id'd profile — it is not persisted until the batch is saved.
*/
cloneOpenCodeProfileFromSeed(
input?: CloneOpenCodeProfileFromSeedInput,
): Promise<AgentProfile>;
/**
* Static catalogue of OpenCode cloud providers (ticket #92), for the
* provider/model pickers of the Cloud sub-form.
*/
listOpenCodeProviders(): Promise<OpenCodeProviderCatalogEntry[]>;
/**
* Creates or replaces (by id) an OpenCode profile in **cloud** mode (ticket
* #92). Unlike {@link saveProfile}, this takes the literal API key: the
* backend seals it into the `SecretStore` and never returns it — the
* returned profile's `opencodeProvider` only ever carries `providerId` +
* `model` (+ the opaque `apiKeyRef`), never the literal key.
*/
saveOpenCodeProviderProfile(
input: SaveOpenCodeProviderProfileInput,
): Promise<AgentProfile>;
}
/** Input for {@link ProfileGateway.cloneOpenCodeProfileFromSeed}. */
export interface CloneOpenCodeProfileFromSeedInput {
/** Optional display name for the new profile. */
name?: string;
/** Optional OpenCode config override; when omitted, the seed config is copied. */
opencode?: OpenCodeConfig;
}
/** Input for {@link ProfileGateway.cloneProfileFromSeed}. */
export interface CloneProfileFromSeedInput {
/** Id of the persisted or reference profile to clone. */
seedProfileId: string;
/** Optional display name for the new profile. */
name?: string;
/** Optional model override. When omitted, the seed model is copied. */
model?: string;
}
/** Input for {@link ProfileGateway.saveOpenCodeProviderProfile}. */
export interface SaveOpenCodeProviderProfileInput {
/** The profile to create or replace (by id). */
profile: AgentProfile;
/** Provider id — a catalogue entry, or a free-form id when {@link custom} is set. */
providerId: string;
/** Model name served by this provider. */
model: string;
/** Literal API key — sealed into the `SecretStore`, never persisted as-is. */
apiKey: string;
/**
* Optional custom-provider configuration (ticket #92): an endpoint outside
* the OpenCode registry. Absent/`undefined` = known catalogue provider
* (unchanged behaviour).
*/
custom?: CustomProviderConfig;
}
/**
* Local model servers (F35). CRUD over the global registry of declared
* `llama.cpp` servers an OpenCode profile can bind to via
* `OpenCodeConfig.localModelServerId`. Mirrors the backend commands
* (`list_model_servers`, `save_model_server`, `delete_model_server`,
* `preview_model_server_command`, `delete_model_artifact`).
*/
export interface ModelServerGateway {
/** Lists the declared local model servers. */
listModelServers(): Promise<LocalModelServerConfig[]>;
/**
* Upserts a server config (create when `id` is a fresh client-minted UUID,
* update otherwise); returns the persisted config.
*/
saveModelServer(config: LocalModelServerConfig): Promise<LocalModelServerConfig>;
/**
* Deletes a server by id. Rejects with a `GatewayError` whose `code` is
* `model_server_in_use` when a profile still references it.
*/
deleteModelServer(serverId: string): Promise<void>;
/**
* Deletes the IdeA-managed downloaded model artifact for a server while
* keeping the server config. Rejects with `invalid` for local `.gguf` paths,
* `model_server_in_use` while downloading or used by a live agent, and
* `not_configured` when the server no longer exists.
*/
deleteModelArtifact(serverId: string): Promise<void>;
/**
* Builds the `llama-server` command line the backend would launch for the
* draft config, without persisting it. The backend is the sole authority on
* the argv (never reconstruct it client-side). Rejects when the config is
* invalid (e.g. `modelSource` missing).
*/
previewModelServerCommand(
config: LocalModelServerConfig,
): Promise<ModelServerCommandPreview>;
}
/**
* Embedded web server lifecycle + exposure settings (ticket #68). Backs the
* desktop `Settings → Deployment` surface, so the user can expose IdeA to
* remote devices without a command line.
*
* **Desktop-only.** Only the Tauri transport implements it for real; the web
* transport rejects (a web client is *served by* this server and must not
* reconfigure it). See `desktop-only.test.ts`.
*
* The backend owns every network fact: LAN candidates and the proxy upstream
* URL come from {@link previewExposure}, never from client-side derivation.
*/
export interface DesktopServerGateway {
/** Reads the persisted exposure settings. */
getExposureSettings(): Promise<ServerExposureSettings>;
/**
* Validates and persists exposure settings. Rejects with a `GatewayError`
* (`INVALID`) when the config is inconsistent — e.g. a remote mode without an
* `https://` `publicOrigin`, or `remoteProxyOtherMachine` without a concrete
* `lanBindAddress` / a non-empty `trustedProxies`.
*/
saveExposureSettings(settings: ServerExposureSettings): Promise<void>;
/**
* Derives LAN candidates, the upstream URL and non-fatal warnings for a
* **draft** config, without persisting it. The single source of truth for the
* addresses the UI displays.
*/
previewExposure(
settings: ServerExposureSettings,
): Promise<ServerExposurePreview>;
/** Current server status. */
status(): Promise<EmbeddedServerStatus>;
/** Starts the server with the persisted settings; resolves with the new status. */
start(): Promise<EmbeddedServerStatus>;
/** Stops the server; resolves with the new status. */
stop(): Promise<EmbeddedServerStatus>;
/**
* Observes status transitions. The backend emits no status event today, so
* the Tauri adapter polls `embedded_server_status`; the schedule is a
* transport detail owned by the adapter, and callers must not depend on it.
*/
onStatusChanged(
handler: (status: EmbeddedServerStatus) => void,
): Promise<Unsubscribe>;
}
/** Input for {@link EmbedderGateway.saveEmbedderProfile}. */
export interface SaveEmbedderProfileInput {
profile: EmbedderProfile;
}
/**
* Embedder configuration (L14 / lot C2). Drives the memory/embedder settings
* panel: lists the configured embedder profiles, the available engines (with
* build-time feature flags so unavailable strategies can be shown disabled),
* and persists / removes profiles. Changing the active embedder takes effect at
* the next app start. Mirrors the four backend embedder commands.
*/
export interface EmbedderGateway {
/** Lists the configured embedder profiles (transparent: no secrets). */
listEmbedderProfiles(): Promise<EmbedderProfile[]>;
/** Creates or replaces (by id) a single profile; returns the saved profile. */
saveEmbedderProfile(profile: EmbedderProfile): Promise<EmbedderProfile>;
/** Deletes an embedder profile by id. */
deleteEmbedderProfile(embedderId: string): Promise<void>;
/** Describes the available engines + build feature flags + detection. */
describeEmbedderEngines(): Promise<EmbedderEngines>;
}
/**
* Paired-device management (ticket #77) — the access surface of the instance.
*
* Mounted identically on web and desktop, so it is a **port**, not a web
* concern: the desktop adapter reaches the same use cases through the Tauri
* composition root, the web adapter over HTTP. Revoking is authoritative
* server-side; revoking the *current* device ends this session, which callers
* detect from {@link PairedDevice.isCurrentDevice} before the call.
*/
export interface DeviceGateway {
/** Lists every paired device, most recently active first (backend order). */
listDevices(): Promise<PairedDevice[]>;
/**
* Generates a fresh single-use pairing code, invalidating any previous one.
* The code exists only in server memory until it is consumed or expires.
*/
createPairingCode(): Promise<PairingCode>;
/** Renames one device (1-40 characters). */
renameDevice(deviceId: string, name: string): Promise<void>;
/** Revokes one device; its live WebSockets are closed server-side (B3). */
revokeDevice(deviceId: string): Promise<void>;
/** Revokes every device, the current one included. */
revokeAllDevices(): Promise<void>;
}
/** Project and agent permission management (LP1). */
export interface PermissionGateway {
/** Reads the full project permission document. */
getProjectPermissions(projectId: string): Promise<ProjectPermissions>;
/** Replaces or removes project-level default permissions. */
updateProjectPermissions(
projectId: string,
permissions: PermissionSet | null,
): Promise<ProjectPermissions>;
/** Replaces or removes one agent-specific override. */
updateAgentPermissions(
projectId: string,
agentId: string,
permissions: PermissionSet | null,
): Promise<ProjectPermissions>;
/** Resolves effective permissions for one agent. */
resolveAgentPermissions(
projectId: string,
agentId: string,
): Promise<EffectivePermissions | null>;
/** Reads the full project system-permission document. */
getProjectSystemPermissions(projectId: string): Promise<ProjectSystemPermissions>;
/** Replaces or removes project-level default system permissions. */
updateProjectSystemPermissions(
projectId: string,
permissions: SystemPermissionSet | null,
): Promise<ProjectSystemPermissions>;
/** Replaces or removes one agent-specific system-permission override. */
updateAgentSystemPermissions(
projectId: string,
agentId: string,
permissions: SystemPermissionSet | null,
): Promise<ProjectSystemPermissions>;
/** Resolves wanted/effective runtime-constrained system permissions. */
resolveAgentSystemPermissions(
projectId: string,
agentId: string,
): Promise<ResolvedAgentSystemPermissions>;
/**
* Reads the project's durable MCP tool permission document plus the
* backend-canonical catalogue classification (ticket #82). Distinct
* document from the file/command permissions above.
*/
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions>;
/** Replaces or removes the project-wide default MCP tool policy. */
updateProjectMcpToolPermissions(
projectId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions>;
/** Replaces or removes one agent's MCP tool policy override. */
updateAgentMcpToolPermissions(
projectId: string,
agentId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions>;
}
/** Read-only live work-state read-model for conversations/delegations. */
export interface WorkStateGateway {
/** Reads the current per-agent live/offline and idle/busy state for a project. */
getProjectWorkState(projectId: string): Promise<ProjectWorkState>;
/**
* Attaches a UI output subscriber to a background task. Running tasks replay
* PTY scrollback and then stream live chunks; terminal tasks return only their
* persisted output tail.
*/
attachBackgroundTask(
taskId: string,
onData: (bytes: Uint8Array) => void,
): Promise<BackgroundTaskAttachment>;
/**
* Cancels a running/pending background task by id. The read-model refreshes
* through the `backgroundTaskChanged` domain event.
*/
cancelBackgroundTask(taskId: string): Promise<void>;
/**
* Re-runs a failed/cancelled background task under a **new** task id (the
* original id is never reused).
*/
retryBackgroundTask(taskId: string): Promise<void>;
}
/** One paginated page request over a conversation transcript (LS7). */
export interface ConversationPageRequest {
/** Turn id to paginate around; omit to start from a thread end. */
anchor?: string;
/** Travel direction; defaults to `"backward"` (latest page first). */
direction?: PageDirection;
/** Requested page size (clamped by the backend to `[1, 200]`). */
limit?: number;
}
/** Read-only, paginated access to a conversation's full transcript (LS7). */
export interface ConversationGateway {
/** Reads one page of a conversation's transcript (full text, never truncated). */
readPage(
projectId: string,
conversationId: string,
request?: ConversationPageRequest,
): Promise<TurnPage>;
}
/**
* Filter/search criteria for {@link TicketGateway.list} (ticket #12).
*
* Status and priority are **multi-select**: `statuses`/`priorities` are sets of
* accepted values with OR semantics *within* a facet and AND *across* facets. An
* empty or absent array means "no constraint on this facet" (all values pass).
* `sort` is optional; when absent, the backend preserves its historical order.
*/
export interface TicketListQuery {
statuses?: TicketStatus[];
priorities?: TicketPriority[];
assignedAgentId?: string;
createdBy?: TicketCreatorFilter;
/** Free-text match over title/description. */
text?: string;
sort?: TicketListSort;
limit?: number;
cursor?: string;
}
export type TicketCreatorFilter =
| { kind: "user" }
| { kind: "agent"; agentId: string };
export type TicketListSortField = "number" | "priority" | "status" | "title";
export type TicketListSortDirection = "asc" | "desc";
export interface TicketListSort {
field: TicketListSortField;
direction: TicketListSortDirection;
}
/** Input for {@link TicketGateway.create}. */
export interface CreateTicketInput {
title: string;
description?: string;
priority?: TicketPriority;
status?: TicketStatus;
assignedAgentIds?: string[];
}
/**
* Fields to change on {@link TicketGateway.update}. Only the provided keys are
* touched; `expectedVersion` is mandatory (optimistic concurrency — a stale
* value surfaces a `versionConflict` {@link GatewayError}).
*/
export interface UpdateTicketInput {
title?: string;
description?: string;
status?: TicketStatus;
priority?: TicketPriority;
assignedAgentIds?: string[];
expectedVersion: number;
}
/**
* Ticket gateway (public wire name for the backend `Issue` model). The only
* frontend place that knows the `ticket_*` command names; features consume this
* port through DI. Every mutation carries an `expectedVersion` and rejects with
* a `versionConflict` {@link GatewayError} when the ticket moved underneath.
*/
export interface TicketGateway {
/** Creates a ticket and returns it. */
create(projectId: string, input: CreateTicketInput): Promise<Ticket>;
/** Reads one ticket, optionally including its carnet body. */
read(
projectId: string,
ref: string,
includeCarnet?: boolean,
): Promise<Ticket>;
/** Lists tickets matching the (optional) filter/search query. */
list(projectId: string, query?: TicketListQuery): Promise<TicketList>;
/** Applies partial edits (title/description/status/priority/assignees). */
update(
projectId: string,
ref: string,
input: UpdateTicketInput,
): Promise<Ticket>;
/**
* Deletes a ticket (ticket #6). Rejects with a `notFound` {@link GatewayError}
* when the ref does not exist. On success the backend emits an `issueDeleted`
* domain event (payload `{ projectId, issueRef, freedSprint }`) — the single
* source of truth that drives the ticket's removal from lists and the closing
* of an open detail view. Callers must not mutate local state imperatively.
*/
delete(projectId: string, ref: string): Promise<void>;
/** Updates several tickets' status in one command; partial failures are per item. */
bulkUpdateStatus(
projectId: string,
refs: string[],
status: TicketStatus,
): Promise<TicketBulkResult>;
/** Updates several tickets' priority in one command; partial failures are per item. */
bulkUpdatePriority(
projectId: string,
refs: string[],
priority: TicketPriority,
): Promise<TicketBulkResult>;
/** Deletes several tickets in one command; partial failures are per item. */
bulkDelete(projectId: string, refs: string[]): Promise<TicketBulkResult>;
/** Reads the ticket-scoped Markdown carnet. */
readCarnet(projectId: string, ref: string): Promise<TicketCarnet>;
/** Replaces (not appends) the ticket-scoped carnet body. */
updateCarnet(
projectId: string,
ref: string,
carnet: string,
expectedVersion: number,
): Promise<Ticket>;
/** Links this ticket to another `#id` with the given relationship kind. */
link(
projectId: string,
ref: string,
targetRef: string,
kind: TicketLinkKind,
expectedVersion: number,
): Promise<Ticket>;
/** Removes a link (optionally scoped to a single kind). */
unlink(
projectId: string,
ref: string,
targetRef: string,
expectedVersion: number,
kind?: TicketLinkKind,
): Promise<Ticket>;
/** Assigns/unassigns an agent to the ticket. */
assign(
projectId: string,
ref: string,
agentId: string,
assigned: boolean,
expectedVersion: number,
): Promise<Ticket>;
/** Lists the project's sprints, ordered by `order` (ticket #10). */
listSprints(projectId: string): Promise<Sprint[]>;
/**
* Sets (or clears with `sprintId === null`) a ticket's sprint membership
* (ticket #10). Optimistic concurrency: `expectedVersion` must match.
*/
setTicketSprint(
projectId: string,
ref: string,
sprintId: string | null,
expectedVersion: number,
): Promise<Ticket>;
/** Creates a sprint (name required by the backend) and returns it (#11). */
createSprint(projectId: string, name: string): Promise<Sprint>;
/**
* Renames a sprint. Optimistic concurrency: `expectedVersion` must match the
* sprint's current version (#11).
*/
renameSprint(
projectId: string,
sprintId: string,
name: string,
expectedVersion: number,
): Promise<Sprint>;
/**
* Reorders sprints to the given full list of ids (execution order). Returns
* the reordered sprint list (#11).
*/
reorderSprints(projectId: string, orderedIds: string[]): Promise<Sprint[]>;
/**
* Deletes a sprint. The backend unassigns its tickets (it does NOT delete
* them); they fall back to the "no sprint" bucket (#11).
*/
deleteSprint(projectId: string, sprintId: string): Promise<void>;
/**
* Opens an AI assistant chat bound to a ticket (ticket #8). Spawns a
* structured assistant session driven by `profileId`; returns the live
* session handle. One session per ticket — reopening supersedes.
*/
openTicketChat(
projectId: string,
issueRef: string,
profileId: string,
): Promise<TicketChat>;
/** Closes (disposes) a ticket's assistant chat session (ticket #8). */
closeTicketChat(projectId: string, issueRef: string): Promise<void>;
/**
* Sends a message on an open assistant session and streams the reply back as
* {@link ReplyChunk}s via `onChunk` (ticket #8). Resolves once the turn has
* started; the turn ends at the `final` chunk. Reuses the shared `agent_send`
* chat transport (the same as the structured chat cell).
*/
sendTicketChat(
sessionId: string,
message: string,
onChunk: (chunk: ReplyChunk) => void,
): Promise<void>;
}
/**
* Payload of the OS window-lifecycle event that fires when a detached view
* window closes (ticket #23). Lets the main window re-toggle the view's
* placement out of `"detached"`.
*
* Ticket #47: a detached window is now **panel-only** and follows the main
* window's focused project rather than embedding a project id, so the close
* event no longer carries a `projectId` — the placement is keyed by `panel`.
*/
export interface ViewWindowClosed {
/** The detached panel id (a `PanelId`, kept as `string` at the port seam). */
panel: string;
}
/**
* A snapshot of one detached view window currently open at the OS level
* (ticket #50). Enumerated at startup so the main window can reconcile its
* `placements` state with windows that were already open before it mounted (a
* panel-only window restored by the OS shows as "detached", not "closed").
*
* `panel` is a `PanelId`-shaped string (kept as `string` at the port seam);
* `visible` distinguishes a truly-open window from a tracked-but-hidden one.
*/
export interface ViewWindowSnapshot {
panel: string;
label: string;
visible: boolean;
}
/**
* Detaching a View into its own OS window (ticket #23, reworked in #47).
*
* The backend owns the Tauri `WebviewWindow` lifecycle (create/focus/close,
* anti-duplicate registry). This port is the UI's only door to it — components
* never call `invoke()` directly. `panel` is a `PanelId`-shaped string; it is
* typed as `string` here so ports need not depend on the `features/` layer.
*
* A detached window is **panel-only**: it no longer carries a project id and
* instead follows the focused project published through
* {@link FocusedProjectGateway}. Windows are therefore keyed by `panel` alone.
*/
export interface WindowGateway {
/**
* Opens — or focuses, if already open — a separate OS window rendering only
* `panel` (the backend's `open_view_window` command). The window follows the
* main window's focused project; no project id is passed.
*/
openViewWindow(panel: string): Promise<void>;
/** Closes the detached window for `panel` if one is open. */
closeViewWindow(panel: string): Promise<void>;
/**
* Lists the detached view windows currently open at the OS level (ticket #50).
* Called once at main-window mount to reconcile `placements` with windows that
* survived a restart / were opened before mount, so a panel-only window shows
* as "detached" rather than "closed" in the Panneaux menu. Best-effort: callers
* treat a rejection as "no reconciliation".
*/
listOpenViewWindows(): Promise<ViewWindowSnapshot[]>;
/**
* Subscribes to detached-window close events (OS close or programmatic). The
* handler receives the `{ panel }` that closed. Returns an unsubscribe.
*/
onViewWindowClosed(
handler: (event: ViewWindowClosed) => void,
): Promise<Unsubscribe>;
}
/**
* A focused-project snapshot (ticket #47). Mirrors the backend
* `FocusedProjectDto` — the minimal identity a panel-only window needs to mount
* a project-scoped view (id for the read-models, name for chrome, root as the
* agents panel's cwd base).
*/
export interface FocusedProject {
id: string;
name: string;
root: string;
}
/**
* The focused-project channel between the main window and detached panel-only
* windows (ticket #47).
*
* The main window is the single writer: it publishes the currently active
* project (or `null` when none is open) via {@link setFocusedProject}. Detached
* windows are readers: they read the current focus at mount
* ({@link getFocusedProject}) then track changes via
* {@link onFocusedProjectChanged}. This decouples a restored panel window from
* any stale embedded project id — with no focus it shows an "open a project"
* shell and mounts no project-scoped panel.
*/
export interface FocusedProjectGateway {
/** Publishes the focused project (or `null` when no project is active). */
setFocusedProject(project: FocusedProject | null): Promise<void>;
/** Reads the current focused project; `null` when none is focused. */
getFocusedProject(): Promise<FocusedProject | null>;
/**
* Subscribes to focused-project changes. The handler receives the new focus
* (or `null`). Returns an unsubscribe.
*/
onFocusedProjectChanged(
handler: (project: FocusedProject | null) => void,
): Promise<Unsubscribe>;
}
/**
* Thin UI-preferences port (ticket #29). Persists small, per-surface pieces of
* UI state (like the ticket filters) so they survive a restart / reopen. This is
* **frontend-owned** state: the backend stays the source of tickets, never of UI
* preferences. Values are opaque JSON blobs addressed by an explicit string key;
* the adapter serialises/parses them. Reads are best-effort — an absent or
* corrupt entry yields `null` so callers fall back to their default state, and
* writes never throw (a full/blocked store degrades to "not persisted").
*
* Deliberately synchronous: hydration happens during a component's first render,
* so the persisted state is applied without a flash of default filters. The
* localStorage adapter is synchronous; a future async backend (Tauri store)
* would front this with an in-memory cache to keep the contract.
*/
export interface UiPreferencesGateway {
/** Reads and JSON-parses the blob at `key`; `null` when absent or invalid. */
read(key: string): unknown;
/** JSON-serialises and persists `value` at `key`. Best-effort (never throws). */
write(key: string, value: unknown): void;
/** Removes any persisted blob at `key`. Best-effort (never throws). */
remove(key: string): void;
}
/**
* The full set of gateways the app depends on, injected via the DI provider.
* The composition (real vs mock) is chosen in `app/`.
*/
/** Input to `reviewPackage` — a candidate package not yet committed to the store. */
export interface ReviewPluginPackageInput {
sourceKind: PluginSourceKind;
path: string;
}
/**
* Plugin system gateway (ticket #43, F1) — admin CRUD + the bootstrap catalog
* the runtime loader consumes. Mirrors the Tauri commands in carnet §5
* (`plugin_*`); no DTO shape is re-derived here beyond what the carnet froze.
*/
export interface PluginGateway {
listPlugins(): Promise<PluginAdmin[]>;
reviewPackage(input: ReviewPluginPackageInput): Promise<PluginReview>;
installFromArchive(path: string): Promise<PluginInstallResult>;
installFromDirectory(path: string): Promise<PluginInstallResult>;
setEnabled(pluginId: string, enabled: boolean): Promise<PluginAdmin>;
uninstall(pluginId: string): Promise<PluginUninstallResult>;
listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog>;
openPluginsFolder(pluginId?: string): Promise<void>;
}
export interface Gateways {
system: SystemGateway;
agent: AgentGateway;
input: InputGateway;
terminal: TerminalGateway;
project: ProjectGateway;
layout: LayoutGateway;
git: GitGateway;
remote: RemoteGateway;
profile: ProfileGateway;
modelServer: ModelServerGateway;
desktopServer: DesktopServerGateway;
template: TemplateGateway;
skill: SkillGateway;
memory: MemoryGateway;
embedder: EmbedderGateway;
device: DeviceGateway;
permission: PermissionGateway;
workState: WorkStateGateway;
conversation: ConversationGateway;
ticket: TicketGateway;
window: WindowGateway;
focusedProject: FocusedProjectGateway;
uiPreferences: UiPreferencesGateway;
plugin: PluginGateway;
}