Skip to content

Decide: integrate with Excel native Style API or keep our own style system #19

@tmustier

Description

@tmustier

Decide: Native Excel Styles vs Our Own Style System

Summary

We're building a composable cell style system (see .research/conventions-design.md). Excel has a native Style API (ExcelApi 1.7) that overlaps significantly. We need to decide whether to build on top of it, alongside it, or hybrid.

For Phase 1 we're proceeding with Option B (our own system) — but this issue tracks the decision on whether to integrate with native Excel styles longer-term.

Excel's Native Style API — What It Can Do

Reading & applying styles

// Apply a style to a range — single property, single API call
range.style = "Currency";

// Read what style a range has
range.load("style");
// Returns: "Currency", "Total", or custom style name

Built-in styles (BuiltInStyle enum)

Relevant ones that overlap with our planned styles:

Excel Built-In Our Equivalent Notes
Currency currency Default format: $#,##0.00 (US locale)
Percent percent Default format: 0%
Comma number Default format: #,##0.00
WholeComma integer Default format: #,##0
Total total-row Bold + top/bottom border
Input input Yellow fill + blue border
Heading14 header Theme-colored headings
Output Light grey fill
Calculation Orange fill

Also: Good/Bad/Neutral, Accent16 (with 20/40/60% variants), SheetTitle, Emphasis13, ExplanatoryText, Note, CheckCell, LinkedCell.

Modifying built-in styles

Built-in styles are fully writable via the API. builtIn is a readonly flag that tells you whether a style is built-in, but it doesn't prevent modification:

const currencyStyle = workbook.styles.getItem("Currency");
currencyStyle.numberFormat = '$* #,##0.00_);$*(#,##0.00);$* --_)';
currencyStyle.font.bold = false;
currencyStyle.includeFont = false;   // don't touch font properties when applied
currencyStyle.includeBorder = false; // don't touch borders when applied
await context.sync();

Writable properties on Excel.Style:

  • numberFormat, numberFormatLocal
  • horizontalAlignment, verticalAlignment, wrapText
  • autoIndent, indentLevel, shrinkToFit, textOrientation
  • formulaHidden, locked
  • includeAlignment, includeBorder, includeFont, includeNumber, includePatterns, includeProtection
  • Navigation properties (writable via child objects): font.* (bold, italic, color, size, name), fill.* (color), borders.getItem(edge).* (style, weight, color)

Readonly: builtIn, name (can't rename).

Creating custom styles

workbook.styles.add("pi-ratio");
const ratioStyle = workbook.styles.getItem("pi-ratio");
ratioStyle.numberFormat = '#,##0.0x_);(#,##0.0x);--_x_)';
ratioStyle.includeFont = false;
ratioStyle.includeBorder = false;
await context.sync();

Composability via include* flags

Each style has boolean flags controlling which property groups it applies:

  • includeNumber — number format
  • includeFont — font (bold, italic, color, size, etc.)
  • includeBorder — borders
  • includePatterns — fill/patterns
  • includeAlignment — alignment + wrap
  • includeProtection — locked/hidden

This enables partial application: a "Currency" style with includeFont: false sets the number format but doesn't touch the font. A "Total" style with includeNumber: false sets bold + border but doesn't touch the number format.

Sequential application works:

range.style = "Currency";  // sets number format only (includeFont=false)
range.style = "Total";     // sets bold + border only (includeNumber=false)
// Result: currency number format + bold + border

Limitation: range.style is a single string — after the above, it reports "Total" (last applied). The metadata that it's also "Currency" is lost. The properties are preserved, but the style association isn't composable.

Style scope

  • Styles are per-workbook (not per-user).
  • Built-in styles reset when you create a new workbook.
  • Custom styles only exist in the workbook where they were created.

Three Options

Option A: Build on native styles

On session start, configure Excel's built-in styles to match our conventions. Create custom styles for ones without built-in equivalents (ratio, blank-section). Apply via range.style = "StyleName".

Pros:

  • Cells show named styles in Excel's Cell Styles gallery (Home ribbon)
  • User can inspect/tweak styles via native Excel UI (right-click > Modify)
  • Single API call to apply (range.style = "Name")
  • Native feel — looks like the user created the styles themselves

Cons:

  • Modifying built-in styles is aggressive — changes the user's existing workbook setup
  • Per-workbook: need to sync styles on every session start, for every workbook
  • Composition: sequential include* approach works but is fragile; style name metadata is lost
  • No parameterisation: can't have "Currency with 0dp" as a single style without creating separate named styles for each variant

Option B: Our own system (current Phase 1 approach)

Conventions module as source of truth. Style resolution produces flat properties. Apply via direct property manipulation (range.format.font.bold = true etc.).

Pros:

  • Full control over composition logic (array of style names → merged properties)
  • No side-effects on the user's workbook styles
  • Per-user persistence via SettingsStore (works across workbooks)
  • Clean parameterisation (currency + dp: 0 → resolved at call time)

Cons:

  • Cells don't show a style name in Excel's Cell Styles gallery
  • Can't leverage include* mask semantics
  • More API calls per formatting operation (set each property individually vs one style assignment)

Option C: Hybrid — our system + sync to native styles

Our conventions module is the source of truth. On session start, sync our styles to the workbook as custom styles (namespaced, e.g. "pi.currency", "pi.total-row"). Apply via range.style where possible; fall back to direct properties for composed/parameterised cases.

Pros:

  • Best of both: clean composition logic + styles visible in Excel UI
  • Don't touch built-in styles (use our own pi.* namespace)
  • User can see what the agent applied (Cell Styles gallery)
  • range.style read-back tells us what was previously applied

Cons:

  • More implementation complexity (sync lifecycle, conflict handling)
  • Need to handle: what if user deletes a pi.* style? What if they modify it?
  • Custom style namespace could feel foreign to users expecting native style names
  • Still per-workbook for the synced styles (our SettingsStore conventions remain per-user)

Recommendation

Option C (hybrid) is likely the best long-term answer, but the conventions module architecture is identical for B and C — the sync layer is additive. So starting with B and adding the sync layer later has no architectural cost.

Questions to answer before choosing

  1. Do users care about seeing style names in Excel's Cell Styles gallery?
  2. Is the per-workbook sync lifecycle acceptable? (Every new workbook needs style provisioning.)
  3. How should we handle conflicts between our styles and user-modified native styles?
  4. Is the pi.* namespace acceptable, or should we hijack built-in names?
  5. Should read_range report both our style label AND Excel's native style name?

Key files

  • .research/conventions-design.md — full design doc for the conventions system
  • src/tools/format-cells.ts — current formatting tool
  • src/prompt/system-prompt.ts — current conventions in system prompt
  • Office.js types: node_modules/@types/office-js/index.d.ts — search for class Style extends, enum BuiltInStyle, StyleCollection

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions