Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions apps/chatgpt-mcp/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,112 @@ export const GetQueueOverviewInput = {
.optional()
.describe("ISO date for computing overdue counts"),
};

// ---------------------------------------------------------------
// Queue View CRUD (Milestone 29)
// ---------------------------------------------------------------

/** Filter configuration for a saved queue view. */
export const QueueViewFilters = {
workspaceId: z.string().optional().describe("Filter by workspace path"),
status: z.string().optional().describe("Filter by status prefix"),
label: z.string().optional().describe("Filter by exact label (case-insensitive)"),
assignee: z.string().optional().describe("Filter by exact assignee"),
includeArchived: z.boolean().optional().describe("Include archived runs (default: false)"),
includeSnoozed: z.boolean().optional().describe("Include snoozed runs (default: false)"),
pinnedOnly: z.boolean().optional().describe("Only pinned runs"),
snoozedOnly: z.boolean().optional().describe("Only snoozed runs"),
archivedOnly: z.boolean().optional().describe("Only archived runs"),
priorityFilter: z.enum(["low", "normal", "high", "urgent"]).optional().describe("Filter by exact priority"),
effortFilter: z.enum(["tiny", "small", "medium", "large", "xlarge"]).optional().describe("Filter by effort bucket"),
blockedOnly: z.boolean().optional().describe("Only blocked runs"),
blockedByRunId: z.string().optional().describe("Only runs blocked by specific run ID"),
dueOnOrBefore: z.string().optional().describe("Only runs with due date on or before threshold (YYYY-MM-DD)"),
triageBucketFilter: z.enum(["ready", "blocked", "deferred"]).optional().describe("Filter by triage bucket"),
staleOnly: z.boolean().optional().describe("Only stale runs"),
freshOnly: z.boolean().optional().describe("Only fresh (not stale) runs"),
blockingRunCountAtLeast: z.number().optional().describe("Only runs blocking at least N other runs"),
today: z.string().optional().describe("ISO date for overdue computation"),
Comment on lines +655 to +664
Comment on lines +659 to +664
};

/** Sort configuration for a saved queue view. */
export const QueueViewSort = {
sortByPriority: z.boolean().optional().describe("Sort by priority (descending)"),
sortByDueDate: z.boolean().optional().describe("Sort by due date (ascending, no due date sorts last)"),
sortByEffort: z.boolean().optional().describe("Sort by effort (ascending)"),
sortByTriage: z.boolean().optional().describe("Sort by triage bucket rank"),
sortByStaleness: z.boolean().optional().describe("Sort by staleness (oldest first)"),
};

/** Input schema for `create_queue_view`. */
export const CreateQueueViewInput = {
name: z.string().describe("Human-readable name for the view (required, non-empty after trim)"),
description: z.string().optional().describe("Optional description"),
filters: z.object(QueueViewFilters).optional().describe("Filter configuration"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): filters is optional in the MCP schema but required in the Rust CreateQueueViewParams, which will cause deserialization failures.

On the Rust side, CreateQueueViewParams.filters is a non-optional QueueViewFilters without #[serde(default)], so JSON requests that omit filters will fail to deserialize. In contrast, the schema and tools.ts allow filters to be undefined and pass it through as-is. Please either make filters required in the schema or update Rust to Option<QueueViewFilters> and/or add #[serde(default)] so it is genuinely optional in the protocol.

sort: z.object(QueueViewSort).optional().describe("Sort configuration"),
limit: z.number().optional().describe("Maximum runs to return"),
};

/** Input schema for `update_queue_view`. */
export const UpdateQueueViewInput = {
viewId: z.string().describe("View ID to update"),
name: z.string().optional().describe("New name (optional - if not provided, name is unchanged)"),
description: z.string().nullable().optional().describe("New description (null to clear)"),
filters: z.object(QueueViewFilters).optional().describe("New filters"),
sort: z.object(QueueViewSort).optional().describe("New sort"),
limit: z.number().nullable().optional().describe("New limit (null to clear)"),
};

/** Input schema for `delete_queue_view`. */
export const DeleteQueueViewInput = {
viewId: z.string().describe("View ID to delete"),
};

/** Input schema for `get_queue_view`. */
export const GetQueueViewInput = {
viewId: z.string().describe("View ID to retrieve"),
};

/** Input schema for `list_queue_views`. */
export const ListQueueViewsInput = {
nameContains: z.string().optional().describe("Filter by name (case-insensitive contains)"),
};

// ---------------------------------------------------------------
// Queue View Schemas (Milestone 29)
// ---------------------------------------------------------------

/** Filter configuration for saved queue views. */
export const QueueViewFiltersSchema = {
workspaceId: z.string().optional().describe("Filter by workspace path"),
status: z.string().optional().describe("Filter by status prefix"),
label: z.string().optional().describe("Filter by exact label (case-insensitive)"),
assignee: z.string().optional().describe("Filter by exact assignee"),
includeArchived: z.boolean().optional().describe("Include archived runs"),
includeSnoozed: z.boolean().optional().describe("Include snoozed runs"),
pinnedOnly: z.boolean().optional().describe("Only pinned runs"),
snoozedOnly: z.boolean().optional().describe("Only snoozed runs"),
archivedOnly: z.boolean().optional().describe("Only archived runs"),
Comment on lines +715 to +724

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Duplicated queue view filter/sort schema objects introduce drift and confusion.

We now have two parallel schema definitions for filters/sort (QueueViewFilters / QueueViewSort vs QueueViewFiltersSchema / QueueViewSortSchema), and they already differ in descriptions and enum values. Unless there’s a strong reason to keep both, prefer a single shared definition to avoid drift between what tools accept and what the daemon expects.

Suggested implementation:

/** Filter configuration for saved queue views.
 *
 * This is an alias of the canonical QueueViewFilters schema used by the daemon,
 * to avoid drift between what tools accept and what the backend expects.
 */
export const QueueViewFiltersSchema = QueueViewFilters;
  1. Ensure that QueueViewFilters is imported or defined in this file. If it lives in another module (e.g. a shared daemon schema file), add an appropriate import such as:
    import { QueueViewFilters } from "path/to/queue-view-schemas";
  2. Apply the same pattern for the sort schema to eliminate the second duplication: replace any QueueViewSortSchema = { ... } object literal with QueueViewSortSchema = QueueViewSort, and ensure QueueViewSort is imported from the canonical location.
  3. If the existing QueueViewFilters / QueueViewSort are Zod object schemas with different descriptions or enum values, reconcile those in the canonical definitions rather than reintroducing them here, so both daemon and tools stay in sync.

priorityFilter: z.enum(["low", "normal", "high", "urgent"]).optional().describe("Filter by priority"),
effortFilter: z.enum(["tiny", "small", "medium", "large", "huge"]).optional().describe("Filter by effort"),
blockedOnly: z.boolean().optional().describe("Only blocked runs"),
blockedByRunId: z.string().optional().describe("Only runs blocked by specific run ID"),
dueOnOrBefore: z.string().optional().describe("Only runs due on or before date (YYYY-MM-DD)"),
triageBucketFilter: z.enum(["urgent", "attention", "stale", "blocked", "ready"]).optional().describe("Filter by triage bucket"),
Comment on lines +726 to +730

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The MCP filter enums (effortFilter, triageBucketFilter) are inconsistent with the earlier QueueViewFilters definition and likely with the Rust types.

QueueViewFilters (used in CreateQueueViewInput) and QueueViewFiltersSchema define different value sets:

  • effortFilter: "tiny" | "small" | "medium" | "large" | "xlarge" vs "tiny" | "small" | "medium" | "large" | "huge"
  • triageBucketFilter: "ready" | "blocked" | "deferred" vs "urgent" | "attention" | "stale" | "blocked" | "ready"
    This ambiguity makes it unclear what the daemon actually accepts and risks values that don’t map to RunEffort/RunTriageBucket in Rust. Please consolidate these to a single shared enum definition aligned exactly with the Rust variants.

staleOnly: z.boolean().optional().describe("Only stale runs"),
freshOnly: z.boolean().optional().describe("Only fresh (not stale) runs"),
blockingRunCountAtLeast: z.number().optional().describe("Only runs blocking at least N other runs"),
today: z.string().optional().describe("ISO date for overdue computation"),
};
Comment on lines +714 to +735

/** Sort configuration for saved queue views. */
export const QueueViewSortSchema = {
sortByPriority: z.boolean().optional().describe("Sort by priority (descending)"),
sortByDueDate: z.boolean().optional().describe("Sort by due date (ascending)"),
sortByEffort: z.boolean().optional().describe("Sort by effort (ascending)"),
sortByTriage: z.boolean().optional().describe("Sort by triage bucket rank"),
sortByStaleness: z.boolean().optional().describe("Sort by staleness (oldest first)"),
};

/** Input schema for `create_queue_view`. */

91 changes: 91 additions & 0 deletions apps/chatgpt-mcp/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ import {
AssignRunOwnerInput,
SetRunDueDateInput,
GetQueueOverviewInput,
CreateQueueViewInput,
UpdateQueueViewInput,
DeleteQueueViewInput,
GetQueueViewInput,
ListQueueViewsInput,
} from "./schemas.js";

/**
Expand Down Expand Up @@ -652,4 +657,90 @@ export function registerTools(server: McpServer, client: DaemonClient): void {
};
},
);

// ---------------------------------------------------------------
// Queue View CRUD (Milestone 29)
// ---------------------------------------------------------------

server.tool(
"create_queue_view",
"Create a saved queue view with deterministic filter/sort configuration. " +
Comment on lines +665 to +667
"The view can be applied to runs.list or runs.overview to reuse common queue slices. " +
"Names must be unique (case-insensitive).",
CreateQueueViewInput,
async (params) => {
const result = await client.call("queue_view.create", {
name: params.name,
description: params.description,
filters: params.filters,
sort: params.sort,
limit: params.limit,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);

server.tool(
"update_queue_view",
"Update a saved queue view. Only provided fields are updated; others remain unchanged. " +
"Name uniqueness is enforced on update.",
UpdateQueueViewInput,
async (params) => {
const result = await client.call("queue_view.update", {
viewId: params.viewId,
name: params.name,
description: params.description,
filters: params.filters,
sort: params.sort,
limit: params.limit,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);

server.tool(
"delete_queue_view",
"Delete a saved queue view by ID.",
DeleteQueueViewInput,
async (params) => {
const result = await client.call("queue_view.delete", {
viewId: params.viewId,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);

server.tool(
"get_queue_view",
"Get a saved queue view definition by ID.",
GetQueueViewInput,
async (params) => {
const result = await client.call("queue_view.get", {
viewId: params.viewId,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);

server.tool(
"list_queue_views",
"List all saved queue views, optionally filtered by name.",
ListQueueViewsInput,
async (params) => {
const result = await client.call("queue_view.list", {
nameContains: params.nameContains,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
}
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/deterministic-daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] }
uuid = { workspace = true, features = ["v4"] }
chrono = { workspace = true }
rusqlite = { workspace = true }
lazy_static = "1.4"

[dev-dependencies]
tempfile = { workspace = true }
Expand Down
Loading
Loading