Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ If a task seems to require breaking one of these, **stop and surface it** — do

## Boundaries — what you must NOT do

- **Do not tick `tasks.md` boxes.** The orchestrator flips `[ ]→[x]` after the gates pass. Instead, report which `N.M` tasks you completed.
- **Do not tick `tasks.md` boxes.** The orchestrator flips `[ ]→[x]` after the gates pass. Instead, report which `N.M` tasks you completed. Never rewrite `tasks.md` wholesale — it holds all future sections.
- **Do not commit, push, open PRs, or amend.** The orchestrator commits per section.
- **Do not self-approve.** When the section builds and tests pass, report it complete and request the `reviewer`.
- Do not suppress warnings, disable analyzers, or weaken tests to go green.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- **`IInputPreamble` / `ITerminal.InputPreamble`** — a persistent multi-row band rendered immediately above the input editor. Set via `terminal.InputPreamble.SetRows(...)` using the same `Line`/`LineBuilder` API as status. Persists across input submissions; clear with `SetRows()` (no args). Truncates under a constrained `MaxFixedHeight` before the input editor loses rows; status rows remain sacred. No overlay/intercept — purely presentational.

### Security

- **VT-escape injection gap closed.** Consumer text passed to `Segment`, `Line.FromText`, or
Expand Down
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ The unit of work is a **`## N.` section**. Walk sections in order from the resum
go hunting — give it what it needs to stay focused.
2. **Worker implements the whole section.** If a section is large or complex (e.g. the VT input parser),
split it into sub-chunks across multiple `worker` calls — but it remains **one commit at section end**.
3. **Audit.** Spawn `reviewer` on the section diff (correctness, ADR compliance, OpenSpec scope, C#
idiom, agentic-AI design quality).
3. **Audit.** Spawn `reviewer` on the section diff (correctness, binding-design-decision compliance,
OpenSpec scope, C# idiom, terminal-rendering safety).
4. **Review loop.** Feed the reviewer's findings back to the `worker`; worker fixes; `reviewer`
re-audits. **Repeat until the reviewer signs off.**
5. **Gates — all four must pass before ticking any box:**
Expand All @@ -80,7 +80,8 @@ The unit of work is a **`## N.` section**. Walk sections in order from the resum
- `openspec validate <change-name> --strict`
- `dotnet format --verify-no-changes` clean
If a gate fails, it's back to step 4, not a commit.
6. **Tick the boxes.** Mark every `- [x] N.M` in the section in `tasks.md`.
6. **Tick the boxes.** Mark every `- [x] N.M` in the section in `tasks.md`. Never rewrite `tasks.md`
wholesale — only flip `[ ]→[x]`; it holds all future sections.
7. **Commit — one conventional commit per section:**
```
feat(<change-name>): <section title> (section N)
Expand All @@ -91,6 +92,8 @@ The unit of work is a **`## N.` section**. Walk sections in order from the resum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
```
8. **Report and pause.** Tell the user what landed in this section and ask before starting the next —
unless told to "apply all sections" / "apply without pausing".

## 4. Stop and ask — do not push on

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-18
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# DEVLOG — persistent-input-preamble

## Status

All sections complete. Section 5 gates all pass (build clean, 872 tests green, format clean, openspec valid). Awaiting commit and archive.

## Pre-flight notes

- Change branch `change/persistent-input-preamble` created from `proposals/terminal-input-chrome`
(not from `main`) because the proposal artifacts only exist on that branch.
- `openspec validate` passes on the correct base branch.

## Section 1 + 2 — Public surface & render model/composer

Sections 1 and 2 are implemented together in one worker call because `InputPreambleSurface`
(Section 1) contains the nested `SetInputPreambleCommand` whose `Apply` references
`model.FixedRegion.Preamble` — the preamble holder that is Section 2 task 2.1. They cannot
compile independently, so the worker produces both sections, then two separate commits are made.

### Key decisions carried into implementation

- D1: Mirror `StatusSurface` exactly — same constructor, same overloads, nested command class.
- D2: Preamble rows placed below an above-input overlay, above the input editor rows.
- D3: Preamble yields before input editor; status is sacred.
- D4: SetRows is set-and-hold; empty argument clears.
- D5: Preamble is presentational — never in the intercept chain.
- D6: Interface `IInputPreamble`, impl `InputPreambleSurface`, parallel to `IStatus`/`StatusSurface`.

### Internal model shape

- New `PreambleLine` class in `src/Dcli/Internal/FixedRegion/` — mirrors `StatusLine`.
- `FixedRegionComposer` gains `_preamble : PreambleLine` field and `Preamble` property.
- `RenderModel.FixedRegion` constructed as `new(new TextBuffer(), new PreambleLine(), new StatusLine())`.
- `IInputPreamble` declared in `src/Dcli/ITerminal.cs` alongside `IStatus`.
- `InputPreambleSurface` in `src/Dcli/InputPreambleSurface.cs`.
- `ITerminal.InputPreamble : IInputPreamble` property added after `Status`.
- `Terminal` ctor: `InputPreamble = new InputPreambleSurface(loop)` after `Status = ...`.

## Section 5 — Validation & packaging

Gates passed by orchestrator before worker brief:
- `dotnet build` clean (0 warnings, 0 errors)
- `dotnet test` 872/872 green
- `dotnet format --verify-no-changes` clean
- `openspec validate persistent-input-preamble --strict` valid

Version bumped: `0.2.0-rc.4` → `0.2.0-rc.5` in `src/Dcli/Dcli.csproj` and `src/Dcli.Testing/Dcli.Testing.csproj`.
CHANGELOG updated with `IInputPreamble` surface entry.

### dmon coordination

dmon's Terminal UX change consumes `ITerminal.InputPreamble` and must reference dcli `0.2.0-rc.5` (not rc.4). Update the NuGet reference in dmon after this change is published.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## Context

The fixed region is a bottom-pinned stack composed by `FixedRegionComposer.Compose` (`src/Dcli/Internal/FixedRegion/FixedRegionComposer.cs`). Its bands, top-to-bottom, are: an optional above-input overlay, the input editor, an optional below-input overlay, and the always-sacred status rows. The consumer can drive the bottom band via `StatusSurface.SetRows` (`ITerminal.Status`), which posts a `SetStatusCommand : ILoopCommand` that sets `model.FixedRegion.Status.Rows` and marks the model dirty; the composer reads those rows each frame.

There is no consumer surface for content pinned **above** the input editor. Transient dialogs already render a multi-line preamble above their widget (`multi-line-dialog-prompts`), but that is overlay-scoped and disappears with the dialog. dmon needs a *persistent* preamble for the base editor.

## Goals / Non-Goals

**Goals:**
- A persistent, consumer-set band of styled rows pinned directly above the base input editor, surviving across renders and input submissions until changed.
- An API that mirrors `StatusSurface` exactly, so the surface is immediately familiar and the wiring reuses a proven path.
- Sensible height-budget behaviour: the preamble yields before the input editor does; status stays sacred.

**Non-Goals:**
- The input prompt-prefix glyph (`❯`) on the editor line — separately deferred.
- Any key handling by the preamble — it is presentational.
- Welcome banner / MOTD — scrollback content owned by the consumer.

## Decisions

### D1: Mirror the StatusSurface wiring exactly

The preamble reuses the status path, one layer up the stack:

| Status (exists) | Preamble (new) |
|---|---|
| `ITerminal.Status : IStatus` | `ITerminal.InputPreamble : IInputPreamble` |
| `StatusSurface.SetRows(…)` → `_loop.Post(new SetStatusCommand(rows))` | `InputPreambleSurface.SetRows(…)` → `_loop.Post(new SetInputPreambleCommand(rows))` |
| `SetStatusCommand.Apply` → `model.FixedRegion.Status.Rows = rows; model.MarkDirty()` | `SetInputPreambleCommand.Apply` → `model.FixedRegion.Preamble.Rows = rows; model.MarkDirty()` |
| `Terminal` ctor: `Status = new StatusSurface(loop)` | `Terminal` ctor: `InputPreamble = new InputPreambleSurface(loop)` |
| Composer reads `_status.Rows` | Composer reads `_preamble.Rows` |

Same `params Line[]` + `IReadOnlyList<Line>` overloads; empty clears. This keeps the public surface symmetric ("rows above" / "rows below") and the implementation a near-copy of a tested path.

### D2: Band placement — directly above the input editor

`FixedRegionComposer.Compose` assembles rows in order. The preamble rows are inserted immediately above the input editor band:

```
live window (scrollback)
[ above-input overlay ] (when an overlay is placed AboveInput)
[ preamble rows ] ← NEW, when set
[ input editor rows ]
[ below-input overlay ] (Autocomplete)
[ status rows ] (sacred, bottommost)
```

The preamble sits below an above-input overlay: while a modal Dialog occupies the above-input slot it owns the visual foreground; the persistent preamble is chrome for the *base* editor. (In practice dmon clears or ignores the preamble's relevance during a modal dialog; the composer simply renders it in its band when present.)

### D3: Budget — preamble yields before the input, status stays sacred

The fixed-region height budget (`MaxHeight = clamp(appSet ?? 50% rows, 8, rows)`) is unchanged. The sacrifice order under pressure becomes: overlays squeeze (as today) → **preamble truncates** → the input editor retains at least one usable row → status rows are never squeezed. This mirrors how dialog preambles already truncate before their widget, so the arithmetic is the established pattern applied to the persistent band.

### D4: Persistence and clearing

`SetRows` is set-and-hold: once applied, the preamble renders every frame until `SetRows` is called again (replace) or with an empty argument (clear). This matches `StatusSurface` and is the whole point — the consumer sets the frame once at startup, not per turn.

### D5: Presentational — not in the intercept chain

The preamble never registers in the key-routing intercept chain (`active overlay → input editor`). It is pure presentation; all keys continue to reach the input editor (or the active overlay). This keeps it orthogonal to the existing **Intercept-chain key routing** and **Single cursor placement** requirements (the hardware cursor still parks at the input caret).

### D6: Naming

`InputPreamble` reuses the "preamble" vocabulary already established for dialogs (`InputRequest.Prompt` is documented as the dialog preamble), and reads as the natural counterpart to `Status`. The surface interface is `IInputPreamble` and the impl `InputPreambleSurface`, paralleling `IStatus` / `StatusSurface`.

## Risks / Trade-offs

- **Above-input overlay vs preamble ordering** is a genuine design choice; D2 places the preamble below an above-input overlay. If a future consumer needs the preamble above a dialog too, that is an additive follow-up — the band is independent.
- **Budget contention on tiny terminals**: with a 1–2 line preamble, an editor that needs ≥1 row, and ≥1 status row, an 8-row floor is comfortable; the truncation rule (D3) guarantees the editor and status survive.

## Open Questions

- **Version target.** `0.2.0-rc.5` assumed (additive, preview channel). Confirm at packaging time against the release-tag flow (`.github/workflows/release.yml`).
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Why

dmon's Terminal host is redesigning its bottom chrome to frame the input on both sides:

```
── dmon ────────────────────────────────────── ← header rule, pinned above the input
❯ user typing here █ ← the always-on input editor
────────────────────────────────────────────── ← rule ┐ status rows
[Ready] dmon core v0.2.0-preview.23 gemini… ← status ┘ (pinned, below input)
```

The **below-input** chrome is already expressible: `StatusSurface.SetRows(...)` pins styled rows beneath the input editor. There is no symmetric surface for the **above-input** chrome — the only way to put a line directly above the always-on editor today is to append it to scrollback, where it scrolls away as the conversation grows instead of staying pinned as a frame.

`multi-line-dialog-prompts` closed this exact gap for **transient** dialogs (`InputRequest.Prompt` et al. render a multi-line preamble above a dialog widget). This change is the analogue for the **persistent** base input editor: a consumer-set preamble band pinned directly above it, mirroring the status surface that already pins rows below it.

dmon is dcli's vehicle for proving and improving the substrate; a workaround in dmon (scrollback re-prints) is a substrate-gap signal. Closing it here lets dmon — and any future consumer wanting persistent input chrome (a session header, a mode banner, a hint line) — use dcli's intended surface.

## What Changes

- **New public surface `ITerminal.InputPreamble`** (type `IInputPreamble`, impl `InputPreambleSurface`), mirroring `ITerminal.Status` / `StatusSurface` one-for-one:
- `SetRows(params Line[])` and `SetRows(IReadOnlyList<Line>)` replace the preamble content; an empty argument clears it.
- Fire-and-forget via `_loop.Post(new SetInputPreambleCommand(rows))`; applied on the render-loop thread.
- Wired in `Terminal` construction exactly as `Status = new StatusSurface(loop)`.
- **New persistent band in the fixed-region stack.** The preamble renders directly above the input editor band (below the live window). `RenderModel.FixedRegion` gains a `Preamble` rows holder; `SetInputPreambleCommand.Apply` sets `model.FixedRegion.Preamble.Rows` and marks dirty; `FixedRegionComposer.Compose` emits the preamble rows immediately above the input rows.
- **Budget participation.** The preamble participates in the fixed-region height budget. Under pressure it truncates **before** the input editor loses its last usable row; the status band stays sacred (never squeezed). Null/empty preamble paints nothing and returns its rows to the budget.
- **Presentational only.** The preamble is not part of the intercept chain — it never consumes keys; all keys reach the input editor (or the active overlay) as before.
- **Version bump:** `dcli` and `Dcli.Testing` `0.2.0-rc.4` → `0.2.0-rc.5` (additive, preview channel).

## Capabilities

### New Capabilities

None — this change extends an existing capability only.

### Modified Capabilities

- `fixed-region`: MODIFIED the **Bottom-pinned component stack** requirement to include a persistent input-preamble band rendered directly above the input editor; ADDED a **Persistent input preamble** requirement specifying the `ITerminal.InputPreamble` surface, its `SetRows` semantics, persistence across renders/turns, null/empty clearing, height-budget participation (truncate-before-input, status stays sacred), and its presentational (non-intercepting) nature.

## Impact

- **Public API (`Dcli`):** additive — one new property `ITerminal.InputPreamble`, one new surface type. No existing signature changes; fully backwards compatible. Binary-compat is irrelevant on the preview channel; dmon takes a local reference and recompiles.
- **Public API (`Dcli.Testing`):** `HeadlessTerminal` exposes the same `InputPreamble` surface so consumers can assert preamble rows in tests; rendering flows through the existing painter.
- **Production code:** small. A surface type + a loop command (both mirror `StatusSurface` / `SetStatusCommand`), a `Preamble` holder on the render model, and one band insertion in `FixedRegionComposer.Compose` plus its budget arithmetic.
- **Tests:** ~6–8 in `tests/Dcli.Tests/` via `HeadlessTerminal` — preamble renders directly above the editor; persists across successive submissions; empty clears; truncates under budget pressure while the editor keeps ≥1 row and status stays fully rendered; keys fall through to the editor. Existing fixed-region tests stay green as a regression guard.
- **Consumers:** unblocks dmon's Terminal UX redesign (the pinned `── dmon ──` header rule above the input). dmon drops the scrollback-reprint workaround.
- **Out of scope (deferred):**
- The input **prompt-prefix glyph** (`❯` shown before the editable region) — still deferred per the existing note in `InputSurface` (`§10` prompt-prefix refinement). The preamble is above the editor, not on the editor's line; the `❯` is a separate feature dmon can pursue next if it wants the glyph on the live line.
- Welcome banner / message-of-the-day — these are ordinary scrollback content owned by the consumer (dmon), not fixed-region chrome.
- Per-band styling/borders beyond what a consumer composes into the `Line` rows themselves.
Loading
Loading