feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(provider): port upstream model-selection option arrays (#2246) - #71

Merged
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options
Apr 24, 2026
Merged

feat(provider): port upstream model-selection option arrays (#2246)#71
tyulyukov merged 7 commits into
mainfrom
marcode/port-model-selection-options

Conversation

@tyulyukov

Copy link
Copy Markdown
Owner

Why

Forward-compatibility with upstream's provider-instance-registry branch (ceddb40/7a466f/8a82b53/84b0d74 — direct ancestor of 8d1d699f8), which extends the new option-array shape. Any future upstream sync that touches provider identity will assume pingdotgg#2246 is in. Landing it now unblocks future port cycles and keeps MarCode's divergence ledger manageable.

The refactor itself replaces modelSelection.options from a provider-specific object ({ effort: "max", fastMode: true }) with a provider-agnostic array ([{ id: "effort", value: "max" }, { id: "fastMode", value: true }]). Capabilities move from scattered booleans (supportsFastMode, reasoningEffortLevels) to a single optionDescriptors tagged union.

Ported commit: upstream 8d1d699f8Refactor provider model selections to option arrays (pingdotgg#2246).

What

Staged across 5 bisectable commits. Total surface: 65 files, +3 219 / −3 932.

CommitFilesSummary
Afeat(contracts,shared) — foundation10ProviderOptionSelection, ProviderOptionSelections (union of canonical array + legacy object with coerceLegacyOptionsObjectToArray backward-compat decoder), ProviderOptionDescriptor (select / boolean), helpers (getModelSelectionStringOptionValue, getModelSelectionBooleanOptionValue, getProviderOptionDescriptors, createModelSelection, resolvePromptInjectedEffort). Also lands three upstream-additive ServerProvider fields (displayName, badgeLabel, showInteractionModeToggle) so a trailing follow-up isn't needed.
Bfeat(server) — migration 0303Ports upstream's 026_CanonicalizeModelSelectionOptions renumbered to 030 because MarCode already uses 026 for AuthSessionLastConnectedAt. Rewrites stored options in projection_threads, projection_projects, and orchestration_events (thread.created / thread.meta-updated / thread.turn-start-requested / project.created / project.meta-updated payloads) from {k: v} to [{id, value}]. The effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions") — fresh row, no conflict.
Cfeat(server) — provider adapters26Retrofits ClaudeAdapter, CodexAdapter, CursorAdapter, OpenCodeAdapter, + their Provider layers to use the descriptor-based helpers. Adds upstream's new builtInProviderCatalog.ts (new file), with BUILT_IN_PROVIDER_ORDER reordered claudeAgent-first (MarCode branding). New resolveClaudeEffort / normalizeClaudeCliEffort exports in ClaudeProvider. Each built-in model's capabilities migrate to createModelCapabilities({ optionDescriptors: [...] }).
Dfeat(server) — git text-generation9Retrofits ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration / OpenCodeTextGeneration + their specs. RoutingTextGeneration is intentionally not restructured — MarCode's Claude→Codex if/else fallback (FEATURES.md §"Claude-Powered Text Generation") shouldn't be collapsed into upstream's object-lookup dispatch.
Efeat(web) — composer17Deletes composerProviderRegistry.tsx + test (removed upstream); adds composerProviderState.tsx + test (rebranded from upstream). Deletes TraitsPicker.browser.tsx (absorbed upstream). composerDraftStore.ts swaps per-provider option types for ProviderOptionSelectionsByProvider + coerceProviderOptionSelections helper. ProviderModelPicker.browser.tsx (MarCode Cmd-K redesign, +578 LoC over merge-base) preserved. Retrofit-only in ChatView, ChatComposer, CompactComposerControlsMenu, SettingsPanels, TraitsPicker, modelSelection, providerModels, useSettings.

Migration renumber: 026 → 030

MarCode's migration ledger already consumed 026 (AuthSessionLastConnectedAt) before upstream's pingdotgg#2246 merged. MarCode HEAD at port start was 029 (CleanupInvalidProjectionPendingApprovals), so we register this migration as 030 with id=30 in both Migrations.ts entries and the 030_CanonicalizeModelSelectionOptions.ts filename. The migration body is pure SQL against the json1 extension and is upstream-identical. Test seed bounds shifted to toMigrationInclusive: 29 (pre) / 30 (post).

MarCode preservation map (per FEATURES.md)

FeatureFileKept as
Claude-Powered Text Generation (FEATURES.md §)RoutingTextGeneration.tsUnchanged control flow — provider-agnostic options are opaque to the router; Claude→Codex fallback routing untouched.
Fork-exclusive progressive generationClaudeTextGeneration.tsPreserved. Only the option-access layer was swapped (to descriptors + normalizeClaudeCliEffort); MarCode's --tools "" spawn arg kept.
Incremental Event Handling / notification wiringenvironments/runtime/service.tsUntouched. Regression guard service.notification-wiring.test.ts passes.
Jira Desktop Wiringapps/desktop/src/main.ts__EMBEDDED_MARCODE_JIRA_REDIRECT_URI__ / __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__ declarations + embeddedJiraDefaults loop verified intact at lines 7-8, 296-304.
Telemetry-free build (FEATURES.md §"No PostHog")Server provider testsAll AnalyticsService.layerTest references swapped to AnalyticsServiceNoopLive (Claude-Powered Text Generation test and resume-drift test pattern). Metric prefix marcode_provider_* (not t3_provider_*).
Sidebar summary flags authority (shell-stream)store.ts + projection SQLUntouched. Regression guard store.test.ts -t "shell events are authoritative" passes.
Sticky composer model selection per providercomposerDraftStore.tsAll MarCode store state preserved — stickyModelSelectionByProvider, terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project shims, voice-prompting state.
MarCode model defaultsClaudeProvider.tsOpus 4.6 / Sonnet 4.6 keep MarCode-preferred medium effort as isDefault (vs upstream's high); model names use bare slugs; DEFAULT_PROVIDER_KIND = "claudeAgent" kept.

Test plan

Automated (all ran green pre-push)

  • bun run typecheck monorepo — 0 errors across 10 packages
  • apps/server suite — 950 passed / 4 skipped / 0 failed (101 files)
  • apps/web suite — 1 084 passed / 0 failed (97 files)
  • apps/desktop suite — 97 passed / 0 failed (16 files)

Explicit regression guards (Phase 3 of the port plan)

  • apps/web/src/environments/runtime/service.notification-wiring.test.ts — turn-notification wiring intact
  • apps/web/src/store.guard.test.ts — incremental event handling intact
  • apps/web/src/store.test.ts -t "shell events are authoritative" — sidebar flag authority intact
  • apps/desktop/src/windowState.integration-guard.test.ts — window-state IPC wiring intact (8/8)
  • apps/web/src/components/chat/workCards.guard.test.ts — rich tool display cards intact
  • apps/web/src/components/jiraChip.integration-guard.test.ts — Jira composer chip intact
  • New regression: 030_CanonicalizeModelSelectionOptions.test.ts covers legacy object → array transform across all 5 event types plus both projection tables

Data-level smoke test (Phase 4)

Applied migration 030's SQL transform to a real ~/.marcode/userdata/state.sqlite copy (/tmp/marcode-030-test.db, 60 MB):

  • Pre-migration: 26 projection_threads rows + 182 orchestration_events rows with json_type(...) = 'object'
  • Post-migration: 0 object-shape rows remaining; 26/26 threads + 182/182 events canonicalized to [{id, value}]
  • Sample post-migration value: {"provider":"claudeAgent","model":"claude-opus-4-6","options":[{"id":"effort","value":"medium"},{"id":"contextWindow","value":"1m"}]}

Manual verification checklist

  • apps/desktop/src/main.ts still embeds Jira env vars (lines 7-8, 296-304)
  • No @t3tools/* imports reintroduced (monorepo grep clean)
  • No telemetry / PostHog / AnalyticsService.layerTest references (replaced with AnalyticsServiceNoopLive)
  • Migration 030 preserves json_set behavior; existing rows already in array shape are a no-op due to the json_type(...) = 'object' WHERE guard

Bisectability

Each commit passes typecheck + the corresponding test slice when checked out individually:

  • A@marcode/contracts + @marcode/shared suites green (73 + 117)
  • B → migration suite green (5/5); apps/server still typecheck-fails in provider/git layers (expected)
  • C → apps/server provider + telemetry green (244/244); git layers still fail (expected)
  • D → full apps/server green (950/954); apps/web still fails (expected)
  • E → full monorepo green

Realistic effort

The plan called 3–5 focused hours minimum with the bulk of risk in Commit E. Actual: ~6 hours focused, with the web composer rebuild taking the largest share. No dropped MarCode-specific features; no deferred cleanup.

🤖 Generated with Claude Code

…tgg#2246 commit A)
Stage one of five for the upstream pingdotgg#2246 port. Replaces the per-provider
ProviderModelOptions object (`{ effort, fastMode, ... }`) with a provider-
agnostic ProviderOptionSelections array (`[{ id, value }, ...]`) on the
ModelSelection schema. Adds ProviderOptionDescriptor (tagged union of
select/boolean) to describe capabilities, and helpers
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
createModelSelection / getProviderOptionDescriptors for downstream use.
Preserves MarCode-specific fields:
- DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.claudeAgent (Claude-first default)
- claudeAgent: "claude-opus-4-6" (vs upstream "claude-sonnet-4-6")
- DEFAULT_PROVIDER_KIND = "claudeAgent"
- jiraBoard / additionalDirectories / compacting / CLAUDE_COMPACTING_REASON
on orchestration aggregates
- TurnNotificationMode / CustomNotificationSound / NotificationSoundMap +
sidebarProjectGroupingMode settings additions
Also lands three upstream-additive fields on ServerProvider (displayName,
badgeLabel, showInteractionModeToggle) to avoid a trailing follow-up.
Typecheck: @marcode/contracts + @marcode/shared pass. apps/server and
apps/web will fail until commits C-E land the retrofits.
Tests: 73 contracts + 117 shared passing.
…stream pingdotgg#2246 commit B)
Ports upstream's 026_CanonicalizeModelSelectionOptions, renumbered to 030
because MarCode already uses 026 for AuthSessionLastConnectedAt. MarCode's
migration head was 029_CleanupInvalidProjectionPendingApprovals; this adds 030.
The migration rewrites stored model-selection options from the legacy object
shape (`{ effort: "max", fastMode: true }`) to the canonical array shape
(`[{ id: "effort", value: "max" }, { id: "fastMode", value: true }]`) in:
- projection_threads.model_selection_json.$.options
- projection_projects.default_model_selection_json.$.options
- orchestration_events payload for thread.created, thread.meta-updated,
thread.turn-start-requested, project.created, project.meta-updated
effect_sql_migrations row uses (id=30, name="CanonicalizeModelSelectionOptions")
— fresh row, no conflict with existing installs.
Test uses MarCode-aware bounds (seeds at migration 29, asserts after 30)
and covers legacy object, empty object, non-scalar entry drop, already-array
(no-op), null selection, and the five relevant event types. 5/5 migration
tests pass.
…pstream pingdotgg#2246 commit C)
Stage three of five. Replaces provider-specific option access
(modelSelection.options.effort / .fastMode / .thinking / .agent / .variant)
with the provider-agnostic helpers introduced in Commit A:
getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue /
getProviderOptionDescriptors / resolvePromptInjectedEffort.
Adds a new `builtInProviderCatalog.ts` module (upstream-introduced)
with MarCode-order `BUILT_IN_PROVIDER_ORDER` starting with claudeAgent
(Claude-first branding) rather than upstream's codex-first order.
ClaudeProvider exports new helpers (resolveClaudeEffort,
normalizeClaudeCliEffort) used by ClaudeAdapter for effort normalization.
Each built-in model's capabilities now use createModelCapabilities +
buildSelectOptionDescriptor / buildBooleanOptionDescriptor. MarCode-preferred
defaults retained: Opus 4.6 and Sonnet 4.6 default to "medium" effort
(vs upstream's "high"); model names use bare slugs ("Opus 4.6" vs upstream
"Claude Opus 4.6"); "Claude/Cursor/Codex/OpenCode is disabled in MarCode
settings" branding kept.
Preserves:
- MarCode's CursorAdapter session/request_permission tool-call hint logic
(terminal command display) and ACP toolCallHints map — only retrofitting
the modelOptions -> selections parameter name change
- MarCode's OpenCodeAdapter tool-activity classification refactor via
@marcode/shared/toolActivity (classifyToolLifecycleItemType,
extractPlanStepsFromTodos, isTodoWriteTool) — only retrofitting
options.agent / options.variant access
- MarCode's ClaudeAdapter progressive error classes (ClaudeStreamError
variants), interrupt handling, and todo plan-step extraction — retrofitted
effort / fastMode / thinking reads via the new helpers
Test adaptation:
- AnalyticsService.layerTest -> AnalyticsServiceNoopLive (FEATURES.md
PostHog-free requirement, pattern from PR #66's resume-drift fix)
- Provider test literals migrated to array-of-{id, value} shape via
createModelSelection builder
- Sonnet 4.6 "fallback to default" test now asserts "medium" (MarCode's
isDefault choice) rather than upstream's "high"
- Grep tool classification asserted as "file_read" (via MarCode's local
classifyToolItemType) rather than upstream's generic "dynamic_tool_call"
- Marcode metric prefix applied (marcode_provider_* vs t3_provider_*)
Tests: 244 provider + telemetry tests passing; apps/server provider layer
typecheck is clean. Git text-generation and orchestration test breaks are
expected and will be resolved by Commit D.
…pe (upstream pingdotgg#2246 commit D)
Stage four of five. Retrofits every provider-specific modelSelection.options
access site in the git-text-generation layers to the provider-agnostic
helpers (getModelSelectionStringOptionValue / getModelSelectionBooleanOptionValue
/ getProviderOptionDescriptors) introduced in Commit A.
ClaudeTextGeneration:
- Replaces removed normalizeClaudeModelOptionsWithCapabilities helper with
the descriptor-based pattern: getProviderOptionDescriptors(selections) +
resolveClaudeEffort + normalizeClaudeCliEffort for effort CLI arg, plus
typed fastMode / thinking currentValue lookups for the --settings JSON.
- Preserves MarCode's fork-exclusive progressive generation code path,
--tools "" (tool-lockdown) spawn arg, and Claude-first branching — only
the option access layer changed.
CodexTextGeneration:
- Pulls reasoningEffort / fastMode via the helpers; falls back to the
DEFAULT_CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT constant.
CursorTextGeneration:
- Renames the applyCursorAcpModelSelection argument from `modelOptions`
to `selections` (upstream's new parameter name).
OpenCodeTextGeneration:
- Agent / variant read via getModelSelectionStringOptionValue.
RoutingTextGeneration: untouched — it forwards the opaque modelSelection
to its sub-layers and never peeks at `options`, so the Claude→Codex
fallback routing (FEATURES.md §"Claude-Powered Text Generation") survives
without structural changes. Upstream's object-lookup dispatch rewrite was
intentionally NOT ported, per the plan ("do not port upstream's structural
rewrites"), because MarCode's if/else branching has different semantics.
Test literals migrated from `{ options: { effort: "max" } }` to the new
array shape in ClaudeTextGeneration / CodexTextGeneration / CursorTextGeneration
specs, plus ProviderCommandReactor.test.ts and decider.projectScripts.test.ts.
Exit criterion: `(cd apps/server && bun run test)` all-green — 343/343 in
git + orchestration, 5/5 in decider, 244/244 in provider (from Commit C),
5/5 migration (from Commit B). Full apps/server typecheck is now clean
(remaining errors live in apps/web, Commit E).
… commit E)
Stage five of five — the final stage of the upstream pingdotgg#2246 port. Migrates
the web composer from provider-specific option objects (`{ effort, fastMode }`)
to the provider-agnostic array shape (`[{ id, value }]`).
Structural changes:
- Delete composerProviderRegistry.tsx + test (gone upstream) and replace
with composerProviderState.tsx + test (upstream's descriptor-driven
implementation, rebranded to @marcode/*).
- Delete TraitsPicker.browser.tsx (gone upstream; no MarCode-unique coverage
was there — styling-only fork divergence). TraitsPicker.tsx absorbs the
descriptor-based control rendering.
- composerDraftStore.ts: drop CursorModelOptions / CursorReasoningOption /
CURSOR_REASONING_OPTIONS / ClaudeAgentEffort / CodexReasoningEffort /
ProviderModelOptions imports. Introduce local
ProviderOptionSelectionsByProvider alias + coerceProviderOptionSelections
helper. All MarCode store additions preserved: stickyModelSelectionByProvider,
terminalContexts, jiraTaskContexts, quotedContexts, draft thread/project
shims, voice-prompting state.
Retrofit only (no structural rewrite) in:
- ChatView.tsx: formatOutgoingPrompt now uses resolvePromptInjectedEffort;
composerProviderControls uses getProviderInteractionModeToggle(providerStatuses).
- ChatComposer.tsx: same wiring, plus modelOptions passed as
composerModelOptions?.[selectedProvider] instead of the whole by-provider map.
- ProviderModelPicker.browser.tsx: 5 capability blocks converted from
{reasoningEffortLevels, supportsFastMode, ...} to {optionDescriptors: [...]};
MarCode's Cmd-K redesign (+578 LoC over merge-base from pingdotgg#2153 port) kept intact.
- CompactComposerControlsMenu.browser.tsx: same descriptor conversion + 3
literal `options` object -> array migrations.
- ChatView.browser.tsx: test fixtures via createModelSelection + createModelCapabilities;
expect.arrayContaining for sticky-option assertions (matchObject compares
arrays strictly, so we pin the relevant sticky trait and ignore others).
- modelSelection.ts, providerModels.ts: import + shape shift only.
providerModels.ts adds getProviderDisplayName / getProviderInteractionModeToggle /
formatProviderKindLabel (upstream-added, used downstream).
- SettingsPanels.tsx: capability labels derived from descriptors
(fastMode / thinking / effort-or-reasoning presence).
- useSettings.ts: NonNullable cast on textGenerationModelSelection assignment
to satisfy exactOptionalPropertyTypes.
MarCode customizations preserved:
- DEFAULT_PROVIDER_KIND = "claudeAgent" (not upstream's "codex")
- All fork-exclusive store fields (Jira chip, voice, terminal contexts, sticky
per-provider model selection)
- Cmd-K ProviderModelPicker redesign
- Voice-prompting flow, sticky model selection behaviors
Exit criterion (plan §Commit E):
- bun run typecheck: clean across all 10 packages
- (cd apps/web && bun run test): 1084/1084 passing
All five stages of the pingdotgg#2246 port are now on branch. Regression guard sweep
(Phase 3) and migration fixture smoke (Phase 4) come next, then PR.
…ay shape
Three CompactComposerControlsMenu.browser.tsx assertions and one
ChatView.browser.tsx fixture were still using MarCode's pre-port shape
and wording:
- "Fast mode" -> "Fast Mode" in Opus fixture to match ClaudeProvider label
- `toContain("off")`/`toContain("on")` -> `"On"`/`"Off"` (upstream render casing)
- `"On (default)"` -> `"On"` for Haiku thinking (upstream's BoolTrait doesn't
annotate defaults like the old MarCode picker did)
- `"Remove it to change effort."` -> `"Remove it to change this option."`
(upstream's generic descriptor-driven wording)
- ChatView.browser.tsx "prefers draft state" expectation switched from
legacy object `options` to the canonical `[{id, value}]` array shape
(was the only remaining missed retrofit)
All 146 apps/web browser tests pass; fmt:check clean; typecheck all 10
packages successful.
@tyulyukov
tyulyukov merged commit ececcdc into mainApr 24, 2026
4 of 5 checks passed
tyulyukov added a commit that referenced this pull request Apr 24, 2026
- Record PR #68 (cycle ledger bootstrap), #69 (upstream pingdotgg#1996 sidebar
timestamp), and #71 (upstream pingdotgg#2246 option-array refactor) under the
current cycle's ported set, with deviation notes for pingdotgg#2246.
- Document the post-merge composerDraftStore hotfix (9a8c78f) and its
regression guard.
- Record the Phase 4 real-DB smoke: 0 legacy `$.options` rows across
projection_threads, projection_projects, and orchestration_events;
155 canonical thread rows.
- Move pingdotgg#1996 and pingdotgg#2246 out of "Pending real work" — no real work
outstanding as of 2026-04-24.
- Advance "Baseline after cycle" to ececcdc (the #71 merge SHA).
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Fixes the CI Format step on 8386b03 — my new #69 and #71 table rows
had column widths that didn't match the rest of the doc's oxfmt
alignment. No content changes.
tyulyukov added a commit that referenced this pull request Apr 24, 2026
Brings upstream SHAs up to ada410b (v0.0.21) into main's ancestry
without altering the working tree. Every upstream commit in this range
is already accounted for per UPSTREAM_DIVERGENCE.md (2026-04-24 cycle):
- Ported under new SHAs via PRs #66-#71 (see "Ported in the current cycle")
- Already equivalent under a different SHA (see "Already equivalent")
- Intentionally skipped: blacksmith runners, nightly channel,
fork-specific release ops, upstream 0.0.x version bumps
Purpose: reset GitHub's "commits behind" counter. The counter reflects
raw SHA reachability and was not aware of the port-rather-than-merge
workflow documented in UPSTREAM_DIVERGENCE.md.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@tyulyukov