Skip to content
Open
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
116 changes: 57 additions & 59 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,59 +1,57 @@
coverage:
precision: 2
round: down
status:
project:
default:
target: auto # never regress below current baseline
threshold: 1%
webview:
target: auto # webview project ratchet: never drop below current baseline
threshold: 0.5%
flags:
- webview-ui
- webview-ui-ct
patch:
default:
target: 80% # new lines must be 80% covered
threshold: 0%
webview-patch:
target: 70% # new lines in webview must be 70% covered
threshold: 0%
flags:
- webview-ui
- webview-ui-ct

flag_management:
individual_flags:
- name: webview-ui
paths:
- webview-ui/src/
carryforward: true
- name: webview-ui-ct
paths:
- webview-ui/src/
carryforward: true
- name: core-unit
paths:
- packages/core/src/
carryforward: true
- name: core-integration
paths:
- packages/core/src/
carryforward: true

component_management:
individual_components:
- component_id: webview_components
name: "Webview UI Components"
paths:
- webview-ui/src/components/
- component_id: webview_state
name: "Webview State & Context"
paths:
- webview-ui/src/context/
- webview-ui/src/state/

comment:
layout: "diff, flags, components"
behavior: default
coverage:
precision: 2
round: down
status:
project:
default:
target: auto # never regress below current baseline
threshold: 1%
webview:
target: auto # webview project ratchet: never drop below current baseline
threshold: 0.5%
flags:
- webview-ui
- webview-ui-ct
patch:
default:
informational: true # patch coverage is advisory, not blocking
webview-patch:
informational: true # patch coverage is advisory, not blocking
flags:
- webview-ui
- webview-ui-ct

flag_management:
individual_flags:
- name: webview-ui
paths:
- webview-ui/src/
carryforward: true
- name: webview-ui-ct
paths:
- webview-ui/src/
carryforward: true
- name: core-unit
paths:
- packages/core/src/
carryforward: true
- name: core-integration
paths:
- packages/core/src/
carryforward: true

component_management:
individual_components:
- component_id: webview_components
name: "Webview UI Components"
paths:
- webview-ui/src/components/
- component_id: webview_state
name: "Webview State & Context"
paths:
- webview-ui/src/context/
- webview-ui/src/state/

comment:
layout: "diff, flags, components"
behavior: default
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./followup.js"
export * from "./git.js"
export * from "./global-settings.js"
export * from "./history.js"
export * from "./task-organization.js"
export * from "./image-generation.js"
export * from "./ipc.js"
export * from "./mcp.js"
Expand Down
182 changes: 182 additions & 0 deletions packages/types/src/task-organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { z } from "zod"

/**
* Maximum number of pinned organization targets allowed at one time.
*/
export const MAX_PINNED_TARGETS = 3

/**
* Error codes for task organization operations.
*
* Format: TASK_ORG/<DOMAIN>/<NNN>
*/
export type TaskOrganizationErrorCode =
| "TASK_ORG/VALIDATION/001"
| "TASK_ORG/CONFLICT/002"
| "TASK_ORG/PIN_LIMIT/003"
| "TASK_ORG/NOT_FOUND/004"
| "TASK_ORG/PERSISTENCE/005"
| "TASK_ORG/CORRUPT/006"
| "TASK_ORG/FUTURE_SCHEMA/007"

/**
* A canonical organization target for dragging, pinning, and folder membership.
*/
export const taskOrganizationTargetSchema = z.discriminatedUnion("kind", [
z.object({
kind: z.literal("task"),
taskId: z.string(),
}),
z.object({
kind: z.literal("autoGroup"),
rootTaskId: z.string(),
}),
z.object({
kind: z.literal("folder"),
folderId: z.string(),
}),
])

export type TaskOrganizationTargetV1 = z.infer<typeof taskOrganizationTargetSchema>

/**
* A single pinned target and the time it was pinned.
*/
export const pinnedItemSchema = z.object({
target: taskOrganizationTargetSchema,
pinnedAt: z.number(),
})

export type PinnedItemV1 = z.infer<typeof pinnedItemSchema>

/**
* A user-created manual folder containing canonical organization units.
*/
export const manualTaskFolderSchema = z.object({
folderId: z.string(),
name: z.string().min(1).max(80),
taskIds: z.array(z.string()),
createdAt: z.number(),
updatedAt: z.number(),
})

export type ManualTaskFolderV1 = z.infer<typeof manualTaskFolderSchema>

/**
* The persisted task organization aggregate for schema version 1.
*/
export const taskOrganizationStateSchema = z.object({
// Accept any positive integer so that future schema versions can be
// detected and handled gracefully by the store instead of failing
// Zod validation and being quarantined as corrupt data.
schemaVersion: z.number().int().min(1),
revision: z.number().int().min(0),
folders: z.array(manualTaskFolderSchema),
pins: z.array(pinnedItemSchema).max(MAX_PINNED_TARGETS),
updatedAt: z.number(),
Comment on lines +68 to +76

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Check the schema version before V1 schema validation.

TaskOrganizationStore.load() calls taskOrganizationStateSchema.safeParse(parsed) before it checks schemaVersion. A valid future document that removes or changes a V1-required field such as folders fails validation, is quarantined, and can later be replaced by empty state. This defeats future-schema protection.

Parse and validate only schemaVersion first. If it is greater than 1, preserve the source file and reject mutations without applying the V1 schema. Add a test with a structurally incompatible future document.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/types/src/task-organization.ts` around lines 68 - 76, The
TaskOrganizationStore.load flow must inspect and validate only schemaVersion
before applying taskOrganizationStateSchema, so structurally incompatible
documents with versions greater than 1 are not quarantined or replaced. Update
the load and mutation-protection logic to preserve future-version source files
and reject mutations, while retaining V1 validation for version 1 documents; add
coverage for a structurally incompatible future document.

})

export type TaskOrganizationStateV1 = z.infer<typeof taskOrganizationStateSchema>

/**
* Idempotent mutation commands for the organization aggregate.
*/
export const taskOrganizationMutationSchema = z.discriminatedUnion("kind", [
z.object({
kind: z.literal("createFolder"),
folderId: z.string(),
name: z.string(),
source: taskOrganizationTargetSchema,
destination: taskOrganizationTargetSchema,
}),
z.object({
kind: z.literal("createFolderFromSelection"),
folderId: z.string(),
name: z.string(),
targets: z.array(taskOrganizationTargetSchema).min(2),
}),
z.object({
kind: z.literal("deleteFolders"),
folderIds: z.array(z.string()).min(1),
}),
z.object({
kind: z.literal("renameFolder"),
folderId: z.string(),
name: z.string(),
}),
z.object({
kind: z.literal("deleteFolder"),
folderId: z.string(),
}),
z.object({
kind: z.literal("moveToFolder"),
source: taskOrganizationTargetSchema,
folderId: z.string(),
}),
z.object({
kind: z.literal("removeFromFolder"),
source: taskOrganizationTargetSchema,
folderId: z.string(),
}),
z.object({
kind: z.literal("setPinned"),
target: taskOrganizationTargetSchema,
pinned: z.boolean(),
}),
])

export type TaskOrganizationMutationV1 = z.infer<typeof taskOrganizationMutationSchema>

/**
* A webview -> host mutation request carrying the client request ID and the
* last observed revision so the host can detect stale clients.
*/
export const taskOrganizationMutationRequestSchema = z.object({
requestId: z.string(),
baseRevision: z.number().int().min(0),
mutation: taskOrganizationMutationSchema,
})

export type TaskOrganizationMutationRequestV1 = z.infer<typeof taskOrganizationMutationRequestSchema>

/**
* Host -> webview acknowledgement or typed rejection for a mutation request.
*/
export const taskOrganizationMutationResultSchema = z.object({
requestId: z.string(),
success: z.boolean(),
committedRevision: z.number().int().min(0),
error: z
.object({
code: z.enum([
"TASK_ORG/VALIDATION/001",
"TASK_ORG/CONFLICT/002",
"TASK_ORG/PIN_LIMIT/003",
"TASK_ORG/NOT_FOUND/004",
"TASK_ORG/PERSISTENCE/005",
"TASK_ORG/CORRUPT/006",
"TASK_ORG/FUTURE_SCHEMA/007",
]),
message: z.string(),
})
.optional(),
})

export type TaskOrganizationMutationResultV1 = z.infer<typeof taskOrganizationMutationResultSchema>

/**
* Creates an empty, version-1 task organization state.
*
* @param now - Optional clock function for deterministic timestamps.
* Defaults to `Date.now`. Pass a fixed-value function in tests to
* avoid timestamp races.
*/
export function createEmptyTaskOrganizationState(now?: () => number): TaskOrganizationStateV1 {
return {
schemaVersion: 1,
revision: 0,
folders: [],
pins: [],
updatedAt: (now ?? Date.now)(),
}
}
34 changes: 34 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { z } from "zod"
import type { GlobalSettings, RooCodeSettings } from "./global-settings.js"
import type { ProviderSettings, ProviderSettingsEntry } from "./provider-settings.js"
import type { HistoryItem } from "./history.js"
import type {
TaskOrganizationStateV1,
TaskOrganizationMutationRequestV1,
TaskOrganizationMutationResultV1,
} from "./task-organization.js"
import type { ModeConfig, PromptComponent } from "./mode.js"
import type { Experiments } from "./experiment.js"
import type { ClineMessage, QueuedMessage } from "./message.js"
Expand Down Expand Up @@ -103,6 +108,8 @@ export interface ExtensionMessage {
| "rules"
| "fileContent"
| "rooHistoryImportProgress"
| "taskOrganizationUpdated"
| "taskOrganizationMutationResult"
text?: string
/** For fileContent: { path, content, error? } */
fileContent?: { path: string; content: string | null; error?: string }
Expand Down Expand Up @@ -248,6 +255,19 @@ export interface ExtensionMessage {
copyProgressItemName?: string
// folderSelected
path?: string

/**
* Full authoritative snapshot of the task organization aggregate.
* Sent on initial state hydration and after every committed mutation
* or cross-instance watcher reload.
*/
taskOrganization?: TaskOrganizationStateV1

/**
* Acknowledgement or typed rejection for a `taskOrganizationMutation`
* request. Correlated by `requestId`.
*/
taskOrganizationMutationResult?: TaskOrganizationMutationResultV1
}

export interface OpenAiCodexRateLimitsMessage {
Expand Down Expand Up @@ -418,6 +438,12 @@ export type ExtensionState = Pick<
* (captured during async getStateToPostToWebview) from overwriting newer messages.
*/
clineMessagesSeq?: number

/**
* Local task organization aggregate (manual folders and pins).
* Sent on initial state hydration and replaced on every update.
*/
taskOrganization?: TaskOrganizationStateV1
}

export interface Command {
Expand Down Expand Up @@ -631,6 +657,7 @@ export interface WebviewMessage {
| "deleteRule"
| "openRuleFile"
| "openRulesDirectory"
| "taskOrganizationMutation"
text?: string
taskId?: string
editedMessageContent?: string
Expand Down Expand Up @@ -741,6 +768,13 @@ export interface WebviewMessage {
worktreeForce?: boolean
worktreeNewWindow?: boolean
worktreeIncludeContent?: string

/**
* Task organization mutation request from webview to extension host.
* The host validates, applies the mutation atomically, and returns a
* `taskOrganizationMutationResult` correlated by `requestId`.
*/
taskOrganizationMutation?: TaskOrganizationMutationRequestV1
}

export interface RequestOpenAiCodexRateLimitsMessage {
Expand Down
Loading
Loading