The conversation viewer's 'uncommitted changes' widget only ever saw work done in a main checkout: the transcript-replay path (editsdiff) rejected every path under ~/worktrees/<dir> and ~/projects/<slug>, so a conversation working in a worktree — the normal case — showed nothing until its branch merged. - backend/worktreediff.py: diff the worktree for real. A worktree's .git file points at a host path, so it maps the host repo root onto the mounted one (PROJECTS_DIR/<slug> or REPO_DIR) and drives git with an explicit --git-dir/--work-tree pair; returns 'git diff HEAD' plus untracked files, with a 1.5s TTL cache so an SSE ping costs one round of git calls. - /api/conversation-uncommitted-diff prefers it, keeping only the replayed files that fall outside the worktree's repo; the replay stays the fallback once a worktree is torn down. - editsdiff: normalise ~/worktrees/<dir>/… and ~/projects/<slug>/… onto the repo-relative paths each repo token's git commands use, and drop __pycache__ and unexpanded-tilde noise. - UI: the Changes row shows the branch, DiffView a branch chip; diff-blob serves image blobs from the worktree copy.
1493 lines
48 KiB
Python
1493 lines
48 KiB
Python
"""Pydantic response models — the OpenAPI schema for the read endpoints.
|
||
|
||
These are **documentation-only**: they are attached to the routes via
|
||
``responses={200: {"model": …}}`` (see ``main.py``), never as ``response_model``.
|
||
FastAPI therefore serves them in ``/openapi.json`` but does **not** validate or
|
||
filter the actual returned dicts through them — so a model that drifts from the
|
||
real payload can only produce a wrong TypeScript type (caught by ``tsc``), never
|
||
a silent runtime regression.
|
||
|
||
They mirror ``frontend/src/types.ts`` field-for-field; fields are intentionally
|
||
**camelCase** (matching the JSON the handlers already emit) so Orval generates
|
||
property names identical to the hand-written types they replace. Class names
|
||
match the TS interface names so the generated model names line up 1:1.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Literal, Optional
|
||
|
||
from pydantic import BaseModel, ConfigDict
|
||
|
||
FileKind = Literal["claude-md", "skill-md", "markdown", "script", "config", "other"]
|
||
|
||
|
||
class Schema(BaseModel):
|
||
# ``model`` is a legit field name on several of these; opt out of Pydantic's
|
||
# protected ``model_`` namespace so it doesn't warn.
|
||
model_config = ConfigDict(protected_namespaces=())
|
||
|
||
|
||
# ── files / bundle ────────────────────────────────────────────────────────────
|
||
class FileEntry(Schema):
|
||
path: str
|
||
name: str
|
||
ext: str
|
||
kind: FileKind
|
||
isMarkdown: bool
|
||
bytes: int
|
||
words: int
|
||
chars: int
|
||
tokens: int
|
||
cost: float
|
||
estimated: bool
|
||
editable: bool
|
||
content: Optional[str] = None
|
||
|
||
|
||
class Pricing(Schema):
|
||
model: str
|
||
inputPerMTok: float
|
||
|
||
|
||
class Totals(Schema):
|
||
files: int
|
||
mdFiles: int
|
||
words: int
|
||
chars: int
|
||
tokens: int
|
||
cost: float
|
||
estimated: int
|
||
|
||
|
||
class Bundle(Schema):
|
||
workspace: str
|
||
totals: Totals
|
||
files: list[FileEntry]
|
||
pricing: Pricing
|
||
|
||
|
||
# ── dashboard ─────────────────────────────────────────────────────────────────
|
||
class SkillStat(Schema):
|
||
name: str
|
||
count: int
|
||
lastUsed: Optional[str] = None
|
||
contextTokens: Optional[int] = None
|
||
contextCost: Optional[float] = None
|
||
estSpend: Optional[float] = None
|
||
|
||
|
||
class DashboardTotals(Schema):
|
||
files: int
|
||
tokens: int
|
||
cost: float
|
||
estimated: int
|
||
skills: int
|
||
invocations: int
|
||
skillSpend: float
|
||
|
||
|
||
class TopFile(Schema):
|
||
path: str
|
||
tokens: int
|
||
cost: float
|
||
estimated: bool
|
||
|
||
|
||
class Dashboard(Schema):
|
||
pricing: Pricing
|
||
totals: DashboardTotals
|
||
skills: list[SkillStat]
|
||
topFiles: list[TopFile]
|
||
|
||
|
||
# ── activity dashboard ────────────────────────────────────────────────────────
|
||
class ActivityDay(Schema):
|
||
"""One local calendar day of agent activity (the heatmap's cell)."""
|
||
date: str # YYYY-MM-DD, in the caller's timezone
|
||
conversations: int # top-level runs started this day (heatmap intensity)
|
||
subagents: int # sidechain runs started this day
|
||
agents: int # conversations + subagents
|
||
activeBins: int # distinct 5-min slots that saw work (× 5 = minutes)
|
||
tokens: int
|
||
cost: float
|
||
|
||
|
||
class ActivityModel(Schema):
|
||
"""Everything one model did, across the whole archive."""
|
||
model: str
|
||
runs: int # agent runs whose busiest model was this one
|
||
conversations: int
|
||
subagents: int
|
||
activeBins: int # distinct 5-min slots this model worked in
|
||
tokens: int
|
||
cost: float
|
||
|
||
|
||
class ActivityTotals(Schema):
|
||
agents: int
|
||
conversations: int
|
||
subagents: int
|
||
activeBins: int
|
||
tokens: int
|
||
cost: float
|
||
days: int
|
||
firstDay: Optional[str] = None
|
||
lastDay: Optional[str] = None
|
||
|
||
|
||
class ActivityResponse(Schema):
|
||
"""The activity dashboard's whole payload — see ``backend/activity.py``.
|
||
|
||
``binsByDay`` maps a local ``YYYY-MM-DD`` to that day's occupied histogram
|
||
cells as positional ``[slot, modelIndex, agents]`` triples: ``slot`` is the
|
||
5-minute slot of the local day (0…``binsPerDay``-1) and ``modelIndex``
|
||
indexes ``models``. Empty slots are simply absent.
|
||
"""
|
||
binMinutes: int
|
||
binsPerDay: int
|
||
tzOffset: int # the minutes-east-of-UTC the rollup was built for
|
||
days: list[ActivityDay]
|
||
models: list[ActivityModel]
|
||
binsByDay: dict[str, list[list[int]]]
|
||
totals: ActivityTotals
|
||
|
||
|
||
# ── skills catalog ────────────────────────────────────────────────────────────
|
||
class SkillSummary(Schema):
|
||
"""One skill card: what it is on disk + what its use has cost."""
|
||
name: str # the invocation name (its dir name)
|
||
title: str # frontmatter `name:`, falls back to `name`
|
||
description: str
|
||
# None for a built-in/plugin skill: nothing of it lives in this repo, so
|
||
# there is no file to link into the editor.
|
||
dir: Optional[str] = None
|
||
path: Optional[str] = None
|
||
source: str # "repo" | <project dir-name> | "builtin"
|
||
sourceKind: str # "repo" | "project" | "builtin"
|
||
files: int
|
||
updatedAt: Optional[str] = None
|
||
count: int # total invocations across all transcripts
|
||
lastUsed: Optional[str] = None
|
||
conversations: int # distinct conversations that invoked it
|
||
contextTokens: Optional[int] = None
|
||
contextCost: Optional[float] = None
|
||
estSpend: Optional[float] = None # contextCost × count
|
||
sessionCost: float # total spend of the conversations using it
|
||
sessionTokens: int
|
||
|
||
|
||
class SkillsTotals(Schema):
|
||
skills: int
|
||
used: int
|
||
invocations: int
|
||
contextCost: float
|
||
estSpend: float
|
||
|
||
|
||
class SkillsResponse(Schema):
|
||
skills: list[SkillSummary]
|
||
totals: SkillsTotals
|
||
|
||
|
||
# ── conversations ─────────────────────────────────────────────────────────────
|
||
class ToolUsage(Schema):
|
||
tokens: int
|
||
cost: float
|
||
output: int
|
||
|
||
|
||
class ConvNotification(Schema):
|
||
title: Optional[str] = None
|
||
body: Optional[str] = None
|
||
type: Optional[str] = None
|
||
url: Optional[str] = None
|
||
at: Optional[str] = None
|
||
# The French line the desk phone read aloud (notify-done --spoken), kept so
|
||
# the UI can replay the push's sound. Absent on pushes that weren't spoken.
|
||
spoken: Optional[str] = None
|
||
|
||
|
||
class WorktreeInfo(Schema):
|
||
name: str
|
||
dir: Optional[str] = None
|
||
createdAt: Optional[str] = None
|
||
removedAt: Optional[str] = None
|
||
|
||
|
||
class ForkedFrom(Schema):
|
||
"""Where a forked conversation branched off: the source conversation and
|
||
the user-message ordinal (``mi``) whose edit created the fork."""
|
||
id: str # source conversation id (<slug>/<sid>.jsonl)
|
||
sessionId: Optional[str] = None # source session id
|
||
mi: int # message ordinal the fork cut before
|
||
|
||
|
||
class CronRef(Schema):
|
||
"""The cron job that spawned a conversation (see cron.py) — rendered as a
|
||
tag on the card/detail linking back to Settings → Cron."""
|
||
id: str
|
||
name: Optional[str] = None
|
||
|
||
|
||
class AgentRunRef(Schema):
|
||
"""The agent definition a conversation was launched from by the agent
|
||
page's "Run now" — the manual sibling of :class:`CronRef`, rendered as a
|
||
tag linking back to ``/agents/<name>``."""
|
||
name: str
|
||
at: Optional[str] = None
|
||
|
||
|
||
class ConvMeta(Schema):
|
||
state: str
|
||
# Title the session chose for itself (`conv-meta title "…"`); the backend
|
||
# already prefers it over the parsed one on every card, so the UI reads
|
||
# `title` — this is here for the raw-sidecar endpoint.
|
||
title: Optional[str] = None
|
||
projects: list[str]
|
||
services: list[str]
|
||
worktrees: Optional[list[WorktreeInfo]] = None
|
||
committed: Optional[str] = None
|
||
pushed: Optional[str] = None
|
||
merged: Optional[str] = None
|
||
deployed: Optional[str] = None
|
||
notified: Optional[str] = None
|
||
notifications: list[ConvNotification]
|
||
archived: bool = False
|
||
# Agent harness the session runs on: "claude" | "pi" (None = pre-field).
|
||
harness: Optional[str] = None
|
||
# Whether the session runs with thinking on. False ⇒ it was spawned (or last
|
||
# resumed) with thinking disabled. None = pre-field, i.e. on.
|
||
thinking: Optional[bool] = None
|
||
# The claude `--effort` level the session was spawned (or last resumed) with:
|
||
# "low" | "medium" | "high" | "xhigh" | "max". Claude-only, and None when the
|
||
# run never picked one (the CLI's own default) — pi runs never set it.
|
||
effort: Optional[str] = None
|
||
# Set when this conversation was forked off another one (edit & resubmit).
|
||
forkedFrom: Optional[ForkedFrom] = None
|
||
# Set when a cron job spawned this conversation.
|
||
cron: Optional[CronRef] = None
|
||
# Set when the agent page's "Run now" spawned this conversation.
|
||
agentRun: Optional[AgentRunRef] = None
|
||
# End-of-conversation CTAs already run on this session, {ctaId: iso} —
|
||
# a record of which buttons were pressed (ctas.py), rendered as a pill on
|
||
# the CTA linking to Settings → CTAs.
|
||
ctas: Optional[dict[str, str]] = None
|
||
# The conversation's published summary, written by the Complete CTA.
|
||
summary: Optional["ConvSummary"] = None
|
||
|
||
|
||
class ConvSummary(Schema):
|
||
"""A conversation's published summary: the filled-in scaffold markdown (see
|
||
scaffold.py) plus when it was published."""
|
||
markdown: str
|
||
at: Optional[str] = None
|
||
|
||
|
||
class Usage(Schema):
|
||
input: int
|
||
output: int
|
||
cacheRead: int
|
||
cacheWriteTokens: int
|
||
cacheWriteUnits: int
|
||
|
||
|
||
class SubagentRef(Schema):
|
||
id: str
|
||
agentType: Optional[str] = None
|
||
description: Optional[str] = None
|
||
title: Optional[str] = None
|
||
model: Optional[str] = None
|
||
tokens: Optional[int] = None
|
||
cost: Optional[float] = None
|
||
messages: Optional[int] = None
|
||
# Lifecycle of the subagent itself: ``running`` while the parent's Task call
|
||
# is still open (no tool_result yet), else ``finished``.
|
||
state: Optional[str] = None
|
||
startedAt: Optional[str] = None
|
||
endedAt: Optional[str] = None
|
||
|
||
|
||
class SubagentListItem(Schema):
|
||
"""A flat subagent (sidechain) ref with its parent conversation id — the
|
||
graph view's node source (the top-level conversation list hides these)."""
|
||
id: str
|
||
parentId: str
|
||
agentType: Optional[str] = None
|
||
description: Optional[str] = None
|
||
title: Optional[str] = None
|
||
tokens: Optional[int] = None
|
||
cost: Optional[float] = None
|
||
state: Optional[str] = None
|
||
|
||
|
||
class SubagentsResponse(Schema):
|
||
subagents: list[SubagentListItem]
|
||
|
||
|
||
class RunningAgent(Schema):
|
||
"""A Task/Agent call that hasn't returned — i.e. a subagent still running."""
|
||
toolUseId: str
|
||
agentType: Optional[str] = None
|
||
description: Optional[str] = None
|
||
startedAt: Optional[str] = None
|
||
|
||
|
||
class ParentConvRef(Schema):
|
||
id: str
|
||
title: Optional[str] = None
|
||
toolUseId: Optional[str] = None
|
||
agentType: Optional[str] = None
|
||
description: Optional[str] = None
|
||
|
||
|
||
class Task(Schema):
|
||
"""One task from the conversation's task list (TaskCreate/TaskUpdate, or the
|
||
older TodoWrite). ``status`` is one of pending / in_progress / completed /
|
||
cancelled."""
|
||
id: str
|
||
subject: str
|
||
status: str
|
||
|
||
|
||
class ConversationSummary(Schema):
|
||
id: str
|
||
sessionId: Optional[str] = None
|
||
project: Optional[str] = None
|
||
cwd: Optional[str] = None
|
||
gitBranch: Optional[str] = None
|
||
model: str
|
||
# Every model that produced a turn, busiest first (a session can switch
|
||
# models mid-thread). Rendered as the conversation's model tags.
|
||
models: Optional[list[str]] = None
|
||
# Every `--effort` level the turns actually ran at, busiest first — the read
|
||
# side of the composer's effort select, mined from the transcript. Empty for
|
||
# pi runs and for transcripts predating the CLI flag.
|
||
efforts: Optional[list[str]] = None
|
||
title: str
|
||
userTurns: int
|
||
assistantTurns: int
|
||
messages: int
|
||
startedAt: Optional[str] = None
|
||
endedAt: Optional[str] = None
|
||
tokens: int
|
||
cost: float
|
||
usage: Optional[Usage] = None
|
||
byTool: Optional[dict[str, ToolUsage]] = None
|
||
# Second-level breakdown of each activity bucket: bucket -> sub-label ->
|
||
# usage. bash by program, read/edit by file extension, msg by prompt/replies,
|
||
# skill as "skills read", other tool buckets by tool name. Powers the
|
||
# click-to-drill-down on the conversations dashboard chart.
|
||
byToolSub: Optional[dict[str, dict[str, ToolUsage]]] = None
|
||
subagentCount: Optional[int] = None
|
||
subagentTokens: Optional[int] = None
|
||
subagentCost: Optional[float] = None
|
||
# Subagents still in flight (their Task call never came back). The list card
|
||
# shows one "running" chip per entry while the conversation itself is running.
|
||
runningAgents: Optional[list[RunningAgent]] = None
|
||
# Current context size: the last assistant API call's full prompt+reply
|
||
# (input + cache read/write + output) — what the next request replays.
|
||
# `contextModel` is the model that made that call, so the UI can pick the
|
||
# right context-window size (see ModelInfo.contextWindow).
|
||
contextTokens: Optional[int] = None
|
||
contextModel: Optional[str] = None
|
||
# The session's first API call's input side, split in two: the injected
|
||
# context (system prompt, CLAUDE.md, memories, other <system-reminder>
|
||
# blocks) vs the user's actual first message. The API reports only the
|
||
# combined total, so the message share is estimated from its length
|
||
# (~4 chars/token) and the remainder is attributed to context.
|
||
firstContextTokens: Optional[int] = None
|
||
firstMessageTokens: Optional[int] = None
|
||
tasks: Optional[list[Task]] = None
|
||
meta: Optional[ConvMeta] = None
|
||
|
||
|
||
class ThreadItem(Schema):
|
||
role: Literal["user", "assistant", "tool"]
|
||
# "turn" is a content-less carrier: an API call that rendered nothing (a
|
||
# redacted-thinking turn) but was still billed. It exists so the cost chart
|
||
# sees every priced turn; the viewer never draws it.
|
||
kind: Literal[
|
||
"text", "thinking", "tool_use", "tool_result", "interrupted", "paused",
|
||
"turn"]
|
||
name: Optional[str] = None
|
||
input: Optional[Any] = None
|
||
text: Optional[str] = None
|
||
isError: Optional[bool] = None
|
||
result: Optional[str] = None
|
||
out: Optional[int] = None
|
||
turnTokens: Optional[int] = None
|
||
turnCost: Optional[float] = None
|
||
# The cached-prompt share of this turn: tokens replayed from the prompt cache
|
||
# and what they cost (10% of the input rate). The rest of `turnTokens` /
|
||
# `turnCost` is fresh input, cache writes and output.
|
||
turnCacheTokens: Optional[int] = None
|
||
turnCacheCost: Optional[float] = None
|
||
# The activity bucket this turn's cost was charged to in the `byTool`
|
||
# breakdown (msg / read / bash / …), so the cost chart colours and totals a
|
||
# turn exactly as "cost by activity" does.
|
||
turnBucket: Optional[str] = None
|
||
# On the first API call only: the share booked to the "context" bucket
|
||
# (injected system prompt, CLAUDE.md, memories), split off from this turn's
|
||
# own activity just as the breakdown does.
|
||
turnContextCost: Optional[float] = None
|
||
turnContextTokens: Optional[int] = None
|
||
# Only on the user's *first* message: how much of the session's first API
|
||
# call was injected context (system prompt, CLAUDE.md, memories, …) vs the
|
||
# message's own text (length-estimated split of the API-reported total).
|
||
firstContextTokens: Optional[int] = None
|
||
firstMessageTokens: Optional[int] = None
|
||
mi: Optional[int] = None
|
||
ts: Optional[str] = None
|
||
toolUseId: Optional[str] = None
|
||
subagent: Optional[SubagentRef] = None
|
||
# On a Task/Agent card: whether the agent it spawned is still running. Set
|
||
# even before the subagent's transcript exists, so a just-spawned agent's
|
||
# card reads "running" straight away.
|
||
agentRunning: Optional[bool] = None
|
||
# Wall-clock duration of this step: for a tool call, how long it ran (call →
|
||
# its result); for a message/thinking block, the assistant API call's latency
|
||
# (previous transcript record → this one).
|
||
durationMs: Optional[int] = None
|
||
# The model that produced this assistant step.
|
||
model: Optional[str] = None
|
||
# Size of a tool call's result payload (before clipping).
|
||
resultChars: Optional[int] = None
|
||
resultLines: Optional[int] = None
|
||
# On a `Skill` tool card: the injected SKILL.md body (shown collapsible in the
|
||
# card) and a repo-relative path to it (a link into the editor), so a skill
|
||
# invocation renders as one self-contained action instead of a huge "You" bubble.
|
||
skillBody: Optional[str] = None
|
||
skillPath: Optional[str] = None
|
||
# On a deploy tool call (npm run deploy / deploy*.sh / zipgo deploy / the
|
||
# deploy-html skill): the URL it reported, scraped from the raw — unclipped —
|
||
# output. Drives the deploy preview widget and the "open the deploy" gizmo.
|
||
deployedUrl: Optional[str] = None
|
||
|
||
|
||
class TurnUsage(Usage):
|
||
model: Optional[str] = None
|
||
|
||
|
||
class MemorySummary(Schema):
|
||
slug: str
|
||
name: str
|
||
description: str
|
||
type: Optional[str] = None
|
||
originSessionId: Optional[str] = None
|
||
projects: list[str]
|
||
services: list[str]
|
||
links: list[str]
|
||
words: int
|
||
bytes: int
|
||
updatedAt: Optional[str] = None
|
||
|
||
|
||
class ArtefactItem(Schema):
|
||
"""One generated artefact (screenshot, render, export) found under a
|
||
project/service's ``.ai/artefacts/<date>/<sessionId>/`` folder."""
|
||
kind: str # "project" | "service"
|
||
slug: str
|
||
name: str
|
||
date: str # the YYYY-MM-DD folder it landed in
|
||
path: str # <date>/<sessionId>/<file>, for /api/artefact
|
||
url: str # ready-to-use /api/artefact serve URL
|
||
bytes: int
|
||
mtime: Optional[str] = None
|
||
video: Optional[bool] = None
|
||
|
||
|
||
class ConversationDetail(ConversationSummary):
|
||
usage: Usage
|
||
thread: list[ThreadItem]
|
||
turns: list[TurnUsage]
|
||
memoriesRead: Optional[list[MemorySummary]] = None
|
||
memoriesCreated: Optional[list[MemorySummary]] = None
|
||
subagents: Optional[list[SubagentRef]] = None
|
||
parentConversation: Optional[ParentConvRef] = None
|
||
# Plans (data/plans/*.md) whose planning session is this conversation — the
|
||
# `plan` skill stamps its sessionId/conversationIds, so we join them back.
|
||
plans: Optional[list[PlanSummary]] = None
|
||
# Generated artefacts collected from the conversation's projects/services
|
||
# (.ai/artefacts/<date>/<sessionId>/), oldest first.
|
||
artefacts: Optional[list[ArtefactItem]] = None
|
||
|
||
|
||
class ConversationsResponse(Schema):
|
||
conversations: list[ConversationSummary]
|
||
# Total conversations matching the filter (pre-pagination).
|
||
count: Optional[int] = None
|
||
# Cursor for the next page (`?before=`); None when this page ends the list.
|
||
nextBefore: Optional[str] = None
|
||
pricing: Pricing
|
||
|
||
|
||
# ── notification feed (light source for the history + "new" badges) ──────────
|
||
class NotifConversation(Schema):
|
||
"""A conversation that recorded notify-done pushes, feed-relevant bits only."""
|
||
id: str
|
||
title: str
|
||
endedAt: Optional[str] = None
|
||
projects: list[str]
|
||
services: list[str]
|
||
notifications: list[ConvNotification]
|
||
|
||
|
||
class NotificationsResponse(Schema):
|
||
conversations: list[NotifConversation]
|
||
count: Optional[int] = None
|
||
|
||
|
||
class FeedNotification(Schema):
|
||
"""A flat notification feed item (the server-side mirror of the frontend's
|
||
FeedNotif), as returned by ``/api/notifications/unread``."""
|
||
id: str
|
||
convId: str
|
||
convTitle: str
|
||
title: str
|
||
body: str
|
||
type: str
|
||
url: str
|
||
at: Optional[str] = None
|
||
projects: list[str]
|
||
services: list[str]
|
||
kind: Optional[str] = None
|
||
spoken: Optional[str] = None
|
||
|
||
|
||
class UnreadNotificationsResponse(Schema):
|
||
notifications: list[FeedNotification]
|
||
count: int # items returned (after the limit)
|
||
total: int # total unread (before the limit)
|
||
|
||
|
||
# ── first-class notifications: webhooks + notify/ask log (notify.py) ─────────
|
||
class Webhook(Schema):
|
||
id: str
|
||
name: str
|
||
url: str
|
||
events: list[Literal["notify", "ask"]]
|
||
enabled: bool
|
||
|
||
|
||
class WebhooksResponse(Schema):
|
||
webhooks: list[Webhook]
|
||
|
||
|
||
class DeliveryStatus(Schema):
|
||
"""One webhook's delivery outcome for a forwarded notification."""
|
||
webhook: str
|
||
ok: bool
|
||
status: int
|
||
|
||
|
||
class NotifyResult(Schema):
|
||
ok: bool
|
||
id: str
|
||
delivered: list[DeliveryStatus]
|
||
|
||
|
||
class AskRecord(Schema):
|
||
"""An ask (choice question) with its lifecycle state."""
|
||
id: str
|
||
kind: Literal["ask"]
|
||
title: str
|
||
body: str
|
||
type: str
|
||
url: str
|
||
at: str
|
||
sessionId: str
|
||
delivered: list[DeliveryStatus]
|
||
options: list[str]
|
||
status: Literal["pending", "answered", "expired"]
|
||
answer: Optional[str] = None
|
||
answerIndex: Optional[int] = None
|
||
answeredAt: Optional[str] = None
|
||
expiresAt: Optional[str] = None
|
||
|
||
|
||
class NotifyLogEntry(Schema):
|
||
"""One recorded notification (kind "notify") or ask (kind "ask")."""
|
||
id: str
|
||
kind: Literal["notify", "ask"]
|
||
title: str
|
||
body: str
|
||
type: str
|
||
url: str
|
||
at: str
|
||
sessionId: str
|
||
delivered: list[DeliveryStatus]
|
||
# The line the desk phone read aloud, kept so the UI can replay the sound.
|
||
spoken: Optional[str] = None
|
||
# ask-only lifecycle fields (absent on plain pushes)
|
||
options: Optional[list[str]] = None
|
||
status: Optional[Literal["pending", "answered", "expired"]] = None
|
||
answer: Optional[str] = None
|
||
answerIndex: Optional[int] = None
|
||
answeredAt: Optional[str] = None
|
||
expiresAt: Optional[str] = None
|
||
|
||
|
||
class NotifyLogResponse(Schema):
|
||
notifications: list[NotifyLogEntry]
|
||
count: Optional[int] = None
|
||
|
||
|
||
# ── forms: rich structured questions answered in the PWA (forms.py) ──────────
|
||
class FormFieldOption(Schema):
|
||
value: str
|
||
label: str
|
||
|
||
|
||
class FormField(Schema):
|
||
"""One field of a form spec (see forms.FIELD_TYPES)."""
|
||
key: str
|
||
label: str
|
||
type: Literal["text", "textarea", "number", "select", "multiselect",
|
||
"radio", "checkbox", "slider", "file", "date"]
|
||
required: bool
|
||
placeholder: Optional[str] = None
|
||
help: Optional[str] = None
|
||
accept: Optional[str] = None # file: input accept filter
|
||
options: Optional[list[FormFieldOption]] = None
|
||
min: Optional[float] = None
|
||
max: Optional[float] = None
|
||
step: Optional[float] = None
|
||
multiple: Optional[bool] = None # file: allow several
|
||
default: Optional[Any] = None
|
||
|
||
|
||
class FormRecord(Schema):
|
||
"""A form with its lifecycle state; ``answers`` keyed by field key."""
|
||
id: str
|
||
title: str
|
||
description: str
|
||
type: str
|
||
url: str
|
||
sessionId: str
|
||
at: str
|
||
fields: list[FormField]
|
||
status: Literal["pending", "submitted", "cancelled"]
|
||
answers: Optional[dict[str, Any]] = None
|
||
submittedAt: Optional[str] = None
|
||
cancelledAt: Optional[str] = None
|
||
|
||
|
||
class FormsResponse(Schema):
|
||
forms: list[FormRecord]
|
||
count: Optional[int] = None
|
||
|
||
|
||
# ── full-text search index ────────────────────────────────────────────────────
|
||
class SearchMsg(Schema):
|
||
i: int
|
||
role: Literal["user", "assistant"]
|
||
text: str
|
||
|
||
|
||
class SearchConv(Schema):
|
||
id: str
|
||
title: str
|
||
project: Optional[str] = None
|
||
model: str
|
||
endedAt: Optional[str] = None
|
||
msgs: list[SearchMsg]
|
||
|
||
|
||
class SearchIndexResponse(Schema):
|
||
conversations: list[SearchConv]
|
||
count: Optional[int] = None
|
||
|
||
|
||
# ── memories ──────────────────────────────────────────────────────────────────
|
||
class MemoryConvRef(Schema):
|
||
id: str
|
||
sessionId: Optional[str] = None
|
||
title: Optional[str] = None
|
||
model: Optional[str] = None
|
||
endedAt: Optional[str] = None
|
||
cost: Optional[float] = None
|
||
tokens: Optional[int] = None
|
||
meta: Optional[ConvMeta] = None
|
||
|
||
|
||
class MemoryDetail(MemorySummary):
|
||
content: str
|
||
createdIn: list[MemoryConvRef]
|
||
readIn: list[MemoryConvRef]
|
||
|
||
|
||
class MemoriesResponse(Schema):
|
||
memories: list[MemorySummary]
|
||
|
||
|
||
# ── plans (data/plans/, written by the `plan` skill) ──────────────────────────
|
||
class PlanSummary(Schema):
|
||
slug: str
|
||
title: str
|
||
status: str
|
||
created: Optional[str] = None
|
||
updatedAt: Optional[str] = None
|
||
sessionId: Optional[str] = None
|
||
conversationIds: list[str]
|
||
projects: list[str]
|
||
services: list[str]
|
||
estCost: Optional[float] = None
|
||
estTimeMinutes: Optional[int] = None
|
||
estTokens: Optional[int] = None
|
||
files: int
|
||
steps: int
|
||
questions: int = 0
|
||
|
||
|
||
class PlanFile(Schema):
|
||
file: str
|
||
change: str
|
||
|
||
|
||
class PlanStep(Schema):
|
||
title: str
|
||
detail: str
|
||
|
||
|
||
class PlanActual(Schema):
|
||
cost: float
|
||
tokens: int
|
||
durationMs: Optional[int] = None
|
||
|
||
|
||
class PlanConvRef(Schema):
|
||
id: str
|
||
sessionId: Optional[str] = None
|
||
title: Optional[str] = None
|
||
model: Optional[str] = None
|
||
startedAt: Optional[str] = None
|
||
endedAt: Optional[str] = None
|
||
cost: Optional[float] = None
|
||
tokens: Optional[int] = None
|
||
durationMs: Optional[int] = None
|
||
meta: Optional[ConvMeta] = None
|
||
|
||
|
||
class PlanQuestionForm(Schema):
|
||
"""The plan's open questions, as an ask-form field spec (the ``## Questions``
|
||
section's fenced JSON). Answered client-side on the plan page — answers live
|
||
in the browser and feed the "implement this plan" prompt."""
|
||
title: str
|
||
description: str
|
||
fields: list[FormField]
|
||
|
||
|
||
class PlanDetail(PlanSummary):
|
||
content: str
|
||
fileList: list[PlanFile]
|
||
stepList: list[PlanStep]
|
||
conversations: list[PlanConvRef]
|
||
actual: Optional[PlanActual] = None
|
||
questionForm: Optional[PlanQuestionForm] = None
|
||
|
||
|
||
class PlansResponse(Schema):
|
||
plans: list[PlanSummary]
|
||
|
||
|
||
# ── projects ──────────────────────────────────────────────────────────────────
|
||
class ProjectCostSummary(Schema):
|
||
cost: float
|
||
tokens: int
|
||
conversations: int
|
||
|
||
|
||
class TokenTypeSpend(Schema):
|
||
tokens: int
|
||
cost: float
|
||
|
||
|
||
class ProjectByType(Schema):
|
||
input: TokenTypeSpend
|
||
output: TokenTypeSpend
|
||
cacheRead: TokenTypeSpend
|
||
cacheWrite: TokenTypeSpend
|
||
|
||
|
||
class ProjectCostDetail(ProjectCostSummary):
|
||
meanCost: float
|
||
medianCost: float
|
||
meanTokens: float
|
||
medianTokens: float
|
||
byType: ProjectByType
|
||
loc: int
|
||
meanCostPerLoc: float
|
||
|
||
|
||
# A directory's GOAL.md: the checklist rollup that tags a card, and — on a
|
||
# detail response — the markdown itself. Absent file ⇒ the field is None.
|
||
class GoalSummary(Schema):
|
||
done: int
|
||
total: int
|
||
|
||
|
||
# One checklist item on a detail page's Goal checklist widget: the raw markdown
|
||
# after its checkbox (rendered), a plain single-line summary (the Work prompt +
|
||
# tooltip), and whether it's ticked.
|
||
class GoalChecklistItem(Schema):
|
||
text: str
|
||
plain: str
|
||
checked: bool
|
||
|
||
|
||
# A goal-keeper's in-flight claim under "## Being worked on" — an item some agent
|
||
# is on right now. Surfaced on the detail (so the checklist can flag a claimed
|
||
# item) and on the board. (Defined here so GoalDetail can reference it.)
|
||
class GoalClaim(Schema):
|
||
text: str
|
||
since: Optional[str] = None # ISO — parsed off a trailing "@<iso>"
|
||
stale: bool # older than the goal-keeper's 5h cadence
|
||
|
||
|
||
class GoalDetail(GoalSummary):
|
||
content: str
|
||
items: list[GoalChecklistItem]
|
||
claims: list[GoalClaim]
|
||
|
||
|
||
# ── the goals board (/api/goals) ─────────────────────────────────────────────
|
||
# One card per project/service that keeps a GOAL.md: its progress, the version
|
||
# milestones off its "## Horizons", the themed wishlist groups, the goal-keeper
|
||
# claims in "## Being worked on", and how many agents are on it right now.
|
||
class GoalMilestone(Schema):
|
||
title: str
|
||
version: Optional[str] = None # "0.3" — absent on an unversioned horizon
|
||
current: bool # the horizon marked "(now)"
|
||
done: int
|
||
total: int
|
||
hasTasks: bool # False ⇒ prose horizon, don't render "0/0"
|
||
|
||
|
||
class GoalTheme(Schema):
|
||
title: str
|
||
done: int
|
||
total: int
|
||
|
||
|
||
class RunningConversation(Schema):
|
||
id: str
|
||
title: str
|
||
startedAt: Optional[str] = None
|
||
|
||
|
||
class GoalBoardEntry(GoalSummary):
|
||
kind: Literal["project", "service"]
|
||
dir: str
|
||
name: str
|
||
description: str
|
||
url: Optional[str] = None
|
||
updatedAt: Optional[str] = None
|
||
milestones: list[GoalMilestone]
|
||
themes: list[GoalTheme]
|
||
claims: list[GoalClaim]
|
||
# The goal's checklist, so a card can expand into its individual items (and
|
||
# put a Work button on each) without a round-trip to the detail page. Capped
|
||
# server-side — see `goal.BOARD_ITEMS` — since the board renders many goals.
|
||
items: list[GoalChecklistItem]
|
||
itemsTruncated: int # items beyond the cap, for a "+N more" hint
|
||
agents: int # sessions running on this unit right now
|
||
runningConversations: list[RunningConversation]
|
||
conversations: int
|
||
lastConversationAt: Optional[str] = None
|
||
|
||
|
||
class GoalsResponse(Schema):
|
||
goals: list[GoalBoardEntry]
|
||
|
||
|
||
class ProjectSummary(Schema):
|
||
dir: str
|
||
name: str
|
||
description: str
|
||
version: Optional[str] = None
|
||
url: Optional[str] = None
|
||
stack: list[str]
|
||
keywords: list[str]
|
||
hasPackageJson: bool
|
||
goal: Optional[GoalSummary] = None
|
||
updatedAt: Optional[str] = None
|
||
costs: Optional[ProjectCostSummary] = None
|
||
|
||
|
||
class IndexMeta(Schema):
|
||
title: Optional[str] = None
|
||
description: Optional[str] = None
|
||
ogImage: Optional[str] = None
|
||
ogUrl: Optional[str] = None
|
||
themeColor: Optional[str] = None
|
||
|
||
|
||
class OgImage(Schema):
|
||
kind: Literal["url", "asset"]
|
||
src: str
|
||
|
||
|
||
class KeyFile(Schema):
|
||
name: str
|
||
bytes: int
|
||
|
||
|
||
class ProjectCommit(Schema):
|
||
hash: str
|
||
date: str
|
||
subject: str
|
||
|
||
|
||
class ProjectDetail(ProjectSummary):
|
||
readme: Optional[str] = None
|
||
claudeMd: Optional[str] = None
|
||
goal: Optional[GoalDetail] = None
|
||
indexMeta: Optional[IndexMeta] = None
|
||
ogImage: Optional[OgImage] = None
|
||
scripts: list[str]
|
||
keyFiles: list[KeyFile]
|
||
commits: list[ProjectCommit]
|
||
costs: Optional[ProjectCostDetail] = None
|
||
|
||
|
||
class ProjectsResponse(Schema):
|
||
projects: list[ProjectSummary]
|
||
|
||
|
||
class ProjectEnvVar(Schema):
|
||
key: str
|
||
# Secrets never travel to the UI: value is None for them, hasValue says
|
||
# whether a non-empty value is set on disk.
|
||
value: Optional[str] = None
|
||
secret: bool
|
||
hasValue: bool
|
||
|
||
|
||
class ProjectEnvResponse(Schema):
|
||
slug: str
|
||
exists: bool
|
||
vars: list[ProjectEnvVar]
|
||
|
||
|
||
# ── templates ─────────────────────────────────────────────────────────────────
|
||
class TemplateSummary(Schema):
|
||
name: str
|
||
description: str
|
||
stack: list[str]
|
||
fileCount: int
|
||
sizeBytes: int
|
||
hasReadme: bool
|
||
|
||
|
||
class TemplateTreeNode(Schema):
|
||
name: str
|
||
path: str
|
||
type: Literal["dir", "file"]
|
||
size: Optional[int] = None
|
||
text: Optional[bool] = None
|
||
children: Optional[list["TemplateTreeNode"]] = None
|
||
|
||
|
||
class TemplateDetail(Schema):
|
||
name: str
|
||
description: str
|
||
stack: list[str]
|
||
fileCount: int
|
||
sizeBytes: int
|
||
readme: Optional[str] = None
|
||
scripts: dict[str, str]
|
||
dependencies: list[str]
|
||
tree: list[TemplateTreeNode]
|
||
|
||
|
||
class TemplatesResponse(Schema):
|
||
templates: list[TemplateSummary]
|
||
|
||
|
||
# ── services catalog ──────────────────────────────────────────────────────────
|
||
class ServiceSummary(Schema):
|
||
dir: str
|
||
name: str
|
||
description: str
|
||
icon: Optional[str] = None
|
||
group: Optional[str] = None
|
||
urls: list[str]
|
||
containers: list[str]
|
||
containerCount: int
|
||
images: list[str]
|
||
profiles: list[str]
|
||
auth: Optional[Literal["auth", "public", "api", "mixed"]] = None
|
||
hasCompose: bool
|
||
hasTraefik: bool
|
||
goal: Optional[GoalSummary] = None
|
||
updatedAt: Optional[str] = None
|
||
|
||
|
||
class ServiceVolume(Schema):
|
||
source: Optional[str] = None
|
||
target: Optional[str] = None
|
||
mode: Optional[str] = None
|
||
readOnly: bool
|
||
|
||
|
||
class ServiceContainer(Schema):
|
||
key: str
|
||
containerName: str
|
||
image: Optional[str] = None
|
||
build: Optional[str] = None
|
||
profiles: list[str]
|
||
restart: Optional[str] = None
|
||
networkMode: Optional[str] = None
|
||
networks: list[str]
|
||
ports: list[str]
|
||
dependsOn: list[str]
|
||
envKeys: list[str]
|
||
envFiles: list[str]
|
||
volumes: list[ServiceVolume]
|
||
hasHealthcheck: bool
|
||
composeFile: str
|
||
|
||
|
||
class ServiceRouter(Schema):
|
||
name: str
|
||
kind: Literal["http", "tcp"]
|
||
rule: str
|
||
hosts: list[str]
|
||
url: Optional[str] = None
|
||
entryPoints: list[str]
|
||
service: Optional[str] = None
|
||
middlewares: list[str]
|
||
tls: bool
|
||
priority: Optional[int] = None
|
||
|
||
|
||
class TraefikService(Schema):
|
||
name: str
|
||
servers: list[str]
|
||
|
||
|
||
class ServiceTreeNode(Schema):
|
||
name: str
|
||
path: str
|
||
type: Literal["dir", "file"]
|
||
size: Optional[int] = None
|
||
text: Optional[bool] = None
|
||
secret: Optional[bool] = None
|
||
children: Optional[list["ServiceTreeNode"]] = None
|
||
|
||
|
||
class HomepageMeta(Schema):
|
||
label: str
|
||
description: Optional[str] = None
|
||
icon: Optional[str] = None
|
||
href: Optional[str] = None
|
||
group: Optional[str] = None
|
||
|
||
|
||
class ServiceDetail(ServiceSummary):
|
||
containersDetail: list[ServiceContainer]
|
||
composeFiles: list[str]
|
||
topNetworks: list[str]
|
||
routers: list[ServiceRouter]
|
||
traefikServices: list[TraefikService]
|
||
middlewares: list[str]
|
||
envKeys: list[str]
|
||
homepage: Optional[HomepageMeta] = None
|
||
readme: Optional[str] = None
|
||
claudeMd: Optional[str] = None
|
||
goal: Optional[GoalDetail] = None
|
||
keyFiles: list[KeyFile]
|
||
commits: list[ProjectCommit]
|
||
tree: list[ServiceTreeNode]
|
||
|
||
|
||
class ServicesResponse(Schema):
|
||
services: list[ServiceSummary]
|
||
|
||
|
||
# ── small mutation / misc envelopes ───────────────────────────────────────────
|
||
class UiStateValue(Schema):
|
||
value: Optional[str] = None
|
||
|
||
|
||
class OkResponse(Schema):
|
||
ok: bool
|
||
|
||
|
||
class ModelInfo(Schema):
|
||
"""One pickable model (a model tag in the composer)."""
|
||
id: str # the exact `--model` argument
|
||
displayName: str
|
||
family: str # opus | sonnet | haiku | fable | qwen
|
||
version: str # "4.8", "5", … (within the family)
|
||
alias: Optional[str] = None # CLI shorthand, only on a family's newest
|
||
latest: bool = False
|
||
# Which agent harness runs sessions on this model: "claude" (the claude
|
||
# CLI) or "pi" (the pi.dev runner). Picking the chip picks the harness.
|
||
harness: Optional[str] = None
|
||
# pi models only: "openrouter" (hosted) or "evox2" (local LM Studio).
|
||
provider: Optional[str] = None
|
||
# Optional short chip label ("qwen 3.6 local"); the frontend derives one
|
||
# from the id when absent.
|
||
label: Optional[str] = None
|
||
# Context-window size in tokens (`max_input_tokens` from the Anthropic
|
||
# Models API, with a static per-family fallback). Drives the conversation
|
||
# page's context-fill gizmo.
|
||
contextWindow: Optional[int] = None
|
||
|
||
|
||
class ModelsResponse(Schema):
|
||
models: list[ModelInfo]
|
||
default: str # model a run uses when nothing is picked
|
||
|
||
|
||
class SpawnResult(Schema):
|
||
sessionId: str
|
||
pid: Optional[int] = None
|
||
|
||
|
||
class InterruptResult(Schema):
|
||
sessionId: str
|
||
ok: bool
|
||
|
||
|
||
class UploadedFile(Schema):
|
||
name: str
|
||
size: int
|
||
contentType: str
|
||
# Repo-relative path the composer injects into the prompt (read by the session).
|
||
repoPath: str
|
||
# Backend serve route the thread viewer renders the file from.
|
||
url: str
|
||
|
||
|
||
class UploadResult(Schema):
|
||
files: list[UploadedFile]
|
||
|
||
|
||
class ConversationMetaResponse(Schema):
|
||
meta: dict[str, ConvMeta]
|
||
|
||
|
||
class MetaUpdateResult(Schema):
|
||
id: str
|
||
meta: ConvMeta
|
||
|
||
|
||
class ArchiveOldResult(Schema):
|
||
archived: int # how many conversations were newly archived
|
||
cutoff: str # UTC start-of-yesterday used as the age threshold
|
||
|
||
|
||
# ── diff view (commit + conversation diffs) ───────────────────────────────────
|
||
class CommitRef(Schema):
|
||
sha: str
|
||
short: str
|
||
date: str
|
||
author: str
|
||
subject: str
|
||
additions: int
|
||
deletions: int
|
||
files: int
|
||
repo: str # repo token: project:<slug> | service:<slug> | super:_
|
||
|
||
|
||
class ConversationCommits(Schema):
|
||
id: str
|
||
commits: list[CommitRef]
|
||
repos: list[str]
|
||
filesChanged: int
|
||
additions: int
|
||
deletions: int
|
||
|
||
|
||
class DiffLine(Schema):
|
||
type: Literal["add", "del", "ctx"]
|
||
oldNo: Optional[int] = None
|
||
newNo: Optional[int] = None
|
||
text: str
|
||
|
||
|
||
class DiffHunk(Schema):
|
||
header: str
|
||
oldStart: int
|
||
oldLines: int
|
||
newStart: int
|
||
newLines: int
|
||
lines: list[DiffLine]
|
||
|
||
|
||
class FileDiff(Schema):
|
||
path: str
|
||
oldPath: Optional[str] = None
|
||
status: Literal["added", "deleted", "modified", "renamed"]
|
||
additions: int
|
||
deletions: int
|
||
binary: bool
|
||
truncated: bool
|
||
hunks: list[DiffHunk]
|
||
repo: Optional[str] = None
|
||
# Worktree directory this file was read from (worktree diffs only) — the
|
||
# blob route needs it to serve the right copy of an image.
|
||
wt: Optional[str] = None
|
||
|
||
|
||
class WorktreeRef(Schema):
|
||
"""A live git worktree a conversation's diff was read from."""
|
||
dir: str
|
||
branch: Optional[str] = None
|
||
repo: str
|
||
|
||
|
||
class DiffResult(Schema):
|
||
title: str
|
||
subtitle: Optional[str] = None
|
||
files: list[FileDiff]
|
||
additions: int
|
||
deletions: int
|
||
commits: Optional[list[CommitRef]] = None
|
||
approx: Optional[bool] = None
|
||
# "worktree" when the changes were read with a real `git diff` from a live
|
||
# worktree; absent/"transcript" when replayed from the transcript's edits.
|
||
source: Optional[Literal["worktree", "transcript"]] = None
|
||
worktrees: Optional[list[WorktreeRef]] = None
|
||
|
||
|
||
# ── cron jobs ────────────────────────────────────────────────────────────────
|
||
class CronRun(Schema):
|
||
"""One recorded firing of a cron job — the history mapping's entry enriched
|
||
with the conversation id the session landed on (once its transcript
|
||
synced), so the UI can link straight to it."""
|
||
at: str
|
||
sessionId: str
|
||
conversationId: Optional[str] = None
|
||
|
||
|
||
class CronRunConfig(Schema):
|
||
"""How a job's firings are launched, read out of the prompt file's YAML
|
||
frontmatter (``cron.run_config``). Every field is optional: None means the
|
||
key isn't set and the spawn path's own default applies."""
|
||
# "claude" (the claude CLI) or "pi" (the pi.dev runner).
|
||
harness: Optional[str] = None
|
||
# A `--model` value: a family alias ("sonnet") or a full model id.
|
||
model: Optional[str] = None
|
||
# `--effort` level (low|medium|high|xhigh|max); "" = explicitly no flag.
|
||
effort: Optional[str] = None
|
||
# False ⇒ run with extended thinking off; None ⇒ the CLI's own default.
|
||
thinking: Optional[bool] = None
|
||
|
||
|
||
class CronJob(Schema):
|
||
id: str
|
||
name: str
|
||
# 5-field cron expression (min hour dom mon dow), container-local time.
|
||
schedule: str
|
||
# Markdown prompt file under .claude/agents/ — its content is the prompt
|
||
# each firing spawns a session with (leading YAML frontmatter stripped),
|
||
# and its frontmatter is what `runConfig` is read from.
|
||
promptFile: str
|
||
enabled: bool
|
||
# Legacy per-job model, kept as the fallback when the frontmatter sets none.
|
||
model: Optional[str] = None
|
||
# The resolved run config from the prompt file's frontmatter.
|
||
runConfig: Optional[CronRunConfig] = None
|
||
createdAt: Optional[str] = None
|
||
lastStatus: Optional[str] = None
|
||
# Next fire time (local ISO minute); None when disabled or never.
|
||
nextRun: Optional[str] = None
|
||
# The stored history: {ISO timestamp: sessionId}.
|
||
history: dict[str, str]
|
||
# `history` enriched + sorted newest-first for rendering.
|
||
runs: list[CronRun]
|
||
|
||
|
||
class CronJobsResponse(Schema):
|
||
jobs: list[CronJob]
|
||
|
||
|
||
class CronPromptFile(Schema):
|
||
path: str
|
||
content: str
|
||
exists: bool
|
||
|
||
|
||
# ── agents catalog ───────────────────────────────────────────────────────────
|
||
# Sits after the cron models because an agent's detail carries the cron job
|
||
# scheduled on its definition file.
|
||
class AgentSummary(Schema):
|
||
"""One agent card: the definition on disk + what its runs actually cost.
|
||
|
||
The skills catalog can only *estimate* spend (context size × invocations);
|
||
an agent run is a whole conversation, so `cost`/`tokens` here are measured.
|
||
"""
|
||
name: str # the invocation name (frontmatter `name:`)
|
||
title: str
|
||
description: str
|
||
# None for a built-in agent type (`Explore`, `general-purpose`, …): nothing
|
||
# of it lives in this repo, so there is no file to link into the editor.
|
||
path: Optional[str] = None
|
||
dir: Optional[str] = None
|
||
source: str # "repo" | <project dir-name> | "builtin"
|
||
sourceKind: str # "repo" | "project" | "builtin"
|
||
tools: list[str] # frontmatter `tools:`; empty ⇒ inherits all
|
||
bytes: int
|
||
updatedAt: Optional[str] = None
|
||
# Run config declared in the frontmatter, verbatim (see agents.RUN_KEYS).
|
||
harness: Optional[str] = None
|
||
model: Optional[str] = None
|
||
effort: Optional[str] = None
|
||
thinking: Optional[str] = None
|
||
runs: int # total recorded runs, both origins
|
||
taskRuns: int # …spawned as a subagent (Task tool)
|
||
sessionRuns: int # …spawned as a session (cron / Run now)
|
||
lastRun: Optional[str] = None
|
||
conversations: int # distinct conversations it ran in
|
||
cost: float # real spend across every run
|
||
tokens: int
|
||
avgCost: float
|
||
contextTokens: Optional[int] = None # cost of loading the definition once
|
||
contextCost: Optional[float] = None
|
||
|
||
|
||
class AgentsTotals(Schema):
|
||
agents: int
|
||
used: int
|
||
runs: int
|
||
cost: float
|
||
tokens: int
|
||
|
||
|
||
class AgentsResponse(Schema):
|
||
agents: list[AgentSummary]
|
||
totals: AgentsTotals
|
||
|
||
|
||
class AgentRun(Schema):
|
||
"""One recorded run of an agent."""
|
||
agent: str
|
||
origin: str # "task" | "cron" | "manual"
|
||
at: Optional[str] = None
|
||
cost: float
|
||
tokens: int
|
||
model: Optional[str] = None
|
||
title: Optional[str] = None
|
||
# The conversation to open: for a subagent run this is its *parent* (where
|
||
# the Task card is); `subConversationId` is the child transcript itself.
|
||
conversationId: Optional[str] = None
|
||
subConversationId: Optional[str] = None
|
||
|
||
|
||
class AgentDay(Schema):
|
||
"""One day of the detail page's run sparkline."""
|
||
date: str
|
||
runs: int
|
||
cost: float
|
||
|
||
|
||
class AgentDetail(Schema):
|
||
agent: AgentSummary
|
||
content: str # the definition file verbatim ("" if none)
|
||
runs: list[AgentRun] # most recent first
|
||
daily: list[AgentDay]
|
||
cron: Optional[CronJob] = None # the job scheduled on this definition
|
||
# False when cron would reject this definition as a prompt file (nested or
|
||
# project-local) — the UI explains instead of offering a button that 400s.
|
||
canSchedule: bool
|
||
|
||
|
||
# ── conversation CTAs ────────────────────────────────────────────────────────
|
||
class CtaRun(Schema):
|
||
"""One background firing of a CTA (long-press), newest first."""
|
||
at: str
|
||
sessionId: str
|
||
|
||
|
||
class Cta(Schema):
|
||
"""A conversation CTA: a prompt file offered as a button under the last
|
||
message of a finished conversation (see ctas.py)."""
|
||
id: str
|
||
name: str
|
||
# Button text. Falls back to `name` when unset.
|
||
label: str = ""
|
||
# Tooltip in the thread, description on the composer tag.
|
||
description: str = ""
|
||
# A lucide icon name, resolved client-side.
|
||
icon: str = ""
|
||
# Markdown prompt file under .claude/agents/cta/ — its content (leading
|
||
# YAML frontmatter stripped) is what the button sends.
|
||
promptFile: str
|
||
enabled: bool
|
||
# Button order, low → left.
|
||
order: int = 0
|
||
# The prompt body itself, so a click can fill the resume box (and a tag can
|
||
# fold the prompt into a typed message) without a second round-trip.
|
||
prompt: str = ""
|
||
promptExists: bool = True
|
||
# The resolved run config from the prompt file's frontmatter. Unset keys
|
||
# mean "keep whatever the session already ran with".
|
||
runConfig: Optional[CronRunConfig] = None
|
||
createdAt: Optional[str] = None
|
||
lastStatus: Optional[str] = None
|
||
# Background firings, newest first (the stored map is {ISO: sessionId}).
|
||
history: list[CtaRun]
|
||
|
||
|
||
class CtasResponse(Schema):
|
||
ctas: list[Cta]
|
||
|
||
|
||
class CtaPromptFile(Schema):
|
||
path: str
|
||
content: str
|
||
exists: bool
|
||
|
||
|
||
class ClaudeHookEntry(Schema):
|
||
"""One native Claude Code hook from the workspace's .claude/settings.json,
|
||
flattened to a single command."""
|
||
# PreToolUse / UserPromptSubmit / Stop / …
|
||
event: str
|
||
# The tool/name matcher, when the event supports one.
|
||
matcher: Optional[str] = None
|
||
type: str = "command"
|
||
command: str
|
||
|
||
|
||
class ClaudeHooksResponse(Schema):
|
||
"""The repo's native hooks — read-only: editing .claude/settings.json from
|
||
the container would be host-level code execution."""
|
||
path: str
|
||
exists: bool
|
||
entries: list[ClaudeHookEntry]
|
||
|
||
|
||
# ── deploy status ────────────────────────────────────────────────────────────
|
||
class DeployStep(Schema):
|
||
"""One phase of the blue-green deploy, with its rendered dot state."""
|
||
n: int
|
||
label: str
|
||
# pending (grey) · active (loading) · done (green) · failed (red)
|
||
state: Literal["pending", "active", "done", "failed"]
|
||
|
||
|
||
class DeployStatus(Schema):
|
||
"""Snapshot of the in-flight ai-agent deploy (``deploy.sh``), or ``null``.
|
||
|
||
Written by the host-side deploy script into the data dir and polled by the
|
||
backend, which pings a ``deploy`` SSE event on change. Drives the sticky
|
||
in-app deploy banner (worktree label + a dot-per-phase timeline)."""
|
||
# Worktree / branch label the deploy was kicked off from.
|
||
label: Optional[str] = None
|
||
status: Literal["running", "done", "failed"]
|
||
phaseNum: int
|
||
totalPhases: int
|
||
# Human description of the current phase.
|
||
phase: Optional[str] = None
|
||
steps: list[DeployStep]
|
||
startedAt: Optional[int] = None
|
||
updatedAt: Optional[int] = None
|
||
# The conversation that triggered the deploy, for the expanded panel's link.
|
||
sessionId: Optional[str] = None
|
||
convUrl: Optional[str] = None
|
||
convTitle: Optional[str] = None
|