Skip to content

2026.10.13 - Phase 2: Compression Tool Call and Result Rendering - #83

Merged
lanhhoang merged 4 commits into
masterfrom
20261013-dcp-ui-refactor-phase-02-compression-rendering
Oct 13, 2026
Merged

lanhhoang merged 4 commits into
masterfrom
20261013-dcp-ui-refactor-phase-02-compression-rendering

Conversation

@lanhhoang

Copy link
Copy Markdown
Contributor

Summary

Diff-sketch of the phase 2 deliverable on 20261013-dcp-ui-refactor-phase-02-compression-rendering (planning acb9de1, implementation 329235c; implementation range acb9de1..329235c):

 src/
+├── tui/theme.ts                                # new: DcpTheme = Pick<Theme, "fg"|"bg"|"bold"|"inverse">
+│                                                #   createDcpTheme(theme, env?)
+│                                                #     NO_COLOR presence (incl. "" and "0")
+│                                                #       → identity fg/bg/bold/inverse
+│                                                #     else: each method delegates at
+│                                                #       call time through private
+│                                                #       safeThemeCall; catch throw, fall
+│                                                #       back to input text on non-string
+└── tui/compress-render.ts                      # new: CompressionDisplayMetadata,
                                                 #   CompressionDisplayDetails,
                                                 #   createCompressionDisplayDetails(mode, result)
                                                 #     (copies blockIds; excludes summaries)
                                                 #   private isCompressionDisplayMetadata guard
                                                 #     (mode range|message, topic string,
                                                 #     three finite non-negative counts,
                                                 #     blockIds finite non-negative integers)
                                                 #   private extractOriginalText(result)
                                                 #     (joins text content with "\n")
                                                 #   renderCompressCall(mode, args, theme)
                                                 #     → TruncatedText line: compress ·
                                                 #       <mode> · [topic] · [N unit]
                                                 #   renderCompressResult(result, options,
                                                 #     theme, context)
                                                 #     partial  → "Compression in progress"
                                                 #     error    → toolOutput(error, text |
                                                 #                 "Compression failed")
                                                 #     metadata → TruncatedText(header) /
                                                 #                 Text(header + text + blocks)
                                                 #     legacy   → toolOutput(text |
                                                 #                 "Compression result unavailable")

 src/
 └── index.ts                                    # executeCompressTool → details: createCompressionDisplayDetails(mode, result)
                                                 #   (rejection/abort/thrown paths still
                                                 #   return details: {} + isError: true)
                                                 # registerCompressTool(message) → +renderCall
                                                 #   (closure over "message"), +renderResult
                                                 # registerCompressTool(range)  → +renderCall
                                                 #   (closure over "range"),  +renderResult
                                                 #   schemas, parameters, descriptions,
                                                 #   defaultActive, executionMode, prompt,
                                                 #   registration order, and the
                                                 #   `registerCompressTool(config.compress.mode)`
                                                 #   pre-session / post-trusted-config calls
                                                 #   are all unchanged

 tests/
+├── theme.test.ts                               # new: delegation across all four methods,
+│                                                #   live delegation (theme mutated between
+│                                                #   calls), NO_COLOR presence incl. "" / "0"
+│                                                #   and absence, one throwing method with
+│                                                #   working siblings, non-string return
+└── compress-render.test.ts                     # new: createCompressionDisplayDetails exact
                                                  #   field copy + blockIds copy + summary-free
                                                  #   JSON; renderCompressCall four call-table
                                                  #   strings + singular/plural, wrong-mode
                                                  #   array exclusion, Unicode topic flatten,
                                                  #   no submitted summaries, role assertions
                                                  #   (toolTitle+bold, accent, text, muted),
                                                  #   visibleWidth(line) <= width at 1/12/60;
                                                  #   renderCompressResult collapsed/expanded
                                                  #   exact strings + ordering, multiple text
                                                  #   items joined, empty blocks / empty topic,
                                                  #   0/1/5 messages, partial-before-metadata,
                                                  #   error-before-metadata, empty error text,
                                                  #   empty legacy text, absent/malformed
                                                  #   details (8 cases incl. bad mode,
                                                  #   non-string topic, negative / non-finite
                                                  #   / missing count, blockIds non-array,
                                                  #   blockIds non-integer, blockIds negative),
                                                  #   multiline Unicode content, NO_COLOR,
                                                  #   one throwing theme method, expanded
                                                  #   width bounding at 1/12/60

 tests/
 ├── index.test.ts                                # +compression rendering and success metadata
 │                                                #   describe: initial range renderCall /
 │                                                #   renderResult functions, range call
 │                                                #   closure visible, message-mode re-register
 │                                                #   renderCall / renderResult closures and
 │                                                #   partial visible, successful range +
 │                                                #   message attach exact dcpCompression
 │                                                #   (mode, topic, messagesCompressed,
 │                                                #   compressedTokens: any Number,
 │                                                #   summaryTokens: any Number, blockIds:[1])
 │                                                #   and never include the submitted summary
 │                                                #   text; capability denial, approval
 │                                                #   false / cancelled / aborted, and
 │                                                #   post-approval policy change (confirm
 │                                                #   flips ctx.model to a disabled one)
 │                                                #   all retain details: {} + isError: true
 └── sdk-contract.test.ts                         # +approved execution captures
                                                  #   event.result.details equal to
                                                  #   { dcpCompression: { mode, topic,
                                                  #   messagesCompressed: 1, compressedTokens:
                                                  #   any Number, summaryTokens: any Number,
                                                  #   blockIds: [1] } } and denied execution
                                                  #   captures {}; +exposes compression
                                                  #   renderers through the real SDK
                                                  #   getToolDefinition("compress") and
                                                  #   invokes renderCall with the installed
                                                  #   SDK types producing
                                                  #   "compress range · Auth · 1 range"

 docs/superpowers/plans/2026-10-12-dcp-ui-refactor-phase-02-compression-rendering.md
                                                  # all implementation checkboxes marked;
                                                  #   implementation record appended with
                                                  #   environment, baseline, verification,
                                                  #   ruling, real-Pi inspection note,
                                                  #   commit message
 README.md                                        # +Compression tool rendering
 CHANGELOG.md                                     # +[Unreleased] / Added

theme.ts and compress-render.ts are the only new source files. index.ts is a minimal additive change at the existing registration/execution boundary; both registered definitions close renderCompressCall over their own mode and share renderCompressResult. Existing builder signatures, command names and arguments, TypeBox schemas, the registered execute/tool-call/tool-execution-end callback contracts, native ui.confirm approval, the policy recheck after approval, the post-approval policy-rejection path, and the notification/timing/persistence/pipeline behavior are unchanged.

createDcpTheme(theme, env = process.env)
  Object.hasOwn(env, "NO_COLOR")      → { fg, bg, bold, inverse } = identity   # "" or "0" also disables
  else:
    fg(color, text)  = safeThemeCall(theme.fg(color, text), text)
    bg(color, text)  = safeThemeCall(theme.bg(color, text), text)
    bold(text)       = safeThemeCall(theme.bold(text),       text)
    inverse(text)    = safeThemeCall(theme.inverse(text),    text)

  safeThemeCall(call, fallback)
    try { result = call(); typeof result === "string" ? result : fallback }
    catch { fallback }

createCompressionDisplayDetails(mode, result)
  { dcpCompression: {
      mode,                       # from arg
      topic: result.topic,        # the model-visible label, never the submitted summary
      messagesCompressed,
      compressedTokens,
      summaryTokens,
      blockIds: [...result.blockIds]   # copy; later state mutation cannot change the result
  } }

isCompressionDisplayMetadata(value)    # private
  mode ∈ {"range", "message"}
  topic typeof string
  messagesCompressed / compressedTokens / summaryTokens: finite, ≥ 0
  blockIds: array of finite non-negative integers
  invalid → legacy path (original text or "Compression result unavailable")

extractOriginalText(result)            # private
  text content items joined with "\n"; non-text content ignored

renderCompressCall(mode, args, theme)
  topic    = flatten( string-or-"" )
  selected = mode === "range" ? args.content : args.targets
  TruncatedText(
    bold(fg("toolTitle", "compress"))
    + " " + fg("accent", mode)
    + (topic ? fg("muted", " · ") + fg("text", topic) : "")
    + (Array.isArray(selected)
        ? fg("muted", " · ") + fg("muted", `${selected.length} ${unit}`)
        : ""))                       # unit = range/ranges or message/messages

renderCompressResult(result, options, theme, context)
  options.isPartial               → TruncatedText(warning("Compression in progress"))
  context.isError                 → toolOutput(error, text | "Compression failed")
                                   Text(text, 0, 0) when expanded, TruncatedText otherwise
  isCompressionDisplayMetadata(meta):
    header = fg("toolTitle", modeLabel)            # "Range compression" / "Message compression"
           + (flatten(meta.topic) ? fg("muted", " · ") + fg("accent", topic) : "")
           + fg("muted", " · ") + fg("text", `${count} ${message|message|...}`)
           + fg("muted", " · ") + fg("text", `${formatDcpTokens(compressedTokens, true)} input`)
           + fg("muted", " · ") + fg("text", `${formatDcpTokens(summaryTokens, true)} summary`)
    options.expanded = false → TruncatedText(header)
    options.expanded = true  → Text(header + "\n"
                                 + (originalText ? fg("toolOutput", originalText) : "")
                                 + (blockIds.length > 0
                                     ? fg("muted", "Blocks: ")
                                       + blockIds.map(id => fg("accent", `b${id}`))
                                                 .join(fg("muted", ", "))
                                     : ""), 0, 0)
  else (legacy):
    toolOutput(text | "Compression result unavailable")
    Text(text, 0, 0) when expanded, TruncatedText otherwise

Evidence

$ git diff --stat acb9de1..329235c
 CHANGELOG.md                                       |   4 +
 README.md                                          |  14 +
 ...p-ui-refactor-phase-02-compression-rendering.md |  84 ++--
 src/index.ts                                       |  11 +-
 src/tui/compress-render.ts                         | 172 +++++++
 src/tui/theme.ts                                   |  35 ++
 tests/compress-render.test.ts                      | 523 +++++++++++++++++++++
 tests/index.test.ts                                | 273 ++++++++++-
 tests/sdk-contract.test.ts                         |  42 +-
 tests/theme.test.ts                                |  80 ++++
 10 files changed, 1200 insertions(+), 38 deletions(-)

pnpm check is green on the pushed result: format, warning-free lint, typecheck, 1070 tests / 65 suites (baseline 1005 / 63; +8 theme, +46 compress-render, +10 index rendering and metadata describe, +1 sdk-contract renderer exposure = +65). git diff --check exits 0.

Red→green sequence per task was observed before each implementation step. Selected red examples:

# Task 1 — theme adapter module missing
 FAIL  tests/theme.test.ts [ tests/theme.test.ts ]
Error: Cannot find module '../src/tui/theme.ts' imported from tests/theme.test.ts

# Task 2 — compression render module missing
 FAIL  tests/compress-render.test.ts [ tests/compress-render.test.ts ]
Error: Cannot find module '../src/tui/compress-render.ts' imported from tests/compress-render.test.ts

# Task 2 — Text component default padding bleeds into visible text
 FAIL  tests/compress-render.test.ts > renderCompressResult > renders the expanded success header, original text, then blocks
 AssertionError: expected [ '', ' Range compression · Auth system · 5 messages · ~12.4k input · ~2.1k summary',
                              ' Original summary text', ' Blocks: b1, b2', '' ]
              to deeply equal [ 'Range compression …', 'Original summary text', 'Blocks: b1, b2' ]
# (fixed: pass paddingX = 0, paddingY = 0 to every expanded-path new Text(...) to match
#  pi-coding-agent built-in renderer convention in dist/core/tools/renderers/*.js)

# Task 2 — ToolRenderContext not publicly exported by Pi 1.1.0
 typecheck: src/tui/compress-render.ts(4,3): error TS2305:
   Module '"@earendil-works/pi-coding-agent"' has no exported member 'ToolRenderContext'.
# (ruling: use an inline `{ isError: boolean }` parameter; structurally identical to
#  `Pick<ToolRenderContext, "isError">`. Ledger: cost if wrong = one-line type rename.)

# Task 3 — registered tool has no custom renderer and success returns details: {}
 FAIL  tests/index.test.ts > compression rendering and success metadata
        > registers both renderers on the initial range definition
 AssertionError: expected undefined to be type of 'function'
 FAIL  tests/sdk-contract.test.ts > DCP public SDK contract
        > exposes compression renderers through the real SDK tool definition
 AssertionError: expected undefined to be type of 'function'
 FAIL  tests/index.test.ts > compression rendering and success metadata
        > attaches summary-free display metadata on a successful range compression
 expected undefined to deeply equal {
   "blockIds": [1], "compressedTokens": Any<Number>, "messagesCompressed": 2,
   "mode": "range", "summaryTokens": Any<Number>, "topic": "Auth",
 }

tests/theme.test.ts pins the adapter contract: delegation across all four methods, live delegation after the underlying implementation changes (test mutates prefix between calls), NO_COLOR presence including "" and "0", absence of NO_COLOR, one throwing fg with bg / bold / inverse still operating, and a non-string bold return falling back to the input text.

tests/compress-render.test.ts exercises every row of the plan's Visible Rendering Contract. Calls: the four call-table strings (compress range · Auth system · 2 ranges, compress message · Auth · 1 message, compress range, compress message · 0 messages) plus singular/plural coverage and a wrong-mode-array exclusion (range mode ignores targets; message mode ignores content), Unicode topic flattening (認証 system), no submitted summaries (the visible text never contains SECRET), and semantic role assertions (fg called with toolTitle / accent / text / muted, bold invoked exactly once); visibleWidth(line) <= width is pinned at widths 1, 12, and 60. Results: the collapsed/expanded exact strings and ordering, multiple text content items joined in source order, the Blocks: b1, b2 expansion (omitted when blockIds is empty), the topic segment omitted when its flattened value is empty, 0/1/many message singular/plural rendering, partial-before-metadata precedence ("Compression in progress" with no metrics), error-before-metadata precedence (the original text in the error role, no metrics), empty error text → "Compression failed", empty legacy text → "Compression result unavailable", the eight malformed-details cases (bad mode, non-string topic, negative count, non-finite count, missing count, blockIds not an array, blockIds containing a non-integer, blockIds containing a negative), multiline Unicode content preserved, NO_COLOR producing zero ANSI escapes and zero fg calls on the theme spy, one throwing theme method leaving the visible text readable, and expanded-path line-width bounding at widths 1, 12, and 60. createCompressionDisplayDetails tests pin the exact field copy, a blockIds array copy that survives later mutation, and a JSON.stringify(details) that contains neither the submitted nor the combined summary text.

tests/index.test.ts adds the compression rendering and success metadata describe. The first two tests prove the registration boundary: the initial range definition exposes renderCall / renderResult; the registered renderCall produces compress range · Auth · 1 range; the trusted-project message reload re-registers both callbacks (with parameters.properties.targets instead of content), the message renderCall produces compress message · Auth · 1 message, and the registered renderResult honours options.isPartial with "Compression in progress". The next two tests run the full createExtension lifecycle: fire session_start, run a context pass to assign canonical m0001/m0002 message refs, then call compress.execute; a range compression returns details.dcpCompression equal to { mode: "range", topic: "Auth", messagesCompressed: 2, compressedTokens: any Number, summaryTokens: any Number, blockIds: [1] }; a message compression (config file { compress: { mode: "message" } }) returns the same shape with mode: "message", messagesCompressed: 1, and blockIds: [1]; the serialized details contains neither "private summary text" nor "private target summary". The remaining tests pin every rejection path to details: {} + isError: true: capability denial via a disabled-model config, approval false, approval cancelled (confirm throws), the effective signal aborted, and a post-approval policy change (the confirm callback flips ctx.model from the enabled test model to the disabled one, so the recheck after approval returns a model suppression reason); the content text in each case is the documented suppression or "Compression was not approved" message.

tests/sdk-contract.test.ts extends the approved/denied lifecycle test to capture event.result.details from tool_execution_end: the approved case has details equal to the documented dcpCompression shape with blockIds: [1] and the exact "Host lifecycle" topic, the denied case has details equal to {}. A new test asserts session.getToolDefinition("compress").renderCall and .renderResult are functions, invokes the registered renderCall with a real Theme-shaped object and the installed SDK parameter type, and asserts the visible text is "compress range · Auth · 1 range".

Domain invariants match the plan: snapshot serialization version 2 and reading of versions 1 and 2 are unchanged; no snapshot format, schema, dependency, configuration option, or release-version change ships. Compression result text, pruning/compression statistics calculations, the compress tool's TypeBox schemas and descriptions, the registered execute boundary (including the policy precheck, the ask permission approval flow, the post-approval policy recheck, and the post-approval policy-rejection path), the tool_call handler suppression, the tool_execution_end timing adapter, notification selection and content, persistence ordering, prompts, and the pipeline are all unchanged. Successful handleCompress is the only path that returns details: { dcpCompression: ... }; every existing rejection (capability denial, approval false/cancelled/aborted, post-approval policy change) still returns details: {} with isError: true, and an unexpected handleCompress throw still rejects. The dcpCompression payload contains only the five numeric/string fields and a copy of blockIds; the submitted summary text and the combined summary body never enter details. Display metadata falls through to the original text (or "Compression result unavailable") when absent or malformed, so persisted results from before this version, results with missing display metadata, and non-text content remain readable. NO_COLOR disables styling, one throwing theme method falls back to the input text without disabling its siblings, and a non-string return falls back to the input text. No new configuration, sidebar integration, search, block drilldown, domain action, package version bump, or future-phase panel/overlay/tab code is added; the pi-status repository is not edited and is not a dependency.

Merge Danger

Door: two-way

The change is additive and reverts cleanly: reverting the one implementation commit restores the prior registered definitions (no renderCall / renderResult, details: {} on success), removes the two new source files and the two new test files, and rolls back the README, CHANGELOG, and plan records. Every prior owner (capability gating, native ui.confirm approval, policy recheck, tool_call suppression, tool_execution_end timing, persistence codec, prompts, pipeline, notification selection) is untouched. The unchanged snapshot codec and persistence ordering mean a revert does not require migrating saved sessions. Persisted tool results carrying details: { dcpCompression: ... } from this version remain harmless structured tool-result details under Pi's default rendering after a revert.

Blast Radius: compression tool call and result presentation

Touches: src/tui/theme.ts (new), src/tui/compress-render.ts (new), src/index.ts (two renderCall / renderResult attachments and one details: createCompressionDisplayDetails(...) replacement in executeCompressTool), tests/theme.test.ts (new), tests/compress-render.test.ts (new), the new compression rendering and success metadata describe in tests/index.test.ts, the new exposes compression renderers through the real SDK tool definition test and the extended lifecycle detailsAtEnd capture in tests/sdk-contract.test.ts, README's new "Compression tool rendering" section, CHANGELOG's new [Unreleased] / Added entry, and the plan file's implementation record. No change to model compression execution, the compress tool registration schema, parameters, descriptions, default activation, or execution mode; the ask permission approval flow; the post-approval policy recheck; the suppression paths (capability denial, post-approval policy change, approval false / cancelled / aborted, tool_call blocking); the tool_execution_end timing adapter; pruning/compression statistics; notification selection, content, or severity; the snapshot codec or persistence ordering; prompts; the pipeline; the panel; the sidebar; or any release version. The only observable behavior change is the new compact, theme-aware compress call line, the new collapsed estimated-metrics line, the new expanded header-plus-text-plus-blocks presentation, the explicit "Compression in progress" partial line, and the error/legacy text fallback.

@lanhhoang
lanhhoang merged commit e393efe into master Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant