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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- **`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.

- **`IInput.SetPrompt(Line)` / `IInput.SetPrompt(string)`** — sets a styled prefix that is rendered before the editable text on the first row of the input editor. The prefix is purely presentational: submitted values and `InputChanged` payloads contain only the user-typed text. Clears with an empty line (`SetPrompt(new Line([]))`) or via the string overload (`SetPrompt("")` or `SetPrompt(null)`). Persists across submissions and `Clear()`. First-row wrap capacity is reduced by the prompt width; continuation rows use full width.

### Security

- **VT-escape injection gap closed.** Consumer text passed to `Segment`, `Line.FromText`, or
Expand Down
49 changes: 49 additions & 0 deletions openspec/changes/archive/2026-06-19-input-prompt-prefix/DEVLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# DEVLOG — input-prompt-prefix

## Status

| Section | Title | Status |
|---------|------------------------|----------|
| 1 | Public surface | complete |
| 2 | Command + editor model | complete |
| 3 | Tests | complete |
| 4 | Sample / demo | complete |
| 5 | Validation & packaging | complete |

## Context

- Base branch: `main` at commit after `persistent-input-preamble` merge (version `0.2.0-rc.5`).
- Target version: `0.2.0-rc.6`.
- Change branch: `change/input-prompt-prefix`.

## Section 1+2 — Public surface & Command+editor model

Briefed worker to implement sections 1 and 2 together (they are inseparable: the public surface
posts `SetPromptCommand` which lives inside `InputSurface`, and the command mutates `TextBuffer`
which needs the prompt field and render logic).

### Key design notes

- `SetPromptCommand` is a private inner class of `InputSurface` (mirrors `SetTextCommand`/`ClearCommand`).
- `TextBuffer.SetPrompt(Line?)` stores the prompt; null/empty = no prefix (default).
- Render changes are in `BuildVisualRows`: row 0 gets a reduced width (`width - promptWidth`);
the prompt is prepended to row 0's `Line` via segment prepend; continuation rows use full width.
- Caret column offset: when `VisualRow == 0`, `VisualCol += promptWidth` in `ComputeVisualPositionFromRows`.
- The `Line` type is used to carry styled segments; `DisplayWidth` measures the prompt width.
- `MoveHome`/`MoveEnd` on row 0 must not include the prompt columns in the buffer's char-index
mapping — the prompt is not in the buffer. The char-index mapping is unchanged; only the
reported visual column is offset.

## Section 5 — Validation & packaging

Gate results (all pass):
- `dotnet build` — clean, 0 warnings, 0 errors (890 tests compiled).
- `dotnet test` — 890 passed, 0 failed, 0 skipped.
- `dotnet format --verify-no-changes` — clean (no output).
- `openspec validate input-prompt-prefix --strict` — "Change 'input-prompt-prefix' is valid".

Actions taken:
- Version bumped `0.2.0-rc.5` → `0.2.0-rc.6` in `src/Dcli/Dcli.csproj` and `src/Dcli.Testing/Dcli.Testing.csproj`.
- CHANGELOG.md updated: added `IInput.SetPrompt` bullet under `[Unreleased] ### Added`.

Task 5.8 (dmon coordination) is informational — no dcli code change required.
35 changes: 35 additions & 0 deletions openspec/changes/archive/2026-06-19-input-prompt-prefix/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## 1. Public surface

- [x] 1.1 Add `SetPrompt(Line)` and `SetPrompt(string)` to `IInput` (string via `Line.FromText`); doc-comment that empty/`null` clears and that it does not emit `InputChanged`
- [x] 1.2 Implement both overloads on `InputSurface`: each posts `_loop.Post(new SetPromptCommand(line))`
- [x] 1.3 Remove the `Prompt` bullet from the `InputSurface` "Documented gaps" remarks (keep the `ReadOnly` bullet)

## 2. Command + editor model

- [x] 2.1 Add `SetPromptCommand : ILoopCommand` whose `Apply(model)` calls `model.FixedRegion.Editor.SetPrompt(line)` and `model.MarkDirty()` (mirror `SetTextCommand`)
- [x] 2.2 Add a prompt field to the owned editor (`TextBuffer`/editor) with a `SetPrompt` mutator; empty/`null` means no prefix
- [x] 2.3 Render the prefix on the editor's first visual row: editable text starts at `promptWidth`, caret column offset by `promptWidth` on row 0, first-row capacity = `width - promptWidth`; continuation rows begin at column 0 (no repeat). Ensure the prefix is never inserted into buffer contents

## 3. Tests (tests/Dcli.Tests, via HeadlessTerminal)

- [x] 3.1 Prefix renders before the editable text on the first row
- [x] 3.2 Caret parks immediately after the prefix when the buffer is empty
- [x] 3.3 No prompt set ⇒ editor renders identically to v1 (regression guard)
- [x] 3.4 Prompt persists across multiple submissions and across `Clear()`
- [x] 3.5 Submitted value and `InputChanged` payload exclude the prefix; history stores only user text
- [x] 3.6 First-row wrapping uses `width - promptWidth`; continuation row starts at column 0

## 4. Sample / demo

- [x] 4.1 Update a sample (or the demo) to set an input prompt (e.g. `SetPrompt("❯ ")`) so the surface is exercised end-to-end

## 5. Validation & packaging

- [x] 5.1 `dotnet build` clean (analyzers warnings-as-errors; nullable enabled)
- [x] 5.2 `dotnet test` all green
- [x] 5.3 `dotnet format --verify-no-changes` clean
- [x] 5.4 `openspec validate input-prompt-prefix --strict` passes
- [x] 5.5 Version bump: `src/Dcli/Dcli.csproj` and `src/Dcli.Testing/Dcli.Testing.csproj` to the next preview revision after `persistent-input-preamble` (`0.2.0-rc.6`, or a shared bump if applied together)
- [x] 5.6 Update `CHANGELOG.md` with the new `SetPrompt` surface
- [x] 5.7 Keep a `DEVLOG.md` in the change directory while applying
- [x] 5.8 dmon coordination: dmon's Terminal UX change consumes `ITerminal.Input.SetPrompt(...)` for the `❯` glyph alongside `ITerminal.InputPreamble`
35 changes: 0 additions & 35 deletions openspec/changes/input-prompt-prefix/tasks.md

This file was deleted.

38 changes: 38 additions & 0 deletions openspec/specs/fixed-region/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,44 @@ When `IsSecret = false`, the editor SHALL render the seeded default as plain tex
- **WHEN** an `InputRequest` is shown with `IsSecret=false` and a non-empty `Default`
- **THEN** the paint shows the default's clear-text content (no masking)

### Requirement: Input editor prompt prefix

The owned input editor SHALL support an optional, consumer-set prompt prefix rendered immediately before the editable region on the editor's first visual row. The persistent input surface (`ITerminal.Input`) SHALL expose `SetPrompt(Line)` and `SetPrompt(string)` (the latter converted via `Line.FromText`); an empty or `null` prompt SHALL render no prefix, which is the default (unchanged v1 behaviour). `SetPrompt` SHALL NOT emit `InputChanged`. The prompt SHALL persist across renders, submissions, history recall, and buffer `Clear` until changed.

The caret SHALL be positioned immediately after the prefix while it is on the first row, accounting for the prefix's display width; the first row's text capacity SHALL be the available width reduced by the prefix width, and wrapping on the first row SHALL be computed against that reduced width. Wrapped continuation rows SHALL begin at column 0 and SHALL NOT repeat the prefix.

The prompt prefix SHALL be presentational chrome: it SHALL NOT be part of the editor buffer, SHALL NOT be returned by `Submit`, SHALL NOT appear in `InputChanged` payloads, and SHALL NOT be stored in input history.

#### Scenario: Prompt renders before the editable text

- **WHEN** the consumer calls `ITerminal.Input.SetPrompt("❯ ")` and the user types `hello`
- **THEN** the editor's first row paints `❯ hello` with the prompt in its configured style

#### Scenario: Caret sits after the prefix

- **WHEN** a prompt is set and the buffer is empty
- **THEN** the hardware cursor parks at the column immediately after the prefix, not at column 0

#### Scenario: Empty prompt renders no prefix

- **WHEN** no prompt is set (or `SetPrompt` is called with an empty/`null` value)
- **THEN** the editor renders exactly as v1 with the editable text starting at column 0

#### Scenario: Prompt persists across submissions

- **WHEN** a prompt is set once and the user submits several inputs in succession
- **THEN** the prefix remains rendered on each new input line without being re-set

#### Scenario: Prompt is not part of submitted text

- **WHEN** a prompt is set and the user types `hello` and submits
- **THEN** the submitted value is `hello` (the prefix is not included), and input history stores only `hello`

#### Scenario: First-row wrapping accounts for the prefix width

- **WHEN** a prompt is set and the user types text that exceeds `width - promptWidth` on the first row
- **THEN** the text wraps at the reduced first-row width and the continuation row begins at column 0

### Requirement: Fixed-region height budget
The fixed region SHALL have a `MaxHeight` equal to `clamp(appSet ?? 50% of rows, 8, rows)`, SHALL consume only the rows its content needs up to that cap, and SHALL scroll components internally beyond the cap.

Expand Down
5 changes: 4 additions & 1 deletion samples/Dcli.Demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

t.InputPreamble.SetRows(Line.Dim("── dcli demo ─────────────────────────────────────────────────────────────"));
t.Scrollback.Append(Line.Dim(" InputPreamble set -- a rule above the input editor."));
t.Input.SetPrompt("❯ ");
t.Scrollback.Append(Line.Dim(" Input prompt set to ❯."));

await Task.Delay(TimeSpan.FromMilliseconds(800));

Expand Down Expand Up @@ -241,7 +243,8 @@
t.Status.SetRows(Line.Fg("DONE - Tour complete - press Ctrl+C to exit, or wait 3s.", Color.Named(Color.AnsiColor.Green)));

t.InputPreamble.SetRows();
t.Scrollback.Append(Line.Dim(" InputPreamble cleared."));
t.Input.SetPrompt("");
t.Scrollback.Append(Line.Dim(" InputPreamble and input prompt cleared."));

t.Scrollback.Append(Line.Bold("--- Tour complete ---"));
t.Scrollback.Append(Line.Fg("All dcli public surfaces exercised successfully.", Color.Named(Color.AnsiColor.BrightGreen)));
Expand Down
2 changes: 1 addition & 1 deletion src/Dcli.Testing/Dcli.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<!-- NuGet packaging -->
<IsPackable>true</IsPackable>
<PackageId>dcli.testing</PackageId>
<Version>0.2.0-rc.5</Version>
<Version>0.2.0-rc.6</Version>
<Authors>daemonicai</Authors>
<Company>daemonicai</Company>
<Description>Headless test harness for the dcli inline terminal-rendering library.</Description>
Expand Down
2 changes: 1 addition & 1 deletion src/Dcli/Dcli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<!-- NuGet packaging -->
<IsPackable>true</IsPackable>
<PackageId>dcli</PackageId>
<Version>0.2.0-rc.5</Version>
<Version>0.2.0-rc.6</Version>
<Authors>daemonicai</Authors>
<Company>daemonicai</Company>
<Description>Inline terminal-rendering library. Claude-Code-style styled output flows into the terminal's real scrollback, with a small interactive region pinned at the bottom.</Description>
Expand Down
17 changes: 17 additions & 0 deletions src/Dcli/ITerminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ public interface IInput
/// Does not emit <see cref="InputChanged"/>.
/// </summary>
void Clear();

/// <summary>
/// Sets the prompt prefix rendered immediately before the editable region on the first visual row.
/// Passing an empty or null-equivalent line clears the prompt and renders no prefix.
/// Does not emit <see cref="InputChanged"/>.
/// </summary>
/// <param name="line">The styled line to render as the prompt prefix.</param>
void SetPrompt(Line line);

/// <summary>
/// Sets the prompt prefix to a plain-text string rendered immediately before the editable region.
/// Passing a null or empty string clears the prompt and renders no prefix.
/// Shorthand equivalent to <c>SetPrompt(Line.FromText(text))</c>.
/// Does not emit <see cref="InputChanged"/>.
/// </summary>
/// <param name="text">The plain text to use as the prompt prefix. Null or empty clears the prompt.</param>
void SetPrompt(string text);
}

/// <summary>
Expand Down
46 changes: 40 additions & 6 deletions src/Dcli/InputSurface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ namespace Dcli;
/// <strong>Thread safety:</strong> all methods are safe to call from any thread.
/// </para>
/// <para>
/// <strong>No <c>InputChanged</c> on programmatic mutation:</strong> <see cref="SetText"/>
/// and <see cref="Clear"/> do NOT emit <see cref="InputChanged"/>. That event is reserved for
/// user-driven edits so that a consumer reacting to <see cref="InputChanged"/> (e.g. to update
/// autocomplete candidates) cannot trigger a feedback loop from its own programmatic writes.
/// <strong>No <c>InputChanged</c> on programmatic mutation:</strong> <see cref="SetText"/>,
/// <see cref="Clear"/>, and <see cref="SetPrompt(Line)"/> do NOT emit <see cref="InputChanged"/>.
/// That event is reserved for user-driven edits so that a consumer reacting to
/// <see cref="InputChanged"/> (e.g. to update autocomplete candidates) cannot trigger a feedback
/// loop from its own programmatic writes.
/// </para>
/// <para>
/// <strong>Documented gaps:</strong>
/// <list type="bullet">
/// <item><c>Prompt</c> — a prompt prefix shown before the editable region — is not in the
/// §10 model and is deferred to a later §10 refinement pass.</item>
/// <item><c>ReadOnly</c> — preventing user edits — is similarly deferred.</item>
/// </list>
/// </para>
Expand Down Expand Up @@ -57,6 +56,28 @@ public void Clear()
_loop.Post(new ClearCommand());
}

/// <summary>
/// Sets the prompt prefix rendered immediately before the editable region on the first visual row.
/// An empty line clears the prompt and renders no prefix.
/// Does not emit <see cref="InputChanged"/>.
/// </summary>
public void SetPrompt(Line line)
{
_loop.Post(new SetPromptCommand(line));
}

/// <summary>
/// Sets the prompt prefix to a plain-text string.
/// Null or empty clears the prompt and renders no prefix.
/// Does not emit <see cref="InputChanged"/>.
/// </summary>
/// <param name="text">The plain text to use as the prompt prefix. Null or empty clears the prompt.</param>
public void SetPrompt(string text)
{
Line line = string.IsNullOrEmpty(text) ? new Line([]) : Line.FromText(text);
_loop.Post(new SetPromptCommand(line));
}

// ── Commands ───────────────────────────────────────────────────────────────

private sealed class SetTextCommand : ILoopCommand
Expand All @@ -80,4 +101,17 @@ void ILoopCommand.Apply(RenderModel model)
model.MarkDirty();
}
}

private sealed class SetPromptCommand : ILoopCommand
{
private readonly Line _line;

internal SetPromptCommand(Line line) => _line = line;

void ILoopCommand.Apply(RenderModel model)
{
model.FixedRegion.Editor.SetPrompt(_line);
model.MarkDirty();
}
}
}
Loading
Loading