diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 77c8a98e2..4631b8975 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -8,10 +8,14 @@ import { Global } from "./global" import { Location } from "./location" import { Policy } from "./policy" import { AbsolutePath } from "./schema" +import { ConfigAttachments } from "./config/attachments" import { ConfigExperimental } from "./config/experimental" +import { ConfigFormatter } from "./config/formatter" +import { ConfigLSP } from "./config/lsp" import { ConfigPlugin } from "./config/plugin" import { ConfigProvider } from "./config/provider" import { ConfigReference } from "./config/reference" +import { ConfigToolOutput } from "./config/tool-output" import { ConfigWatcher } from "./config/watcher" export class Info extends Schema.Class("Config.Info")({ @@ -27,12 +31,35 @@ export class Info extends Schema.Class("Config.Info")({ autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]).pipe(Schema.optional).annotate({ description: "Automatically update or notify when a new version is available", }), + share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ + description: "Control whether sessions may be shared manually, automatically, or not at all", + }), + enterprise: Schema.Struct({ + url: Schema.String.pipe(Schema.optional), + }).pipe(Schema.optional).annotate({ + description: "Enterprise sharing service configuration", + }), + username: Schema.String.pipe(Schema.optional).annotate({ + description: "Username displayed in conversations and used for telemetry identity", + }), snapshots: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Enable snapshots used for undo and revert behavior", }), watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({ description: "Filesystem watcher configuration", }), + formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in formatters or configure formatter overrides", + }), + lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in language servers or configure server overrides", + }), + attachments: ConfigAttachments.Info.pipe(Schema.optional).annotate({ + description: "Attachment processing configuration", + }), + tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ + description: "Tool output truncation thresholds", + }), skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ description: "Additional paths or URLs to discover skills from", }), diff --git a/packages/core/src/config/attachments.ts b/packages/core/src/config/attachments.ts new file mode 100644 index 000000000..f14775ff3 --- /dev/null +++ b/packages/core/src/config/attachments.ts @@ -0,0 +1,15 @@ +export * as ConfigAttachments from "./attachments" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Image extends Schema.Class("ConfigV2.Attachments.Image")({ + auto_resize: Schema.Boolean.pipe(Schema.optional), + max_width: PositiveInt.pipe(Schema.optional), + max_height: PositiveInt.pipe(Schema.optional), + max_base64_bytes: PositiveInt.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Attachments")({ + image: Image.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/experimental.ts b/packages/core/src/config/experimental.ts index 377ed26ad..12a02635d 100644 --- a/packages/core/src/config/experimental.ts +++ b/packages/core/src/config/experimental.ts @@ -8,11 +8,11 @@ import { Policy as PolicyV2 } from "../policy" // this union makes it valid in authored config while keeping Policy generic. export const PolicyAction = Schema.Union([Catalog.PolicyActions]) -export class Policy extends Schema.Class("Config.Experimental.Policy")({ +export class Policy extends Schema.Class("ConfigV2.Experimental.Policy")({ ...PolicyV2.Info.fields, action: PolicyAction, }) {} -export class Experimental extends Schema.Class("Config.Experimental")({ +export class Experimental extends Schema.Class("ConfigV2.Experimental")({ policies: Policy.pipe(Schema.Array, Schema.optional), }) {} diff --git a/packages/core/src/config/formatter.ts b/packages/core/src/config/formatter.ts new file mode 100644 index 000000000..e1f90302d --- /dev/null +++ b/packages/core/src/config/formatter.ts @@ -0,0 +1,12 @@ +export * as ConfigFormatter from "./formatter" + +import { Schema } from "effect" + +export class Entry extends Schema.Class("ConfigV2.Formatter.Entry")({ + disabled: Schema.Boolean.pipe(Schema.optional), + command: Schema.String.pipe(Schema.Array, Schema.optional), + environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + extensions: Schema.String.pipe(Schema.Array, Schema.optional), +}) {} + +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) diff --git a/packages/core/src/config/lsp.ts b/packages/core/src/config/lsp.ts new file mode 100644 index 000000000..651597bef --- /dev/null +++ b/packages/core/src/config/lsp.ts @@ -0,0 +1,18 @@ +export * as ConfigLSP from "./lsp" + +import { Schema } from "effect" + +export const Disabled = Schema.Struct({ + disabled: Schema.Literal(true), +}) + +export class Server extends Schema.Class("ConfigV2.LSP.Server")({ + command: Schema.String.pipe(Schema.Array), + extensions: Schema.String.pipe(Schema.Array, Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + env: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + initialization: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export const Entry = Schema.Union([Disabled, Server]) +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) diff --git a/packages/core/src/config/plugin.ts b/packages/core/src/config/plugin.ts index 4268a7f79..e5fd6661f 100644 --- a/packages/core/src/config/plugin.ts +++ b/packages/core/src/config/plugin.ts @@ -2,7 +2,7 @@ export * as ConfigPlugin from "./plugin" import { Schema } from "effect" -export class Entry extends Schema.Class("Config.Plugin.Entry")({ +export class Entry extends Schema.Class("ConfigV2.Plugin.Entry")({ package: Schema.String, options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts index 55878bfab..dc9042e6f 100644 --- a/packages/core/src/config/reference.ts +++ b/packages/core/src/config/reference.ts @@ -2,12 +2,12 @@ export * as ConfigReference from "./reference" import { Schema } from "effect" -export class Git extends Schema.Class("Config.Reference.Git")({ +export class Git extends Schema.Class("ConfigV2.Reference.Git")({ repository: Schema.String, branch: Schema.String.pipe(Schema.optional), }) {} -export class Local extends Schema.Class("Config.Reference.Local")({ +export class Local extends Schema.Class("ConfigV2.Reference.Local")({ path: Schema.String, }) {} diff --git a/packages/core/src/config/tool-output.ts b/packages/core/src/config/tool-output.ts new file mode 100644 index 000000000..53e4d4d08 --- /dev/null +++ b/packages/core/src/config/tool-output.ts @@ -0,0 +1,9 @@ +export * as ConfigToolOutput from "./tool-output" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Info extends Schema.Class("ConfigV2.ToolOutput")({ + max_lines: PositiveInt.pipe(Schema.optional), + max_bytes: PositiveInt.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/watcher.ts b/packages/core/src/config/watcher.ts index be5c91a9b..2df6c876b 100644 --- a/packages/core/src/config/watcher.ts +++ b/packages/core/src/config/watcher.ts @@ -2,6 +2,6 @@ export * as ConfigWatcher from "./watcher" import { Schema } from "effect" -export class Info extends Schema.Class("Config.Watcher")({ +export class Info extends Schema.Class("ConfigV2.Watcher")({ ignore: Schema.String.pipe(Schema.Array, Schema.optional), }) {} diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index a6f6489e1..aac60acb1 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -168,8 +168,15 @@ describe("Config", () => { shell: "/bin/bash", model: "anthropic/claude", autoupdate: "notify", + share: "disabled", + enterprise: { url: "https://share.example.com" }, + username: "test-user", snapshots: false, watcher: { ignore: ["node_modules/**", "dist/**", ".git"] }, + formatter: { prettier: { disabled: true }, custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] } }, + lsp: { typescript: { disabled: true }, custom: { command: ["custom-lsp"], extensions: [".foo"] } }, + attachments: { image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 } }, + tool_output: { max_lines: 1000, max_bytes: 32768 }, skills: ["./skills", "~/shared-skills", "https://example.com/.well-known/skills/"], instructions: ["CONTRIBUTING.md", ".cursor/rules/*.md", "https://example.com/shared-rules.md"], references: { @@ -193,8 +200,23 @@ describe("Config", () => { expect(documents[0]?.info.shell).toBe("/bin/bash") expect(documents[0]?.info.model).toBe("anthropic/claude") expect(documents[0]?.info.autoupdate).toBe("notify") + expect(documents[0]?.info.share).toBe("disabled") + expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" }) + expect(documents[0]?.info.username).toBe("test-user") expect(documents[0]?.info.snapshots).toBe(false) expect(documents[0]?.info.watcher).toEqual({ ignore: ["node_modules/**", "dist/**", ".git"] }) + expect(documents[0]?.info.formatter).toEqual({ + prettier: { disabled: true }, + custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] }, + }) + expect(documents[0]?.info.lsp).toEqual({ + typescript: { disabled: true }, + custom: { command: ["custom-lsp"], extensions: [".foo"] }, + }) + expect(documents[0]?.info.attachments).toEqual({ + image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 }, + }) + expect(documents[0]?.info.tool_output).toEqual({ max_lines: 1000, max_bytes: 32768 }) expect(documents[0]?.info.skills).toEqual([ "./skills", "~/shared-skills", diff --git a/specs/v2/config.md b/specs/v2/config.md index 196c3fc4b..32a6f996b 100644 --- a/specs/v2/config.md +++ b/specs/v2/config.md @@ -110,10 +110,31 @@ Settings controlling local file observation, snapshots, language tooling, and to | ------------- | --------------------------------------- | ------- | ----- | | `watcher` | Ignore patterns for filesystem watching | keep | Keep `{ ignore?: string[] }`; this configures the filesystem watcher subsystem. | | `snapshot` | Enable filesystem snapshot tracking | redesign | Rename to plural `snapshots`; controls creation of snapshots used for undo and revert behavior. | -| `formatter` | Configure formatters | pending | | -| `lsp` | Configure language servers | pending | | -| `attachment` | Configure attachment/image processing | pending | | -| `tool_output` | Configure tool output truncation limits | pending | | +| `formatter` | Configure formatters | keep | Keep singular `boolean \| Record` shape; it configures built-in enablement and named formatter overrides. | +| `lsp` | Configure language servers | keep | Keep singular `boolean \| Record` shape; custom servers need commands and file extensions. | +| `attachment` | Configure attachment/image processing | redesign | Rename to plural `attachments`; retain `{ image?: { auto_resize?, max_width?, max_height?, max_base64_bytes? } }` for input normalization limits. | +| `tool_output` | Configure tool output truncation limits | keep | Keep `{ max_lines?, max_bytes? }`; both positive thresholds apply to saved-preview truncation behavior. | + +`formatter` and `lsp` configure one project tooling subsystem each, so their singular names remain appropriate. `true` enables the built-in registrations, `false` disables them, and a keyed object enables built-ins while applying named overrides or custom registrations. Custom language servers must declare `extensions` so runtime file attachment is deterministic; validation of known built-in server IDs belongs with the eventual v2 LSP integration rather than the aggregate core config schema. + +Rename legacy `attachment` to `attachments` in v2. This setting controls processing for the attachment domain and may expand beyond image handling, while singular `attachment` is already used as a model capability flag indicating whether one model accepts attachments. + +```jsonc +{ + "formatter": { + "prettier": { "disabled": true }, + "project": { "command": ["./scripts/format", "$FILE"], "extensions": [".foo"] }, + }, + "lsp": { + "typescript": { "disabled": true }, + "project": { "command": ["project-language-server", "--stdio"], "extensions": [".foo"] }, + }, + "attachments": { + "image": { "auto_resize": true, "max_width": 2000, "max_height": 2000 }, + }, + "tool_output": { "max_lines": 2000, "max_bytes": 51200 }, +} +``` ## Group 6: Sharing And Identity @@ -121,10 +142,22 @@ Settings affecting sharing behavior or user/account identity rather than model e | Field | Current Purpose | Status | Notes | | ------------ | ----------------------------------------------- | ------- | ------------------------------- | -| `share` | Session sharing behavior | pending | | -| `autoshare` | Legacy automatic sharing flag | pending | Deprecated in favor of `share`. | -| `enterprise` | Enterprise URL configuration | pending | | -| `username` | Display username in conversations and telemetry | pending | | +| `share` | Session sharing behavior | keep | Keep `"manual" \| "auto" \| "disabled"`; it controls manual sharing permission and automatic sharing of new sessions. | +| `autoshare` | Legacy automatic sharing flag | remove | Do not port deprecated alias; use `share: "auto"`. | +| `enterprise` | Enterprise URL configuration | keep | Keep `{ url?: string }`; currently selects the legacy sharing service endpoint when no organization account is active. | +| `username` | Display username in conversations and telemetry | keep | Keep string identity override; runtime may otherwise resolve an operating-system username. | + +Retain `share` as the single session-sharing setting. `"manual"` permits explicit sharing, `"auto"` shares newly created top-level sessions, and `"disabled"` prevents sharing. Legacy `autoshare: true` is only an alias for `share: "auto"`, so v2 does not expose it. + +Retain `enterprise.url` for legacy enterprise share hosting selection and `username` as a user-facing identity override. These remain separate from server authentication credentials; `username` identifies the user in conversation and telemetry behavior rather than HTTP basic-auth configuration. + +```jsonc +{ + "share": "disabled", + "enterprise": { "url": "https://share.example.com" }, + "username": "developer", +} +``` ## Group 7: Providers And Model Selection