Files
IdeA/frontend/src/domain/index.ts
Blomios c1d82eee8d feat(tickets): surface frontend V1 du système de tickets
Surface UI complète des tickets (domaine Issue exposé « ticket »),
validée QA de bout en bout : cargo build + tests backend verts,
npm run build + npm test (459) verts.

- Domaine : DTO Ticket, 9 events Issue* + guard isTicketEvent.
- Ports : TicketGateway (+ types query/input) ajouté à Gateways.
- Adapters : TauriTicketGateway réel (+ isTicketVersionConflict),
  wiring, et MockTicketGateway pour les tests.
- Feature tickets : hooks (useTickets, useTicketDetail,
  useProjectAgents), TicketsPanel, TicketDetail, TicketsView,
  ticketMeta + tests.
- ProjectsView : onglet sidebar « Tickets ».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:56:36 +02:00

845 lines
26 KiB
TypeScript

/**
* Pure UI-domain types — TS mirrors of the backend DTOs (camelCase wire format,
* ARCHITECTURE §1.3). No React, no Tauri here: these are plain data shapes the
* ports speak in, so view logic and gateways stay testable in isolation.
*/
/** Health report returned by the `health` gateway/command. */
export interface HealthReport {
version: string;
alive: boolean;
timeMillis: number;
correlationId: string;
note: string | null;
}
/** A domain event relayed from the backend (tagged union on `type`). */
export type DomainEvent =
| { type: "projectCreated"; projectId: string }
| { type: "agentLaunched"; agentId: string; sessionId: string }
| { type: "agentExited"; agentId: string; code: number }
| { type: "agentProfileChanged"; agentId: string; profileId: string }
| { type: "agentBusyChanged"; agentId: string; busy: boolean }
| {
/**
* A delegation is ready to be injected into the agent's native terminal
* (ARCHITECTURE §20). The backend is the queue/busy authority but no longer
* PTY-writes the turn: the frontend write-portal runs the handshake (b→e)
* and writes `text` + `submitSequence`. `submitSequence`/`submitDelayMs`
* come from the target's profile; absent ⇒ the portal applies its defaults
* (`"\r"`, ~350 ms).
*/
type: "delegationReady";
agentId: string;
ticket: string;
text: string;
submitSequence?: string;
submitDelayMs?: number;
}
| {
/**
* An agent entered a session/rate limit (ARCHITECTURE §21). Low-frequency,
* model-agnostic. `resetsAtMs` is the reset instant in epoch-milliseconds;
* absent ⇒ unknown (no reliable time → no auto-resume). The frontend badges
* "limité jusqu'à HH:MM" when known, "limité" otherwise.
*/
type: "agentRateLimited";
agentId: string;
resetsAtMs?: number;
}
| {
/**
* An auto-resume was armed for a rate-limited agent (ARCHITECTURE §21).
* `fireAtMs` is the wake-up deadline in epoch-milliseconds. The frontend
* shows the countdown + the "Annuler la reprise" button (cancellable window).
*/
type: "agentResumeScheduled";
agentId: string;
fireAtMs: number;
}
| {
/**
* An agent's auto-resume was cancelled (ARCHITECTURE §21): the user clicked
* "Annuler la reprise". The frontend removes the countdown but keeps the
* "limité" state (no auto-resume will fire).
*/
type: "agentResumeCancelled";
agentId: string;
}
| {
/**
* An agent was effectively resumed after a limit (ARCHITECTURE §21): the
* wake-up fired (or an immediate resume). The frontend clears all limit state.
*/
type: "agentResumed";
agentId: string;
}
| {
/**
* Human net (level 3): a session limit is suspected without any reliable
* reset time (ARCHITECTURE §21.1). IdeA never resumes blind: this asks the
* frontend to surface an "heure inconnue" state and prompt the user.
* `resetsAtMs` carries an estimate when one exists, else absent.
*/
type: "agentRateLimitSuspected";
agentId: string;
resetsAtMs?: number;
}
| { type: "templateUpdated"; templateId: string; version: number }
| { type: "agentDriftDetected"; agentId: string; from: number; to: number }
| { type: "agentSynced"; agentId: string; to: number }
| { type: "layoutChanged"; projectId: string }
| { type: "remoteConnected"; projectId: string }
| { type: "gitStateChanged"; projectId: string }
| { type: "memorySaved"; slug: string }
| {
type: "orchestratorRequestProcessed";
requesterId: string;
action: string;
ok: boolean;
/**
* Which entry door the delegation arrived through: `"mcp"` (MCP server) or
* `"file"` (`.ideai/requests` watcher). Optional for backward-compatibility:
* an event relayed without it simply yields no source badge.
*/
source?: "mcp" | "file";
}
| { type: "issueCreated"; issueId: string; issueRef: string }
| { type: "issueUpdated"; issueRef: string; version: number }
| {
type: "issueStatusChanged";
issueRef: string;
status: TicketStatus;
version: number;
}
| {
type: "issuePriorityChanged";
issueRef: string;
priority: TicketPriority;
version: number;
}
| { type: "issueCarnetUpdated"; issueRef: string; version: number }
| {
type: "issueLinked";
issueRef: string;
target: string;
kind: TicketLinkKind;
version: number;
}
| {
type: "issueUnlinked";
issueRef: string;
target: string;
kind: TicketLinkKind;
version: number;
}
| {
type: "issueAgentAssigned";
issueRef: string;
agentId: string;
version: number;
}
| {
type: "issueAgentUnassigned";
issueRef: string;
agentId: string;
version: number;
}
| { type: "ptyOutput"; sessionId: string; bytes: number[] };
/**
* Whether a domain event is one of the ticket (`Issue*`) events. A small helper
* so ticket view-models refresh on any ticket mutation without re-enumerating
* every variant at each call site.
*/
export function isTicketEvent(
event: DomainEvent,
): event is Extract<DomainEvent, { type: `issue${string}` }> {
return event.type.startsWith("issue");
}
/** Where a project physically lives (mirror of the backend `RemoteRef`). */
export type RemoteRef =
| { kind: "local" }
| {
kind: "ssh";
host: string;
port: number;
user: string;
auth: unknown;
remoteRoot: string;
}
| { kind: "wsl"; distro: string };
/** A project as returned by the `project` gateway (mirror of `ProjectDto`). */
export interface Project {
id: string;
name: string;
root: string;
remote: RemoteRef;
createdAt: number;
}
/** Stable error shape mirrored from the backend `ErrorDto`. */
export interface GatewayError {
code: string;
message: string;
}
// ---------------------------------------------------------------------------
// Work state (UX conversations/delegations live read-model)
// ---------------------------------------------------------------------------
/** Live session currently associated with an agent in the work-state read-model. */
export interface LiveWorkSession {
nodeId: string;
sessionId: string;
kind: "pty" | "structured";
}
/** Busy/idle status for an agent in the work-state read-model. */
export type WorkBusyState =
| { state: "idle" }
| { state: "busy"; ticket: string; sinceMs: number };
/** FIFO status for an input/ticket currently tracked by the backend queue. */
export type TicketWorkStatus = "inProgress" | "queued";
/** Who produced an input/ticket in the work-state read-model. */
export type TicketWorkSource =
| { kind: "human" }
| { kind: "agent"; agentId: string };
/** One queued or in-progress input/ticket for an agent. */
export interface AgentTicketState {
ticketId: string;
conversationId: string;
position: number;
status: TicketWorkStatus;
source: TicketWorkSource;
requesterLabel: string;
taskPreview: string;
taskLen: number;
}
/** Availability of a compact conversation summary in the work-state read-model. */
export type ConversationPreviewStatus =
| "ready"
| "missing"
| "partial"
| "unavailable";
/** Recent turn preview attached to a conversation summary. */
export interface ConversationTurnWorkPreview {
role: "prompt" | "response" | "toolActivity";
source: TicketWorkSource;
atMs: number;
textPreview: string;
textLen: number;
}
/** Compact thread context joined to tickets by conversation id. */
export interface ConversationWorkSummary {
conversationId: string;
status: ConversationPreviewStatus;
objectivePreview: string | null;
summaryPreview: string | null;
summaryLen: number;
upTo: string | null;
recentTurns: ConversationTurnWorkPreview[];
}
/** One agent row in the project work-state read-model. */
export interface AgentWorkState {
agentId: string;
name: string;
profileId: string;
live?: LiveWorkSession;
busy: WorkBusyState;
tickets: AgentTicketState[];
}
/** Minimal read-only live-state surface for a project. */
export interface ProjectWorkState {
agents: AgentWorkState[];
conversations: ConversationWorkSummary[];
}
// ---------------------------------------------------------------------------
// Conversation transcript (LS7 — human thread-per-pair viewer, mirror of LS6 DTO)
// ---------------------------------------------------------------------------
/** Nature of a transcript turn (mirror of the backend `TurnRole`). */
export type TurnRole = "prompt" | "response" | "toolActivity";
/** Origin of a transcript turn (mirror of the backend `TurnSource`). */
export type TurnSource = { kind: "human" } | { kind: "agent"; agentId: string };
/** One turn of the human transcript — full text, never truncated. */
export interface TurnView {
id: string;
atMs: number;
role: TurnRole;
source: TurnSource;
text: string;
textLen: number;
}
/** Pagination travel direction (`"backward"` = towards older turns). */
export type PageDirection = "forward" | "backward";
/** A page of the human transcript, oldest-to-newest. */
export interface TurnPage {
turns: TurnView[];
hasMore: boolean;
nextAnchor?: string;
}
// ---------------------------------------------------------------------------
// Permissions (LP1)
// ---------------------------------------------------------------------------
/** Agent capability governed by IdeA permissions. */
export type Capability = "read" | "write" | "delete" | "executeBash";
/** Permission rule verdict. */
export type PermissionEffect = "allow" | "deny";
/** Fallback stance when no permission rule matches. */
export type PermissionPosture = "ask" | "allow" | "deny";
/** Glob path scope, relative to the project root. */
export type PathScope = string[];
/** Shell command matcher. */
export type CommandMatcher =
| { kind: "exact"; value: string }
| { kind: "prefix"; value: string }
| { kind: "glob"; value: string };
/** Per-command bash verdict. */
export interface CommandRule {
matcher: CommandMatcher;
effect: PermissionEffect;
}
/** One permission rule. */
export interface PermissionRule {
capability: Capability;
effect: PermissionEffect;
paths?: PathScope;
commands?: CommandRule[];
}
/** Permission policy bundle. */
export interface PermissionSet {
rules: PermissionRule[];
fallback: PermissionPosture;
}
/** One sparse agent override in `.ideai/permissions.json`. */
export interface AgentPermissionOverride {
agentId: string;
permissions: PermissionSet;
}
/** Full project permission document. */
export interface ProjectPermissions {
version: number;
projectDefaults?: PermissionSet;
agents?: AgentPermissionOverride[];
}
/** Effective project+agent policy. */
export interface EffectivePermissions {
rules: PermissionRule[];
fallback: PermissionPosture;
}
// ---------------------------------------------------------------------------
// Layout (L4) — mirror of the domain `LayoutTree` (ARCHITECTURE §3, §7).
// ---------------------------------------------------------------------------
/** Split direction: `row` = columns (left→right), `column` = rows (top→bottom). */
export type Direction = "row" | "column";
/** A terminal-hosting leaf cell. `session` is the hosted SessionId, if any.
* `agent` is the agent id if an agent is pinned to this cell (absent = plain terminal).
* `conversationId` is the persistent CLI conversation id (survives PTY close/reopen,
* lets the agent resume); omitted when absent (mirrors the backend `skip_serializing_if`).
* `agentWasRunning` records whether the cell's agent process was running at close time;
* omitted when `false`. */
export interface LeafCell {
id: string;
session?: string | null;
agent?: string;
conversationId?: string;
agentWasRunning?: boolean;
}
/** A weighted child of a split. `weight` is a relative (`> 0`) share. */
export interface WeightedChild {
node: LayoutNode;
weight: number;
}
/** A weighted n-ary split (rows or columns). */
export interface SplitContainer {
id: string;
direction: Direction;
children: WeightedChild[];
}
/** A spreadsheet grid cell placement with spans. */
export interface GridCell {
node: LayoutNode;
row: number;
col: number;
rowSpan: number;
colSpan: number;
}
/** A spreadsheet-style grid with per-row/col weights and span-based merging. */
export interface GridContainer {
id: string;
colWeights: number[];
rowWeights: number[];
cells: GridCell[];
}
/**
* A node in the layout tree. Tagged on `type` with the payload under `node`,
* matching the backend `#[serde(tag = "type", content = "node")]`.
*/
export type LayoutNode =
| { type: "leaf"; node: LeafCell }
| { type: "split"; node: SplitContainer }
| { type: "grid"; node: GridContainer };
/** The root of a tab's terminal layout. */
export interface LayoutTree {
root: LayoutNode;
}
/**
* A layout mutation (mirror of the backend `LayoutOperationDto`, tagged on
* `type`). Node/session ids are UUID strings.
*/
export type LayoutOperation =
| {
type: "split";
target: string;
direction: Direction;
newLeaf: string;
container: string;
}
| { type: "merge"; container: string; keepIndex: number }
| { type: "resize"; container: string; weights: number[] }
| { type: "move"; from: string; to: string }
| { type: "setSession"; target: string; session?: string | null }
| { type: "setCellAgent"; target: string; agent: string | null }
| { type: "setCellConversation"; target: string; conversationId: string | null }
| { type: "setAgentRunning"; target: string; running: boolean };
/** The kind of a named layout. */
export type LayoutKind = "terminal" | "gitGraph";
/** Named layout entry returned by `listLayouts`. */
export interface LayoutInfo {
id: string;
name: string;
kind: LayoutKind;
}
/**
* A commit node in the full git graph (DAG). `refs` carries short branch/tag
* names (e.g. `"main"`, `"tag: v1.0"`). `timestamp` is seconds since Unix epoch.
*/
export interface GraphCommit {
hash: string;
summary: string;
parents: string[];
refs: string[];
author: string;
timestamp: number;
}
/** Response of `listLayouts`. */
export interface LayoutList {
layouts: LayoutInfo[];
activeId: string;
}
/** Unsubscribe handle returned by event subscriptions. */
export type Unsubscribe = () => void;
// ---------------------------------------------------------------------------
// AI profiles & first-run (L5) — mirror of the domain `AgentProfile` /
// `ContextInjection` (CONTEXT §9, ARCHITECTURE §3).
// ---------------------------------------------------------------------------
/**
* Context-injection strategy (tagged on `strategy`, mirror of the backend
* `ContextInjection`):
* - `conventionFile`: write the `.md` to a conventional file (e.g. `CLAUDE.md`),
* - `flag`: pass the context path through a CLI flag,
* - `stdin`: pipe the content on stdin,
* - `env`: pass the context via an environment variable.
*/
export type ContextInjection =
| { strategy: "conventionFile"; target: string }
| { strategy: "flag"; flag: string }
| { strategy: "stdin" }
| { strategy: "env"; var: string };
/** The four injection strategy discriminants. */
export type InjectionStrategy = ContextInjection["strategy"];
/**
* A declarative AI-CLI profile (mirror of the backend `AgentProfile`). `id` is a
* UUID string; `detect` is the optional detection command line.
*/
export interface AgentProfile {
id: string;
name: string;
command: string;
args: string[];
contextInjection: ContextInjection;
detect: string | null;
cwdTemplate: string;
/** Optional CLI flags for agent-session continuity: `assignFlag` to bind a
* conversation id at launch, `resumeFlag` to resume an existing conversation. */
session?: { assignFlag?: string; resumeFlag: string };
}
/** Availability of a candidate profile after detection (mirror of the DTO). */
export interface ProfileAvailability {
profile: AgentProfile;
available: boolean;
}
/** First-run state (mirror of `FirstRunStateDto`). */
export interface FirstRunState {
isFirstRun: boolean;
referenceProfiles: AgentProfile[];
}
// ---------------------------------------------------------------------------
// Agents (L6) — mirror of the domain `Agent` / `AgentOrigin` (ARCHITECTURE §6).
// ---------------------------------------------------------------------------
/**
* Origin of an agent (tagged on `type`, mirror of the backend `AgentOrigin`):
* - `scratch`: created from scratch, no template link,
* - `fromTemplate`: derived from a template, tracking the last synced version.
*/
export type AgentOrigin =
| { type: "scratch" }
| { type: "fromTemplate"; templateId: string; syncedTemplateVersion: number };
/**
* A project-scoped agent (mirror of the backend `Agent` DTO, camelCase wire
* format). `contextPath` is the relative path of the agent's `.md` within
* `.ideai/` (e.g. `agents/foo.md`).
*/
export interface Agent {
id: string;
name: string;
contextPath: string;
profileId: string;
origin: AgentOrigin;
synchronized: boolean;
/** Skills assigned to this agent (injected into its convention file). */
skills: SkillRef[];
}
/**
* A terminal/PTY session as returned by the backend (mirror of
* `TerminalSessionDto`, camelCase wire format). Surfaced by `changeAgentProfile`
* as the freshly relaunched session when a live agent was hot-swapped.
*/
export interface TerminalSession {
/** Stable session id (UUID) — used for write/resize/close + the output channel. */
sessionId: string;
/** Working directory the shell runs in. */
cwd: string;
/** Current rows. */
rows: number;
/** Current cols. */
cols: number;
/**
* Conversation id assigned by this (re)launch, when the profile supports
* session assignment and the hosting cell had none yet; absent otherwise.
*/
assignedConversationId?: string;
}
/**
* An agent that can be resumed when its project is (re)opened (mirror of the
* backend `ResumableAgentDto`, camelCase wire format; ARCHITECTURE §15.2). It is
* a read-only inventory entry computed at open time — never a spawn:
* - `nodeId` is the layout leaf hosting the agent (where to relaunch),
* - `conversationId` is the persisted CLI conversation id; absent ⇒ relaunch fresh,
* - `wasRunning` is `agentWasRunning` frozen at close time (drives the status label),
* - `resumeSupported` reflects whether the agent's profile exposes a usable
* session strategy; when `false`, "Reprendre" relaunches fresh ("relance à neuf").
*/
export interface ResumableAgent {
agentId: string;
name: string;
nodeId: string;
conversationId?: string;
wasRunning: boolean;
resumeSupported: boolean;
}
// ---------------------------------------------------------------------------
// Skills (L12) — mirror of the domain `Skill` / `SkillRef`.
// ---------------------------------------------------------------------------
/**
* Where a skill lives (selects its backing store): `global` skills are reusable
* across projects; `project` skills are specific to one project's `.ideai/`.
*/
export type SkillScope = "global" | "project";
/**
* A reusable, model-agnostic workflow assignable to agents (mirror of the
* backend `Skill` DTO, camelCase wire format).
*/
export interface Skill {
id: string;
name: string;
contentMd: string;
scope: SkillScope;
}
/** A reference from an agent to one assigned skill (mirror of `SkillRef`). */
export interface SkillRef {
skillId: string;
scope: SkillScope;
}
// ---------------------------------------------------------------------------
// Memory (L14) — mirror of the backend `MemoryDto` / `MemoryIndexEntryDto`.
// ---------------------------------------------------------------------------
/**
* The category of a memory note (selects how it is recalled/injected). Mirrors
* the backend `type` field; identity of a note is its **slug** (kebab-case).
*/
export type MemoryType = "user" | "feedback" | "project" | "reference";
/**
* A memory note (mirror of the backend `Memory` DTO, camelCase wire format).
* `name` doubles as the human title and the source of the slug identity.
*/
export interface Memory {
name: string;
description: string;
type: MemoryType;
content: string;
}
/**
* One entry of the memory index (mirror of the backend `MemoryIndexEntry`):
* a lightweight, recall-oriented projection of a note.
*/
export interface MemoryIndexEntry {
slug: string;
title: string;
hook: string;
type: MemoryType;
}
/** A resolved `[[wikilink]]` target — the slug of another note. */
export type MemoryLink = string;
// ---------------------------------------------------------------------------
// Embedder (L14 / lot C2) — mirror of the backend `EmbedderProfileDto` and
// `EmbedderEnginesDto`. Drives the memory/embedder settings panel. Identity of
// a profile is its `id`; changing the active embedder takes effect at the next
// app start (the UI says so explicitly).
// ---------------------------------------------------------------------------
/**
* Embedding strategy of an embedder profile (mirror of the backend `strategy`).
* `none` ⇒ no vector tier (naïve recall); the other strategies select where the
* embeddings come from.
*/
export type EmbedderStrategy = "localOnnx" | "localServer" | "api" | "none";
/**
* A declarative embedder profile (mirror of the backend `EmbedderProfile` DTO,
* camelCase wire format). Transparent by design: secrets are never stored here —
* `apiKeyEnv` is the *name* of an environment variable, never the key itself.
*/
export interface EmbedderProfile {
id: string;
name: string;
strategy: EmbedderStrategy;
/** Model id/name (ONNX model, server/api model). Omitted for `none`. */
model?: string;
/** Server/API endpoint URL. Omitted for `localOnnx` / `none`. */
endpoint?: string;
/** Name of the env var holding the API key (api strategy). Never the key. */
apiKeyEnv?: string;
/** Embedding vector dimension. */
dimension: number;
}
/**
* A recommended local ONNX engine (mirror of the backend `recommendedOnnx`
* entry). `e5-small` is the curated default (`recommended: true`).
*/
export interface RecommendedOnnxEngine {
id: string;
displayName: string;
dimension: number;
approxSizeMb: number;
recommended: boolean;
}
/**
* Capabilities/availability snapshot of the embedder engines on this build
* (mirror of the backend `describe_embedder_engines`). The `vector*Enabled`
* flags reflect Cargo feature flags compiled into the build: a strategy whose
* flag is `false` is shown disabled ("not available in this build").
*/
export interface EmbedderEngines {
recommendedOnnx: RecommendedOnnxEngine[];
ollamaDetected: boolean;
onnxCachedModels: string[];
vectorHttpEnabled: boolean;
vectorOnnxEnabled: boolean;
}
// ---------------------------------------------------------------------------
// Templates (L7) — mirror of the domain `Template` / `AgentDrift`.
// ---------------------------------------------------------------------------
/**
* A reusable agent template (mirror of the backend `Template` DTO).
* `version` is incremented on each `updateTemplate` call.
*/
export interface Template {
id: string;
name: string;
contentMd: string;
version: number;
defaultProfileId: string;
}
/**
* Drift between a synchronized agent's last-synced template version and the
* template's current version (mirror of `AgentDrift` backend DTO).
*/
export interface AgentDrift {
agentId: string;
from: number;
to: number;
}
// ---------------------------------------------------------------------------
// Git (L8) — UI-domain types for the git feature.
// ---------------------------------------------------------------------------
/** A file in the working tree with its staging state. */
export interface GitFileStatus {
path: string;
staged: boolean;
}
/** A git commit summary (short representation). */
export interface GitCommit {
hash: string;
summary: string;
}
/** The list of branches and the currently checked-out branch. */
export interface GitBranches {
branches: string[];
current: string | null;
}
// ---------------------------------------------------------------------------
// Tickets (public wire name for the backend `Issue` model — "ticket" is the
// user-visible term; all commands/DTOs/events speak `ticket`/`issue`). Mirrors
// the `Ticket*Dto` shapes in `crates/app-tauri/src/tickets.rs` (camelCase).
// ---------------------------------------------------------------------------
/** A ticket reference on the wire, always `#<n>` (e.g. `"#42"`). */
export type TicketRef = string;
/** Lifecycle status of a ticket. */
export type TicketStatus = "open" | "inProgress" | "QA" | "closed";
/** Priority of a ticket. */
export type TicketPriority = "low" | "medium" | "high" | "critical";
/** The kind of relationship between two tickets. */
export type TicketLinkKind =
| "relatesTo"
| "blocks"
| "blockedBy"
| "duplicates"
| "dependsOn";
/** Who performed a ticket mutation (mirror of `TicketActorDto`, tagged `kind`). */
export type TicketActor =
| { kind: "user" }
| { kind: "agent"; agentId: string }
| { kind: "system" };
/** One directed link from a ticket to another (mirror of `TicketLinkDto`). */
export interface TicketLink {
targetRef: TicketRef;
kind: TicketLinkKind;
}
/** The full ticket, as returned by `ticket_read`/`ticket_create`/… . */
export interface Ticket {
id: string;
ref: TicketRef;
number: number;
title: string;
description: string;
status: TicketStatus;
priority: TicketPriority;
/** Present only when the ticket was read with `includeCarnet`. */
carnet?: string;
links: TicketLink[];
assignedAgentIds: string[];
createdBy: TicketActor;
updatedBy: TicketActor;
createdAt: number;
updatedAt: number;
/** Optimistic-concurrency version; echoed back as `expectedVersion` on writes. */
version: number;
}
/** A ticket summary row from `ticket_list` (mirror of `TicketSummaryDto`). */
export interface TicketSummary {
ref: TicketRef;
path: string;
title: string;
status: TicketStatus;
priority: TicketPriority;
assignedAgentIds: string[];
updatedAt: number;
}
/** One page of ticket summaries (mirror of `TicketListDto`). */
export interface TicketList {
items: TicketSummary[];
/** Opaque cursor for the next page, absent when the list is exhausted. */
nextCursor?: string;
}
/** The scoped-to-a-ticket Markdown carnet (mirror of `TicketCarnetDto`). */
export interface TicketCarnet {
ref: TicketRef;
carnet: string;
version: number;
}