summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-contract/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/transport-contract/src')
-rw-r--r--packages/transport-contract/src/contract.types.test.ts21
-rw-r--r--packages/transport-contract/src/index.ts76
2 files changed, 96 insertions, 1 deletions
diff --git a/packages/transport-contract/src/contract.types.test.ts b/packages/transport-contract/src/contract.types.test.ts
index 34ff544..3cc1b1e 100644
--- a/packages/transport-contract/src/contract.types.test.ts
+++ b/packages/transport-contract/src/contract.types.test.ts
@@ -68,6 +68,17 @@ const _chatWithHttpImage: ChatRequest = {
images: [{ url: "https://example.com/diagram.png" }],
};
+// ─── ChatRequest.title (additive optional) ───────────────────────────────────
+
+const _chatWithTitle: ChatRequest = {
+ message: "implement the feature",
+ title: "Summon: add --title flag",
+};
+
+const _chatWithoutTitle: ChatRequest = {
+ message: "hello",
+};
+
// ─── Computer list / single response ─────────────────────────────────────────
const _computer: Computer = {
@@ -285,6 +296,16 @@ describe("transport-contract types compile and are exported", () => {
expect(_chatWithHttpImage.images?.[0]?.mimeType).toBeUndefined();
});
+ // ─── ChatRequest.title (additive optional) ────────────────────────────────
+
+ it("ChatRequest: title is additive optional (omittable)", () => {
+ expect(_chatWithoutTitle.title).toBeUndefined();
+ });
+
+ it("ChatRequest: carries title when set", () => {
+ expect(_chatWithTitle.title).toBe("Summon: add --title flag");
+ });
+
it("ModelsResponse: ModelMetadata carries optional vision flag", () => {
const resp: ModelsResponse = {
models: ["umans/kimi-k2.7", "umans/glm-5.2"],
diff --git a/packages/transport-contract/src/index.ts b/packages/transport-contract/src/index.ts
index 797ad22..32b03d3 100644
--- a/packages/transport-contract/src/index.ts
+++ b/packages/transport-contract/src/index.ts
@@ -121,6 +121,23 @@ export interface ChatRequest {
* defaultCwd = null).
*/
readonly workspaceId?: string;
+
+ /**
+ * A human-readable title for the conversation tab — persisted at creation
+ * time, after the new-conversation workspace setup resolves (so workspace
+ * assignment and first-turn system-prompt construction are not skipped) and
+ * before the first message append (so the append's auto-derived title does
+ * not overwrite it). The tab shows it instead of the default derived from
+ * the first message (`"Untitled"` until the first append). Omit to keep the
+ * auto-derived title. When present, the value is trimmed server-side; a
+ * whitespace-only value is treated as absent (auto-derive). A non-string
+ * value → HTTP 400 `{ error }`.
+ *
+ * Backward compatible — clients that omit it are unaffected. Mirrors the
+ * dedicated `PUT /conversations/:id/title` endpoint but is atomic with the
+ * turn (no second round-trip from the client).
+ */
+ readonly title?: string;
}
/**
@@ -468,6 +485,27 @@ export interface QueueResponse {
readonly queue: readonly QueuedMessage[];
}
+/**
+ * Response body for
+ * `DELETE /conversations/:id/queue/:messageId` — cancel (remove) a single
+ * queued steering message by id so it never runs.
+ *
+ * `cancelled` is `true` when a message with the given id was found in the
+ * conversation's queue and removed (it will never be delivered as steering nor
+ * carried into a new turn). `cancelled` is `false` when the message was not in
+ * the queue (already drained/delivered, never existed, unknown conversation)
+ * OR when the message-queue extension isn't loaded (degraded — feature off).
+ * `queue` is the post-cancel snapshot (empty when no queue extension is
+ * loaded). Idempotent — cancelling a message that is no longer queued returns
+ * `cancelled: false` with HTTP 200 (not an error), so a client may optimistically
+ * fire-and-forget a cancel and reconcile from the surface.
+ */
+export interface QueueCancelResponse {
+ readonly conversationId: string;
+ readonly cancelled: boolean;
+ readonly queue: readonly QueuedMessage[];
+}
+
// ─── Per-conversation LSP status ──────────────────────────────────────────────
/** The connection state of a single language server for a workspace. */
@@ -688,6 +726,24 @@ export interface ChatQueueMessage {
}
/**
+ * Client → server: cancel (remove) a SINGLE queued steering message by id so
+ * it never runs. The WebSocket counterpart of the HTTP
+ * `DELETE /conversations/:id/queue/:messageId` (`QueueCancelResponse`).
+ * Fire-and-forget: success is confirmed by the message-queue SURFACE updating
+ * (the cancelled message leaves the snapshot); a failure (missing/empty
+ * `conversationId` or `messageId`) arrives as a `chat.error`. Idempotent —
+ * cancelling a message that is no longer queued (already drained/delivered) is
+ * a silent no-op (no surface update, no error). `messageId` is the stable
+ * client-visible `QueuedMessage.id` (obtained from the queue surface snapshot
+ * or the enqueue response).
+ */
+export interface ChatQueueCancelMessage {
+ readonly type: "chat.queue.cancel";
+ readonly conversationId: string;
+ readonly messageId: string;
+}
+
+/**
* Every client → server WS message: surface ops (`@dispatch/ui-contract`) + chat
* ops. A server discriminates on `type`.
*/
@@ -696,7 +752,8 @@ export type WsClientMessage =
| ChatSendMessage
| ChatSubscribeMessage
| ChatUnsubscribeMessage
- | ChatQueueMessage;
+ | ChatQueueMessage
+ | ChatQueueCancelMessage;
/**
* Every server → client WS message: surface ops (`@dispatch/ui-contract`) + chat
@@ -968,10 +1025,26 @@ export interface TestComputerResponse {
* level. The scheduler resets its timer after each run completes (not a fixed
* wall-clock schedule); on backend restart it resumes scheduling for enabled
* heartbeats.
+ *
+ * `inactiveOnly` (default `true`) gates each fire on the configured workspace
+ * having NO active agents (no conversation with status `"active"` or `"queued"`):
+ * the heartbeat stays quiet while the user is actively working, and only fires
+ * when the workspace is idle. Set `false` to fire unconditionally.
*/
export interface HeartbeatConfig {
/** Whether the heartbeat loop is active for this workspace. */
readonly enabled: boolean;
+ /**
+ * When `true` (the default), the heartbeat SKIPS a fire when the configured
+ * workspace has any active agents — conversations whose persisted status is
+ * `"active"` (driving a turn) or `"queued"` (waiting on the message queue).
+ * The fire is silently skipped (no run is recorded); the scheduler re-arms
+ * and tries again at the next interval. When `false`, the heartbeat fires
+ * unconditionally regardless of workspace activity. The spawned heartbeat
+ * conversation lives in the DEDICATED heartbeat workspace, so it never
+ * counts as an "active agent" of the configured workspace (no self-block).
+ */
+ readonly inactiveOnly: boolean;
/** Custom system prompt for the heartbeat AI (empty = no system prompt). */
readonly systemPrompt: string;
/** Task prompt sent as the first user message when the heartbeat fires. */
@@ -993,6 +1066,7 @@ export interface HeartbeatConfig {
*/
export interface UpdateHeartbeatRequest {
readonly enabled?: boolean;
+ readonly inactiveOnly?: boolean;
readonly systemPrompt?: string;
readonly taskPrompt?: string;
readonly intervalMinutes?: number;