# Tickets for the delphi-terminal project
# Repo remote url: https://github.com/continuous-delphi/delphi-terminal
# File used by create-github-issues.ps1
# dev-automation-tooling script & document reference1: C:\code\dev-automation-tooling

---
Title: Demo VCL app with tabbed CMD shell
Issue: #1
Status: closed
Labels: enhancement
Body:
Create the standalone demo VCL application (`projects/demo-vcl/`) with a
main form containing a TPageControl. The first tab page is named "CMD Shell"
and hosts an interactive CMD session.

Implementation notes:
- Create the demo VCL project (.dpr, .dproj, main form)
- Main form has a TPageControl docked to fill the client area
- First tab: "CMD Shell"
- Launch `cmd.exe` with working directory set to the .exe's folder
- Redirect stdin/stdout/stderr via pipes
- Display console output in a scrollable control (TMemo or TRichEdit)
- Provide an input mechanism for typing commands
- The CMD process should stay alive across commands (persistent session)

Acceptance criteria:
- [ ] Demo VCL app builds and runs standalone
- [ ] Main form displays a tab control with a "CMD Shell" tab
- [ ] CMD shell starts automatically in the .exe's directory
- [ ] User can type commands and see output
- [ ] Session persists across commands (e.g. `cd ..` changes the working directory for subsequent commands)
- [ ] Core functionality is unit-tested where possible

---
Title: Add PowerShell tab to demo VCL app
Issue: #2
Status: closed
Labels: enhancement
Body:
Add a second tab to the demo VCL application for an interactive PowerShell
session, reusing the existing `TCmdShellProcess` and `TframeCmdShell`
infrastructure.

Implementation notes:
- Add a "PowerShell" tab to the TPageControl in the main form
- Launch `pwsh.exe` (or `powershell.exe` fallback) instead of `cmd.exe`
- Refactor `TCmdShellProcess` or `TframeCmdShell` as needed to accept a
  configurable shell executable
- PowerShell session should start in the .exe's directory
- Persistent session across commands, same as CMD tab

Acceptance criteria:
- [ ] Second tab labeled "PowerShell" appears in the demo app
- [ ] PowerShell starts automatically and displays its banner
- [ ] User can type PowerShell commands and see output
- [ ] Session persists across commands
- [ ] CMD tab continues to work as before
- [ ] Unit tests cover PowerShell shell variant

---
Title: Add legacy PowerShell (powershell.exe) tab to demo VCL app
Issue: #3
Status: closed
Labels: enhancement
Body:
Add a third tab to the demo VCL application for a legacy Windows PowerShell
(powershell.exe) session, alongside the existing CMD and pwsh tabs.

Implementation notes:
- Add a "PowerShell" tab launching `powershell.exe`
- Distinct from the "pwsh" tab which launches PowerShell 7+
- Reuses the same TCmdShellProcess/TframeCmdShell infrastructure

Acceptance criteria:
- [ ] Third tab labeled "PowerShell" appears in the demo app
- [ ] Legacy Windows PowerShell starts and displays its banner
- [ ] User can type commands and see output
- [ ] Unit tests cover powershell.exe variant

---
Title: ANSI escape code rendering in terminal output
Issue: #4
Status: closed
Labels: enhancement
Body:
pwsh and other modern CLI tools emit VT100/ANSI escape sequences for colored
output. Currently these render as raw text (e.g. `[31;1m...`). The terminal
output control should interpret ANSI CSI sequences and render colored text.

Implementation notes:
- Replace TMemo with a control capable of colored text (TRichEdit or a
  dedicated terminal control)
- Parse ANSI CSI sequences (SGR codes: bold, foreground/background colors,
  reset) and apply corresponding formatting
- Consider existing Delphi ANSI terminal libraries before building from
  scratch
- As a simpler interim step, stripping ANSI codes (regex `\x1B\[[0-9;]*[a-zA-Z]`)
  would give clean plaintext output

Acceptance criteria:
- [ ] ANSI color codes from pwsh render as colored text (not raw escape sequences)
- [ ] Basic SGR codes supported: reset, bold, foreground colors, background colors
- [ ] Non-color ANSI sequences (cursor movement, etc.) are stripped gracefully
- [ ] CMD tab output is unaffected

---
Title: Suppress ANSI escape codes via NO_COLOR environment variable
Issue: #5
Status: closed
Labels: enhancement
Body:
As an interim workaround until ANSI rendering is implemented (#4), pass
`NO_COLOR=1` in the process environment block when launching shell processes.
This tells pwsh and other tools to suppress ANSI escape sequences at the
source, avoiding raw escape codes in the output without risk of stripping
valid content.

Implementation notes:
- Build a custom environment block for CreateProcess that includes
  `NO_COLOR=1` alongside the inherited parent environment
- Apply to all shell types (cmd, pwsh, powershell) -- the variable is
  harmless for shells that do not recognize it
- When #4 (ANSI rendering) is implemented, remove NO_COLOR so colored
  output flows through

Acceptance criteria:
- [ ] pwsh output no longer contains raw ANSI escape sequences
- [ ] Existing CMD and PowerShell functionality unaffected
- [ ] Environment block correctly inherits parent environment variables
- [ ] Unit test verifies ANSI-free output from pwsh

---
Title: Command history with up/down arrow keys
Issue: #6
Status: closed
Labels: enhancement
Body:
Add command history navigation to the terminal input. When the user presses
Up or Down arrow in the input TEdit, cycle through previously entered
commands.

Implementation notes:
- Store commands in a TStringList (per frame instance)
- Track a history index; Up moves backward, Down moves forward
- Pressing Enter resets the index to the end
- Empty commands should not be added to history
- Duplicate consecutive commands should not be added

Acceptance criteria:
- [ ] Up arrow recalls the previous command
- [ ] Down arrow moves forward through history
- [ ] Enter submits and resets history position
- [ ] Empty and duplicate consecutive commands are not stored
- [ ] Each tab maintains its own independent history
- [ ] Unit tests verify history navigation

---
Title: Detect shell process exit and handle gracefully
Issue: #7
Status: closed
Labels: enhancement
Body:
When the shell process exits (e.g. user types `exit`), the UI should
detect this and respond appropriately rather than leaving a dead session.

Implementation notes:
- The reader thread already exits when the pipe closes; use this as
  the signal that the process has ended
- Fire an OnTerminated event from the reader thread exit path
- In the frame, show a message in the output memo (e.g. "[Process exited]")
- Disable the input edit when the process is not running
- Optionally offer a restart mechanism (e.g. press Enter to restart)

Acceptance criteria:
- [ ] Typing `exit` in any shell shows a process-exited message
- [ ] Input edit is disabled or shows restart prompt after exit
- [ ] Running property reflects the actual process state
- [ ] Unit test verifies OnTerminated fires after process exit

---
Title: IDE plugin skeleton with dockable form
Issue: #8
Status: closed
Labels: enhancement
Body:
Create the minimal RAD Studio IDE plugin (BPL package) in
`projects/ide-plugin/` that registers a dockable form and hosts the
shared TframeCmdShell frame.

Implementation notes:
- Create a design-time/runtime BPL package (.dpk, .dproj)
- Register a dockable form using ToolsAPI (IDockableForm or
  TDockableForm descendant)
- Add a menu item under View (or Tools) to show/hide the terminal
- Host the shared frame inside the dockable form
- Keep the plugin as thin as possible -- no business logic
- The plugin should work with RAD Studio 12+

Acceptance criteria:
- [ ] BPL package builds successfully
- [ ] Installing the package adds a menu item to show the terminal
- [ ] The dockable form appears and hosts the terminal frame
- [ ] Terminal is functional within the IDE (CMD/pwsh/PowerShell)
- [ ] Form position/docking state persists across IDE sessions

---
Title: Working directory shortcuts (project dir / file dir)
Issue: #9
Status: closed
Labels: enhancement
Body:
Add a mechanism to quickly change the shell's working directory to:
- The active project's folder (simulated in demo VCL via folder picker)
- The currently open file's folder (simulated in demo VCL via folder picker)

This prepares the infrastructure for IDE integration where ToolsAPI will
provide the actual paths.

Implementation notes:
- Add a toolbar panel at the top of TframeCmdShell with "Project Dir" and
  "File Dir" buttons, plus a label showing the current working directory
- Add `SetWorkingDirectory(const APath: string)` to the frame which sends
  the appropriate shell command (`cd /d "path"` for cmd, `Set-Location "path"`
  for pwsh/powershell)
- TCmdShellProcess needs to know its shell type so the frame can pick the
  correct cd command (store the shell exe from Start)
- In the demo VCL app, the buttons open a folder picker dialog
- The frame exposes an event or callback so the host can provide paths
  (IDE plugin will supply via ToolsAPI; demo app uses folder picker)

Acceptance criteria:
- [ ] Toolbar with Project Dir and File Dir buttons visible in each tab
- [ ] Clicking a button opens folder picker (demo VCL) or resolves path (IDE)
- [ ] Selecting a folder changes the shell working directory
- [ ] Correct command used per shell type (cd /d for cmd, Set-Location for PS)
- [ ] Current working directory label updates after change
- [ ] Unit tests verify correct command generation per shell type

---
Title: Move menu item from Help Wizards to View menu
Issue: #10
Status: closed
Labels: bug
Body:
IOTAMenuWizard places the radTerminal menu item under Help > Help Wizards.
It should appear under View > radTerminal, matching the convention used by
other IDE tool windows.

Replace the IOTAMenuWizard approach with manual menu insertion via
INTAServices.MainMenu. Find the View menu by component name or caption
and add a TMenuItem directly. Use the radListHotkeys project
(C:\code\radListHotkeys\source\radHotkeys.Menu.pas) as the reference
pattern.

Implementation notes:
- Remove IOTAMenuWizard from TDelphiTerminalWizard (keep IOTAWizard for
  RegisterPackageWizard lifecycle)
- Create a new unit radTerminal.Plugin.Menu that handles menu installation
  via INTAServices.MainMenu with View menu fallback resolution
- Menu item click calls TfrmradTerminalDock.ShowInstance
- Install in unit initialization, uninstall in finalization
- Remove stale menu items on install (guard against double-install)

Acceptance criteria:
- [ ] radTerminal menu item appears under View menu
- [ ] No menu item under Help > Help Wizards
- [ ] Clicking View > radTerminal shows the dockable terminal form
- [ ] Uninstalling the package removes the menu item

---
Title: Keyboard shortcut to toggle terminal panel
Issue: #11
Status: closed
Labels: enhancement
Body:
Add a configurable keyboard shortcut to show/hide the radTerminal dockable
form. The specific key combination is TBD -- most obvious choices (Ctrl+Alt+T,
etc.) are already taken by the IDE.

Implementation notes:
- Register a shortcut via IOTAKeyboardBinding or a TAction with ShortCut
- The shortcut should toggle visibility (show if hidden, focus if shown,
  hide if focused)
- Consider making the shortcut configurable via the Tools > Options config
  screen (see config ticket)

Acceptance criteria:
- [ ] Keyboard shortcut toggles the terminal panel visibility
- [ ] Shortcut does not conflict with existing IDE shortcuts
- [ ] Shortcut is discoverable (shown in View menu item hint or caption)

---
Title: Tab switching with Ctrl+Tab and Ctrl+1/2/3
Issue: #12
Status: closed
Labels: enhancement
Body:
Allow switching between terminal tabs (CMD, pwsh, PowerShell) using keyboard
shortcuts within the terminal panel.

Implementation notes:
- Ctrl+Tab / Ctrl+Shift+Tab cycles forward/backward through tabs
- Ctrl+1, Ctrl+2, Ctrl+3 jumps directly to the corresponding tab
- Handle these in the frame's input edit KeyDown handler
- After switching tabs, focus the new tab's input edit
- Only intercept these when the terminal input has focus

Acceptance criteria:
- [ ] Ctrl+Tab moves to the next tab, Ctrl+Shift+Tab to the previous
- [ ] Ctrl+1/2/3 jumps to CMD/pwsh/PowerShell tab directly
- [ ] Focus moves to the input edit on the newly active tab
- [ ] Shortcuts only fire when terminal input is focused

---
Title: Clear terminal output with Ctrl+L and Clear button
Issue: #13
Status: closed
Labels: enhancement
Body:
Add a way to clear the terminal output without restarting the shell process.

Implementation notes:
- Add a "Clear" TSpeedButton to the toolbar panel (next to Project Dir
  and File Dir)
- Ctrl+L in the input edit also clears the output
- Clear only resets FRichOutput content and FAnsiParser state -- the shell
  process continues running
- Do not add the clear command to command history

Acceptance criteria:
- [ ] Clear button on toolbar clears the terminal output
- [ ] Ctrl+L in the input edit clears the terminal output
- [ ] Shell process continues running after clear
- [ ] ANSI parser state is reset (colors return to default)
- [ ] Works in both demo VCL app and IDE plugin

---
Title: Auto-cd on active project switch
Issue: #14
Status: closed
Labels: enhancement
Body:
When the user switches the active project in the IDE, automatically change
the terminal's working directory to the new project's folder.

Implementation notes:
- Register an IOTAProjectNotifier or IOTAIDENotifier to detect active
  project changes
- When the active project changes, call SetWorkingDirectory on the
  affected tab(s)
- Open question: should all tabs change directory, or only the currently
  active tab? The user should be prompted for preference, or this could
  be a config option (see config ticket). Default suggestion: change only
  the active tab, since the user may intentionally have different tabs in
  different directories.
- Guard against rapid-fire notifications (e.g. project group loading)

Acceptance criteria:
- [ ] Switching active project changes the terminal working directory
- [ ] Behavior is configurable (all tabs vs active tab only)
- [ ] No errors when no project is open or project has no folder
- [ ] Rapid project switches do not flood the terminal with cd commands

---
Title: Tools > Options configuration screen for radTerminal
Issue: #15
Status: closed
Labels: enhancement
Body:
Add a configuration screen under Tools > Options > Third Party > radTerminal
for user preferences. Settings are persisted to the Windows registry under
the Embarcadero key (standard for IDE experts).

Implementation notes:
- Register a frame via INTAAddInOptions (ToolsAPI) to appear under
  Third Party in the Options dialog
- Registry key: HKCU\Software\Embarcadero\BDS\<version>\radTerminal
- Configuration options (initial set):
  - Default shell: which shell tab to activate on first open
    (CMD / pwsh / PowerShell)
  - Visible tabs: checkboxes for which shell tabs to create
    (e.g. disable legacy PowerShell if not needed)
  - Auto-cd behavior: all tabs vs active tab only (relates to auto-cd
    ticket)
  - Font size override
- Load settings on plugin initialization, apply to dockable form creation
- Save settings on Options dialog OK

Acceptance criteria:
- [ ] radTerminal appears under Tools > Options > Third Party
- [ ] Settings are saved to and loaded from the registry
- [ ] Changing visible tabs takes effect on next terminal open
- [ ] Default shell selection is respected
- [ ] Settings survive IDE restart

---
Title: ANSI 256-color and RGB color support
Issue: #16
Status: closed
Labels: enhancement
Body:
Extend the ANSI parser to support 256-color and 24-bit RGB color sequences
used by modern CLI tools like `git diff --color`, `bat`, `delta`, etc.

Implementation notes:
- 256-color foreground: ESC[38;5;Nm where N is 0-255
- 256-color background: ESC[48;5;Nm where N is 0-255
- 24-bit RGB foreground: ESC[38;2;R;G;Bm
- 24-bit RGB background: ESC[48;2;R;G;Bm
- The 256-color palette consists of:
  - 0-7: standard colors (already supported)
  - 8-15: bright colors (already supported)
  - 16-231: 6x6x6 color cube
  - 232-255: grayscale ramp
- Extend TAnsiAttributes to hold a TColor directly instead of TAnsiColor
  enum when extended colors are used
- Update ApplySegmentFormat to use the direct TColor value
- Add unit tests for 256-color and RGB parsing

Acceptance criteria:
- [ ] `git diff --color=always` renders with correct colors
- [ ] 256-color codes (ESC[38;5;Nm) are parsed and rendered
- [ ] 24-bit RGB codes (ESC[38;2;R;G;Bm) are parsed and rendered
- [ ] Both foreground and background extended colors work
- [ ] Existing 16-color support is not broken
- [ ] Unit tests cover 256-color and RGB parsing

---
Title: Fix use-after-free risk in queued thread notifications during shutdown
Issue: #22
Status: closed
Labels: bug
Body:
`NotifyOutput` and `NotifyExit` in `TCmdShellProcess` use `TThread.Queue(nil, ...)`
with closures that capture `LOwner` (a raw pointer to the owning `TCmdShellProcess`).
If the shell object is destroyed while queued messages are still pending in the main
thread message queue, the closures execute against freed memory.

**Scenario:** The reader thread queues multiple output notifications. The owner calls
`Terminate` (which waits for thread exit and frees the thread). The destructor
completes. The queued anonymous methods then fire with a dangling `LOwner` pointer,
causing an access violation.

This is most likely to manifest during IDE shutdown or when rapidly closing/reopening
the terminal panel while the shell is producing output.

**Affected code:** `source/Delphi.Terminal.CmdShell.pas` lines 76-101
(`TPipeReaderThread.NotifyOutput` and `TPipeReaderThread.NotifyExit`).

**Possible approaches:**
- Add a guard flag (e.g. `FDestroying`) checked inside each queued closure before
  accessing the owner, protected by a shared reference or critical section
- Switch from `TThread.Queue(nil, ...)` to `TThread.Queue(FReaderThread, ...)` so
  that freeing the thread cancels its pending queued methods
- Use an ref-counted shared-state record so closures can detect owner destruction
- Drain the message queue after `WaitFor` and before freeing the owner

Acceptance criteria:
- [ ] Destroying a `TCmdShellProcess` while output is actively being produced does
      not cause an access violation
- [ ] Rapid close/reopen of the terminal panel does not crash
- [ ] IDE shutdown with active shell output does not crash
- [ ] Existing tests continue to pass

---
Title: Check WriteFile return value in SendCommand
Issue: #23
Status: closed
Labels: bug
Body:
`TCmdShellProcess.SendCommand` does not check the return value of `WriteFile`.
If the shell process has crashed between the `FRunning` check and the write,
the call fails silently -- the user gets no feedback that their command was
not delivered.

**Affected code:** `source/Delphi.Terminal.CmdShell.pas`, `SendCommand` method.

```delphi
WriteFile(FStdInWrite, Bytes[0], Length(Bytes), Written, nil);
```

The fix should check the `WriteFile` result. On failure (broken pipe), close
the stdin write handle so subsequent sends also fail fast. The reader thread
will independently detect the broken stdout pipe and fire `HandleNaturalExit`
to update `FRunning` and notify the UI.

Acceptance criteria:
- [ ] `WriteFile` return value is checked in `SendCommand`
- [ ] Broken pipe closes the stdin handle to prevent repeated writes to a dead handle
- [ ] Existing tests continue to pass

---
Title: Multi-byte UTF-8 characters can split across pipe reads
Issue: #24
Status: closed
Labels: bug
Body:
The reader thread in `TPipeReaderThread.Execute` reads raw bytes from the
pipe and immediately decodes them in a single call:

```delphi
NotifyOutput(FEncoding.GetString(Bytes));
```

A multi-byte UTF-8 character (2-4 bytes) can be split across two `ReadFile`
calls. The first call produces a replacement character for the incomplete
sequence; the trailing bytes in the next call also produce a replacement
character. Result: non-ASCII text (accented characters, CJK, emoji, Unicode
file paths) randomly corrupts depending on where the read boundary falls.

**Affected code:** `source/Delphi.Terminal.CmdShell.pas`, `TPipeReaderThread.Execute`
(lines 103-117).

**Fix:** Accumulate a small leftover-byte buffer between reads. After each
`ReadFile`, append the new bytes to the leftover buffer, decode only complete
characters, and carry over any trailing incomplete byte sequence for the next
read. For UTF-8, an incomplete trailing sequence can be detected by checking
the lead-byte pattern of the final bytes.

Acceptance criteria:
- [ ] Multi-byte UTF-8 characters split across read boundaries decode correctly
- [ ] Single-byte ASCII output is unaffected
- [ ] OEM codepage path (CMD) still works correctly
- [ ] Unit test verifies correct decoding of a split multi-byte character
- [ ] Existing tests continue to pass

---
Title: Shell start failure crashes entire terminal panel
Issue: #25
Status: closed
Labels: bug
Body:
If `pwsh.exe` (or `powershell.exe`) is not installed and the user has that
tab enabled in settings, `TCmdShellProcess.Start` calls `RaiseLastOSError`.
This exception propagates out of the dock form constructor, preventing the
terminal panel from opening at all. A missing optional shell kills the
entire panel.

**Affected code:** `projects/ide-plugin/Delphi.Terminal.Plugin.DockForm.pas`,
lines 143-157. Each `StartShell` call can raise, and none are wrapped in
try/except.

**Fix:** Wrap each `StartShell` call in a try/except. On failure, show an
error message in that tab's output area (e.g. "[pwsh.exe not found]") and
leave the tab visible but inactive. The remaining tabs should still start
normally.

Acceptance criteria:
- [ ] Terminal panel opens even if one or more shell executables are missing
- [ ] Failed tabs show a descriptive error message in the output area
- [ ] Working tabs are unaffected by a sibling tab's failure
- [ ] Existing tests continue to pass

---
Title: Add Ctrl+C support to stop long-running / runaway commands
Issue: #28
Status: closed
Labels: enhancement
Body:
The shell runs with CREATE_NO_WINDOW, so there is no console to receive
GenerateConsoleCtrlEvent. The user has no way to interrupt a long-running
command (e.g., ping -t, an infinite loop).

Implementation uses the AttachConsole + GenerateConsoleCtrlEvent approach:
- Create shell with CREATE_NEW_PROCESS_GROUP instead of CREATE_NO_WINDOW
- SendCtrlC: AttachConsole(childPID), disable our handler with
  SetConsoleCtrlHandler(nil, True), call GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0),
  then FreeConsole and re-enable our handler
- Stop speed button on toolbar and Ctrl+C in input edit (when no text selected)

Acceptance criteria:
- [ ] Ctrl+C in the input edit interrupts the running command
- [ ] Stop button on toolbar interrupts the running command
- [ ] Shell session survives the interrupt (user can continue typing)
- [ ] Ctrl+C with selected text still copies (standard behavior)
- [ ] All three shell types (CMD, pwsh, PowerShell) work with the new process group flag
- [ ] Existing tests continue to pass

---
Title: Dockable form not restored on IDE restart -- missing desktop registration
Issue: #35
Status: closed
Labels: bug
Body:
When the delphi-terminal window is docked and the user saves the default
desktop, the terminal does not reappear in its docked position after
restarting RAD Studio. The IDE saves the dock state (via `DeskSection`,
`AutoSave`, `SaveStateNecessary`) but has no way to recreate the form
on startup because the class is not registered with the desktop manager.

**Root cause:** The plugin never calls `RegisterDesktopFormClass` or
`RegisterFieldAddress` (from the `DeskUtil` unit). Without these, the
IDE records the form's dock state but cannot instantiate it during
desktop restore.

**Reference implementations:**
- GExperts `GX_IdeDock.pas` -- wraps both calls in an `IIdeDockManager`
  interface; sets `DeskSection := Name` so the section matches the
  form's component name.
- DTerminal `uMain.pas` (lines 63-68) -- calls both in its `Register`
  procedure with a stable identifier.

**Required changes:**

1. Add `DeskUtil` to the uses clause of `Delphi.Terminal.Plugin.Registration`.
2. In `Register`, after the wizard/options/keybinding registration, call:
   - `RegisterDesktopFormClass(TfrmDelphiTerminalDock, ID, ID)` where
     `ID` is a stable string matching `DeskSection`.
   - `RegisterFieldAddress(ID, @FInstance)` -- requires exposing the
     `FInstance` unit variable (or moving it to a shared location).
3. In `Delphi.Terminal.Plugin.DockForm`:
   - Set `Name := ID` in the constructor so the VCL component name
     matches the desktop section.
   - Ensure `DeskSection` uses the same identifier (currently
     `'continuous_delphi_delphi_terminal'` -- keep this value but use
     it consistently as a named constant).
   - The constructor must handle IDE-initiated creation (the IDE calls
     `Create(nil)` with no prior `ShowInstance` call) -- assign
     `FInstance := Self` inside the constructor so the singleton is set
     regardless of creation path.
4. In `Delphi.Terminal.Plugin.Lifecycle`:
   - Call `UnregisterFieldAddress(@FInstance)` during shutdown, before
     freeing the form.

**Consistency note:** The `DeskSection` value, the `Name` property, and
the identifiers passed to `RegisterDesktopFormClass` /
`RegisterFieldAddress` must all be the same string.

Acceptance criteria:
- [ ] `RegisterDesktopFormClass` and `RegisterFieldAddress` are called
      during plugin registration
- [ ] `UnregisterFieldAddress` is called during plugin shutdown
- [ ] Form `Name`, `DeskSection`, and registration identifier are the
      same constant string
- [ ] Docking the terminal, saving the desktop, and restarting the IDE
      restores the terminal in its docked position
- [ ] `ShowInstance` and `ToggleInstance` continue to work correctly
      after the registration changes
- [ ] The form constructor handles both user-initiated and IDE-initiated
      creation paths

---
Title: Add "Export..." button to Saved Commands window
Issue: #40
Status: closed
Labels: enhancement
Body:
Add an Export button to the Saved Commands editor dialog that exports
commands matching a user-specified prefix to a JSON bundle file. The
exported file uses the standard bundle format (prefix, description,
commands array) and can be imported on other machines.

Current workaround is to access
HKCU\Software\Embarcadero\BDS\{ver}\Continuous-Delphi\delphi-terminal\SavedCommands
REG_SZ key directly.

Acceptance criteria:
- [ ] Export button appears in the Saved Commands editor
- [ ] User is prompted for prefix and description
- [ ] Only commands matching the prefix are exported
- [ ] Output file is valid bundle JSON (importable via Import)
- [ ] Unit tests cover ToBundleJSON round-trip and prefix filtering

---
Title: Add version number display within the IDE
Issue: #36
Status: closed
Labels: enhancement
Body:
Display the plugin version number within the IDE, either in the
dockable terminal form and/or in the config screen.

Acceptance criteria:
- [ ] Version number is visible in the Tools > Options config screen
- [ ] Version is read from the BPL VERSIONINFO resource at runtime

---
Title: Per-project saved command lists
Issue: #33
Status: closed
Labels: enhancement
Body:
Auto-detect and load commands from a .delphi-terminal.json file next to
the active project's .dproj. Project commands are auto-prefixed with
project:<dprojBaseName>. and only appear in the command palette when
that project is active. No UI editor -- users create/edit the JSON file
manually or use Export to generate a starting point.

Acceptance criteria:
- [ ] .delphi-terminal.json next to .dproj is auto-loaded on palette open
- [ ] Project commands are prefixed with project:<name>.
- [ ] Commands only appear when the corresponding project is active
- [ ] Shell type filtering applies to project commands
- [ ] Missing or malformed file is handled gracefully
- [ ] Unit tests cover LoadBundleFile

---
Title: New variables needed for selected Build Config and selected Platform
Issue: #41
Status: closed
Labels: enhancement
Body:
Add ${BuildConfig} and ${Platform} variable tokens to the variable
expander. ${BuildConfig} resolves to the active build configuration
(Debug, Release) and ${Platform} resolves to the active target platform
(Win32, Win64, etc.) from ToolsAPI. Commands containing these variables
are automatically filtered out of the command palette when no project
is open.

Acceptance criteria:
- [ ] ${BuildConfig} expands to the active build configuration name
- [ ] ${Platform} expands to the active target platform name
- [ ] Commands using these variables are hidden from palette when no project is open
- [ ] Variable legend in the editor dialog includes the new variables
- [ ] Unit tests cover expansion and ContainsProjectVariable detection
- [ ] Documentation updated with new variable tokens

---
Title: Filter commands with file-variables when no file is open
Issue: #42
Status: closed
Labels: enhancement
Body:
Commands containing `${FileDir}`, `${FilePath}`, or `${FileName}` should
be hidden from the command palette when no editor file is currently open,
matching the existing behavior for project-variables (`${BuildConfig}`,
`${Platform}`) which are hidden when no project is active.

Implementation notes:
- Add a `ContainsFileVariable` function to `Delphi.Terminal.VariableExpander`
  (similar to the existing `ContainsProjectVariable`)
- In `HandleCommandPaletteRequested`, check whether an active file exists
  (i.e. `GetCurrentFilePath` returns non-empty) and skip commands that
  reference file-variables when it does not
- Apply the same filtering to both global and per-project commands

Note: `ContainsFileVariable` already exists as a local function in
`Delphi.Terminal.Plugin.SavedCommandsEditor.pas` for working-dir
validation. Consider promoting it to the shared VariableExpander unit
and removing the local duplicate.

Acceptance criteria:
- [ ] Commands with `${FileDir}`, `${FilePath}`, or `${FileName}` are
      hidden from the palette when no editor file is open
- [ ] Commands without file-variables are unaffected
- [ ] Unit tests cover `ContainsFileVariable` detection
- [ ] Documentation updated

---
Title: AttachConsole undefined in Delphi 10.2
Issue: #37
Status: closed
Labels: "older version support"
Body:
AttachConsole ? something above d10.2.3?

---
Title: TOTAFileNotification in Delphi 10.2 does not have ofnBeginProjectGroupOpen and ofnEndProjectGroupOpen
Issue: #38
Status: closed
Labels: "older version support"
Body:
Support older versions by only referencing `ofnBeginProjectGroupOpen` and `ofnEndProjectGroupOpen`
in 10.4 or later.  Added DELPHI_COMPILER_VERSIONS.inc to project and added Source to search path
v1.1.55
---
Title: TJSONAncestor.Format not available in 10.2
Issue: #43
Status: closed
Labels: "older version support"
Body:
Delphi.Terminal.SavedCommands.pas references Result := Root.Format(2);
v1.1.57
---
Title: PowerShell terminals should start without the banner
Issue: #46
Status: closed
Labels: "enhancement"
Body:
Can start pwsh with the -NoLogo switch to suppress
v1.2.62
---
Title: CMD.exe should use 'COMSPEC' environment variable
Issue: #47
Status: closed
Labels: "enhancement"
Body:
Currently hardcoded to cmd.exe
v1.2.62
---
Title: Add support for WSL
Issue: #39
Status: closed
Labels: "enhancement"
Body:
wsl starts, but:
The output does not appear in the console
Any command will result in an error message: "-bash: line 1: $'dir\r': command not found", probably because it does not like CR/LF line ends.
v1.3.63
---
Title: VCL demo crashes on startup if optional shell is not installed
Issue: #49
Status: closed
Labels: bug
Body:
The VCL demo app (`Delphi.Terminal.VCLDemo.exe`) unconditionally launches
all four shell types (CMD, pwsh, PowerShell, WSL) in `FormCreate`. If any
shell executable is not installed (e.g. `pwsh.exe` or `wsl.exe`),
`CreateProcess` fails and `RaiseLastOSError` raises an unhandled `EOSError`
(code 2 -- file not found), crashing the app on startup.

The IDE plugin already handles this correctly -- `StartTerminalShell` wraps
each `StartShell` call in try/except and calls `ShowStartupError` on failure
(`Delphi.Terminal.Plugin.DockForm.pas` lines 279-284). The VCL demo needs
the same pattern.

Fix: wrap each `StartShell` call in `Form.Main.pas` with try/except and
call `ShowStartupError` on failure, matching the IDE plugin approach.

Acceptance criteria:
- [ ] VCL demo opens even if pwsh.exe, wsl.exe, or powershell.exe is missing
- [ ] Failed tabs show a descriptive error message in the output area
- [ ] Working tabs are unaffected by a sibling tab's failure

---
Title: ConPTY: add foundation units to plugin package and test project
Issue: #52
Status: closed
Labels: enhancement, conpty
Body:
Part of the ConPTY 2.0 effort (umbrella #31; see plan-conpty.md, Phase 0).
Bring the committed foundation units into the build so they compile and are
testable. Effort: S. Verify: build.

Implementation notes:
- Add `WinAPI.ConPty`, `Delphi.Terminal.Pty`, `Delphi.Terminal.TextDecode`,
  `Delphi.Terminal.ConPtyReader` to the plugin `Delphi.Terminal.Plugin.dpk`
  `contains` clause.
- Reference the shared units from the DUnitX test project as needed.

Acceptance criteria:
- [ ] Plugin package builds with the four units included
- [ ] Test project builds referencing the shared units
- [ ] No runtime behavior change

---
Title: ConPTY: TConPty/TConPtyReader round-trip smoke test
Issue: #53
Status: closed
Labels: enhancement, conpty, test
Body:
Phase 0. Prove the session + reader stack end to end. Effort: S. Verify: integration.

Implementation notes:
- Start `cmd.exe` via `TConPty`; attach a `TConPtyReader`; send `echo hello`
  and assert the output is delivered on the main thread.
- Exercise deliberate teardown (`Terminate` reader -> `TConPty.Close`); assert
  the reader joins, handles close, and there is no hang or leak.
- Guard for ConPTY availability; skip gracefully below Windows 1903.

Acceptance criteria:
- [ ] Test captures `echo` output from a real cmd.exe
- [ ] Deliberate teardown joins the reader and closes cleanly
- [ ] Skips when ConPTY is unavailable

---
Title: ConPTY: Job Object process-tree cleanup in TConPty
Issue: #54
Status: closed
Labels: enhancement, conpty
Body:
Phase 0. Prevent orphaned descendant processes (e.g. Claude Code's `node.exe`
grandchildren) when a session ends. Correctness-critical for the headline use
case. Effort: S. Verify: integration.

Implementation notes:
- Create a Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; assign the
  child to it at spawn in `TConPty.Start`.
- Close the job handle during `TConPty.Close` so the whole tree terminates.

Acceptance criteria:
- [ ] Child and its descendants terminate on Close
- [ ] No orphaned processes after a session that spawned grandchildren
- [ ] Job handle lifecycle leaks nothing

---
Title: ConPTY: gate availability at Windows build 18362 (1903)
Issue: #55
Status: closed
Labels: enhancement, conpty
Body:
Phase 0. Require known-good ConPTY (stable resize/alt-screen) and fall back to
legacy below it. Effort: S. Verify: unit.

Implementation notes:
- Extend the availability check beyond `Win32MajorVersion >= 10` to also require
  OS build >= 18362.
- Keep `GetProcAddress` presence as the primary gate; version as secondary.

Acceptance criteria:
- [ ] IsAvailable is False below build 18362
- [ ] IsAvailable is True on 1903+ with the exports present
- [ ] Below-threshold systems resolve to the legacy backend

---
Title: ConPTY: ITerminalProcess abstraction; adapt legacy TCmdShellProcess
Issue: #56
Status: closed
Labels: enhancement, conpty
Body:
Phase 1. One backend contract both paths implement. Effort: M. Verify: unit
(existing tests pass).

Implementation notes:
- Define `ITerminalProcess`: `Start(shellInfo, workDir, size)`, `WriteInput`,
  `SendInterrupt`, `Resize`, `Terminate`, `Running`, `DiscardQueuedOutput`,
  `OnOutput`, `OnProcessExit`.
- Make legacy `TCmdShellProcess` implement it with unchanged behavior.

Acceptance criteria:
- [ ] ITerminalProcess defined
- [ ] Legacy backend implements it; existing DUnitX tests pass
- [ ] No behavior change to the legacy path

---
Title: ConPTY: TConPtyShell backend (input, interrupt, resize, shell mapping)
Issue: #57
Status: closed
Labels: enhancement, conpty
Body:
Phase 1. The backend that owns `TConPty` + `TConPtyReader` and implements
`ITerminalProcess`. Effort: M. Verify: integration.

Implementation notes:
- Start -> `TConPty.Start` + create/`RegisterReader`; Stop -> Terminate reader +
  `TConPty.Close` + free.
- `WriteInput`: UTF-8 encode -> `WriteFile(InputWrite)`. `SendInterrupt`: write `#3`.
- `Resize` -> `TConPty.Resize`.
- Map shell types (CMD/pwsh/PowerShell/WSL, `%COMSPEC%`, WSL path translation)
  to a command line, reusing `TCmdUtils`.

Acceptance criteria:
- [ ] Starts CMD/pwsh/PowerShell/WSL through ConPTY
- [ ] WriteInput and SendInterrupt work against a live shell
- [ ] Clean shutdown with no orphaned processes

---
Title: ConPTY: wire TframeCmdShell to ITerminalProcess
Issue: #58
Status: closed
Labels: enhancement, conpty
Body:
Phase 1. Frame depends on the abstraction, not `TCmdShellProcess` directly.
Effort: M. Verify: manual.

Implementation notes:
- Replace direct `TCmdShellProcess` usage with `ITerminalProcess`.
- ConPTY output still flows through the existing append renderer at this stage
  (expected; gated off Auto until the renderer ships).

Acceptance criteria:
- [ ] Frame drives either backend via the interface
- [ ] Legacy path is visually unchanged
- [ ] Demo can force a backend for testing

---
Title: ConPTY: backend selection and fallback resolution logic
Issue: #59
Status: closed
Labels: enhancement, conpty
Body:
Phase 2. Pure resolution logic, separate from UI. Effort: S. Verify: unit.

Implementation notes:
- Resolve Auto/ConPTY/Legacy to a concrete backend given availability.
- Auto = legacy until the renderer ships; fall back to legacy when ConPTY is
  unavailable or `CreatePseudoConsole`/`CreateProcess` fails.

Acceptance criteria:
- [ ] Forced modes are honored
- [ ] Auto resolves to legacy for now
- [ ] Unavailable/failed ConPTY falls back to legacy
- [ ] Unit tests cover the resolution matrix

---
Title: ConPTY: backend setting persistence and options-page UI
Issue: #60
Status: closed
Labels: enhancement, conpty
Body:
Phase 2. The persistence + UI half of backend selection. Effort: S. Verify: manual.

Implementation notes:
- Add the Auto/ConPTY/Legacy setting to `TDelphiTerminalSettings` with registry
  load/save.
- Add the control to the options page; surface ConPTY startup failure in the tab
  output, consistent with missing-shell handling.

Acceptance criteria:
- [ ] Setting persists across sessions
- [ ] Options page exposes the choice
- [ ] Startup failure is surfaced in the tab

---
Title: ConPTY: TScreenBuffer core (cells, cursor, attributes, write, erase)
Issue: #61
Status: closed
Labels: enhancement, conpty
Body:
Phase 3. Pure cell-grid model. Effort: M. Verify: unit.

Implementation notes:
- Cells: char, foreground, background, style (bold/italic/underline/inverse).
- Cursor row/col, current attributes, basic write and erase.

Acceptance criteria:
- [ ] Writing text populates the expected cells
- [ ] Cursor movement overwrites cells
- [ ] Erase clears the expected ranges
- [ ] Unit-tested

---
Title: ConPTY: TScreenBuffer scroll region, alt screen, scrollback, resize
Issue: #62
Status: closed
Labels: enhancement, conpty
Body:
Phase 3. Effort: M. Verify: unit.

Implementation notes:
- Scroll region; main/alt screen swap; scrollback kept separate from the active
  screen; resize (preserve visible content, clamp cursor, invalidate rows);
  dirty-row tracking.

Acceptance criteria:
- [ ] Alt-screen enter/exit swaps buffers
- [ ] Scroll region behaves correctly
- [ ] Resize preserves sane content and clamps the cursor
- [ ] Unit-tested

---
Title: ConPTY: VT parser core (C0 controls, SGR, escape-boundary buffering)
Issue: #63
Status: closed
Labels: enhancement, conpty
Body:
Phase 4. Consumes string chunks from the reader and mutates `TScreenBuffer`.
Effort: M. Verify: unit.

Implementation notes:
- C0: LF/CR/BS/TAB/BEL. SGR: reset/bold/underline/inverse, 16 / 256 / truecolor.
- Buffer incomplete escape sequences across chunks (separate from the reader's
  UTF-8 buffering). Consume unknown sequences harmlessly.

Acceptance criteria:
- [ ] C0 and SGR update the model correctly
- [ ] Escape sequence split across reads is handled
- [ ] Unknown sequences produce no visible garbage
- [ ] Unit-tested

---
Title: ConPTY: VT parser cursor movement, erase, scroll
Issue: #64
Status: closed
Labels: enhancement, conpty
Body:
Phase 4. Effort: M. Verify: unit.

Implementation notes:
- Cursor: CUU/CUD/CUF/CUB, CUP/HVP, save/restore. Erase: EL/ED. Scroll:
  index/reverse-index, scroll region.

Acceptance criteria:
- [ ] Cursor commands move as specified
- [ ] Erase commands clear the expected regions
- [ ] Scrolling behaves within the region
- [ ] Unit-tested

---
Title: ConPTY: VT parser OSC and DEC private modes
Issue: #65
Status: closed
Labels: enhancement, conpty
Body:
Phase 4. Effort: M. Verify: unit.

Implementation notes:
- OSC: window title (buffered across reads). DEC private modes: cursor
  show/hide, alt screen `?1049`, bracketed paste `?2004`.

Acceptance criteria:
- [ ] Title sequences are captured
- [ ] Alt-screen and bracketed-paste modes toggle model state
- [ ] Cursor visibility is honored
- [ ] Unit-tested

---
Title: ConPTY: evaluate prebuilt Delphi VT render controls (spike)
Issue: #66
Status: closed
Labels: enhancement, conpty, research
Body:
Phase 5. Time-boxed research before building `TTerminalView` from scratch.
Effort: S. Verify: research.

Implementation notes:
- Survey MIT/BSD-licensed Delphi VT grid/renderer controls; assess license, VCL
  fit, and maturity. The screen model and parser are ours regardless; only the
  paint control is in question. Default outcome: build from scratch.
- Deliverable: a short build-vs-reuse decision note.

Acceptance criteria:
- [ ] Candidates surveyed with license/fit notes
- [ ] Documented build-vs-reuse decision

---
Title: ConPTY: TTerminalView painting (metrics, dirty rows, cursor)
Issue: #67
Status: closed
Labels: enhancement, conpty
Body:
Phase 5. Effort: L. Verify: manual.

Implementation notes:
- `TCustomControl`; monospace metrics (cell width/height/baseline); paint dirty
  rows (background rects, attribute-grouped text runs, cursor).

Acceptance criteria:
- [ ] Renders the screen model with correct colors and attributes
- [ ] Cursor is drawn correctly
- [ ] Large output stays responsive via dirty-row painting

---
Title: ConPTY: TTerminalView scrollback, selection/copy, context menu
Issue: #68
Status: closed
Labels: enhancement, conpty
Body:
Phase 5. Effort: M. Verify: manual.

Implementation notes:
- Vertical scrollback; mouse selection -> clipboard copy; context menu
  (Copy/Paste/Clear/Stop).

Acceptance criteria:
- [ ] Scrollback scrolls history
- [ ] Mouse selection copies text
- [ ] Context menu actions work

---
Title: ConPTY: frame integration of TTerminalView and resize wiring
Issue: #69
Status: closed
Labels: enhancement, conpty
Body:
Phase 5. Effort: M. Verify: manual.

Implementation notes:
- In ConPTY mode host `TTerminalView` instead of `TRichEdit`; RichEdit stays for
  legacy.
- Font/size change or control resize -> compute cols/rows from metrics ->
  `TConPty.Resize`.

Acceptance criteria:
- [ ] ConPTY mode uses TTerminalView; legacy uses RichEdit
- [ ] Resizing the control reflows the shell
- [ ] No glyph overlap at configured font sizes

---
Title: ConPTY: raw input path (key to VT, focusable view)
Issue: #70
Status: closed
Labels: enhancement, conpty
Body:
Phase 6. Effort: M. Verify: manual.

Implementation notes:
- Make `TTerminalView` focusable; hide the line-entry `TEdit` in ConPTY mode.
- Key -> VT: printables, Enter, Backspace, Tab, Esc, Ctrl+C/L/D, arrows,
  Home/End, Delete, PgUp/PgDn.
- Command history stays legacy-line-mode only.

Acceptance criteria:
- [ ] Interactive cmd/pwsh line editing works
- [ ] Ctrl+C interrupts the foreground application
- [ ] Line-entry TEdit hidden in ConPTY mode

---
Title: ConPTY: paste and bracketed paste
Issue: #71
Status: closed
Labels: enhancement, conpty
Body:
Phase 6. Effort: S. Verify: manual.

Implementation notes:
- Paste clipboard text -> `InputWrite`; wrap with `ESC[200~`/`ESC[201~` when
  bracketed-paste mode is active.

Acceptance criteria:
- [ ] Paste inserts at the shell/app prompt
- [ ] Bracketed paste is used when the app requests it

---
Title: ConPTY: interrupt, restart, and teardown semantics
Issue: #72
Status: closed
Labels: enhancement, conpty
Body:
Phase 7. Effort: M. Verify: manual.

Implementation notes:
- Stop -> `#3`; double-Stop within an interval -> restart; restart after exit;
  reconcile with the existing "Press Enter to restart" UX.
- Clear parser/screen/queue state on restart.
- Dock-form cleanup stops all terminals before settings release.

Acceptance criteria:
- [ ] `ping localhost -t` interrupts and returns to the prompt
- [ ] Exit shows restart UI; restart clears state
- [ ] IDE close / package unload does not hang

---
Title: ConPTY: flip Auto to ConPTY on supported systems
Issue: #73
Status: closed
Labels: enhancement, conpty
Body:
Phase 8. Effort: S. Verify: manual.

Implementation notes:
- `Auto` -> ConPTY when available (1903+), legacy otherwise; fallback intact.

Acceptance criteria:
- [ ] Auto uses ConPTY on supported systems
- [ ] Auto falls back cleanly when ConPTY is unavailable

---
Title: ConPTY: Claude Code acceptance, documentation, and 2.0 release
Issue: #74
Status: closed
Labels: enhancement, conpty
Body:
Phase 8. Effort: M. Verify: manual.

Implementation notes:
- Acceptance matrix in the demo AND the plugin: `claude -p "..."` (arg and
  piped); interactive `claude` (type, Backspace, arrows, paste, Ctrl+C).
- Troubleshooting doc: legacy backend is not a true terminal; interactive TUIs
  require ConPTY (Windows 10 1903+).
- Update README + CHANGELOG; bump version to 2.0.

Acceptance criteria:
- [ ] `claude -p` works in both hosts
- [ ] Interactive `claude` works (input, paste, Ctrl+C)
- [ ] Docs updated; version bumped to 2.0

---
Title: ConPTY: unify TConPtyReader and legacy TPipeReaderThread (post-2.0)
Issue: #75
Status: closed
Labels: enhancement, conpty, tech-debt
Body:
Post-2.0 follow-up. Fold both readers into a single parameterized reader once
ConPTY is proven end to end. Effort: M. Verify: unit/integration.

Acceptance criteria:
- [ ] A single shared reader is used by both backends
- [ ] Existing tests pass

---
Title: ConPTY: environment-block support (post-2.0, if needed)
Issue: #76
Labels: enhancement, conpty
Body:
Post-2.0 follow-up. `TConPty.Start` currently inherits the parent environment.
Add an optional custom environment block if a use case needs it (parallel to the
legacy `BuildEnvironmentBlock` / `NO_COLOR` handling). Effort: S.

Acceptance criteria:
- [ ] An optional environment block is passed to the child when configured

---
Title: ConPTY: performance pass on large output / rapid redraws (post-2.0)
Issue: #77
Status: closed
Labels: enhancement, conpty
Body:
Post-2.0 follow-up. Profile and optimize the reader -> parser -> renderer path
under heavy output and rapid redraws. Effort: M. Verify: manual.

Acceptance criteria:
- [ ] Sustained heavy output stays responsive
- [ ] No unbounded memory growth

---
Title: ConPTY: deferred line wrap (DECAWM) and reliable PTY size sync
Issue: #78
Status: closed
Labels: bug, conpty
Body:
Found during #70 acceptance testing. In ConPTY mode, editing a line that reaches
the right margin corrupts the display. Repro (VCL demo, pwsh tab): type `get` so
PSReadLine shows an inline prediction that fills past the right edge, then press
Backspace -- a spurious newline is inserted and the redraw is misplaced (the
prompt keeps `get` and the remainder of the prediction drops to a new line).

Two root causes:

1. Immediate vs. deferred wrap. `TScreenBuffer.PutChar` advances to the next
   line the instant the cursor passes the last column. Real terminals (DECAWM)
   use a pending-wrap state: after writing the last column the cursor stays
   there, and the wrap happens only when the next printable arrives; any cursor
   movement (CR, BS, CUP, LF) cancels the pending wrap. Without this, an app that
   fills a line exactly and then repositions the cursor desyncs by one row, so
   its erase/move sequences land on the wrong line.

2. PTY size can remain at the 80x24 startup fallback. The session starts before
   the view has its real size; the screen buffer can be resized without the
   pseudoconsole (or a later sync early-returns because the buffer already
   matches), leaving the shell wrapping at 80 columns in a wider window.

Fix: implement pending-wrap in the screen model (cleared on cursor movement /
line feed); track the last size sent to the pseudoconsole and always push the
current view size when it differs, plus a post-layout size sync after start.

Acceptance criteria:
- [ ] Editing a wrapped prediction line in pwsh (type + Backspace) no longer inserts spurious newlines
- [ ] The shell wraps at the view's actual width; resizing reflows correctly
- [ ] Screen-model unit tests cover deferred wrap and its cancellation on cursor movement

---
Title: ConPTY: in-place line-editing CSIs (ECH/ICH/DCH/IL/DL)
Issue: #79
Status: closed
Labels: enhancement, conpty
Body:
Follow-up to #78. A raw-stream capture of pwsh confirmed that PSReadLine blanks
the continuation of a wrapped prediction with ECH (`ESC[nX`, observed as
`\e[51X`), which the VT parser does not implement -- so stale glyphs linger on
the wrapped line when editing mid-line. Cosmetic (no state corruption), but it
exposes a fidelity gap that full-screen TUIs (vim, less, Claude Code) also hit.

Implement the standard in-place editing CSIs in TScreenBuffer + TVTParser:
- ECH (`ESC[Ps X`) -- erase Ps chars in place, cursor unchanged (confirmed needed)
- ICH (`ESC[Ps @`) -- insert Ps blanks, shifting the rest of the line right
- DCH (`ESC[Ps P`) -- delete Ps chars, shifting the rest of the line left
- IL  (`ESC[Ps L`) -- insert Ps blank lines within the scroll region
- DL  (`ESC[Ps M`) -- delete Ps lines within the scroll region

Acceptance criteria:
- [ ] Editing a wrapped prediction line leaves no stale glyphs on the continuation line
- [ ] ECH/ICH/DCH/IL/DL implemented with unit tests

---
Title: ConPTY: win32-input-mode input encoding (rich keys + pwsh block paste)
Issue: #80
Status: closed
Labels: enhancement, conpty
Body:
CLOSED as not-needed: the block-paste premise was disproven. pwsh runs multi-line
paste line-by-line identically in conhost, Windows Terminal, and our VCLDemo (it
never enables ?2004 in any host), so there is no fidelity gap to close. No key
input problems have been observed either. Reopen with concrete evidence if a real
win32-input-mode need surfaces.

Captured during #71 verification. Windows console line editors (cmd, pwsh,
PowerShell) request win32-input-mode (`ESC[?9001h`) and do NOT enable classic
bracketed paste (`ESC[?2004h`); only Linux apps under WSL/bash enable `?2004`.
We currently send legacy VT input, which ConPTY converts to console key events.

Consequences:
- pwsh/PSReadLine cannot tell pasted text from typing, so multi-line paste runs
  line-by-line instead of inserting as an editable block (differs from Windows
  Terminal). Paste into WSL/bash already works as a block because bash enables
  `?2004` (handled by #71).
- Legacy VT input loses fidelity that win32-input-mode conveys: key-up events,
  Enter vs Ctrl+M distinction, richer modifier reporting that some TUIs want.

Implement win32-input-mode: when the app enables `ESC[?9001h`, encode keyboard
and paste input as win32-input-mode records
(`ESC[<Vk>;<Sc>;<Uc>;<Kd>;<Cs>;<Rc>_`) instead of legacy VT, and gate on it
(fall back to legacy VT when not requested). This yields pwsh block-paste and
better key fidelity. Effort: L. Verify: manual (capture-assisted). Optional for
the 2.0 milestone -- Claude Code and interactive editing already work without it.

Acceptance criteria:
- [ ] When the app requests ?9001h, keyboard input is sent as win32-input-mode records; legacy VT otherwise
- [ ] Multi-line paste into pwsh inserts as an editable block (no premature execution)
- [ ] Existing typing / editing / TUI behavior is unaffected in both modes

---
Title: Demo: data-driven ConPTY perf profiling harness (interactive + PROFILE_COMMANDS batch)
Issue: #81
Status: closed
Labels: enhancement, conpty
Body:
Prep work for #77 (ConPTY performance pass). Add a repeatable, data-driven
performance profiling harness to the VCL demo so the same commands can be
measured over time / across builds.

Single source of truth: a `TProfiledCommand` record (UniqueID, Description,
Shell, Command, EndAfterSeconds, CaptureMemory, Cols, Rows, Repeats) and a
predefined const array. The dropdown (interactive) and the batch runner both
iterate it.

Measurement: per run, capture chars received, elapsed, chars/sec, chunk count,
render-pass count, and memory (start/peak/end). The "done" signal is the process
exit: bounded commands (EndAfterSeconds = 0) append `exit` and report on process
exit; timed commands (> 0) run a fixed window then terminate and report. A
generous safety cap covers bounded commands that hang. Captured via tiny frame
hooks (OnOutputReceived, OnRenderPass, OnShellExited) plus a public
RunCommandLine and a SetFixedTerminalSize override for reproducibility.

Interactive mode: command dropdown + Run/Stop + a results memo, appending to a
rolling CSV. Batch mode ({$DEFINE PROFILE_COMMANDS}, demo-only): on launch,
auto-run the full array x Repeats sequentially, write
ProfileResults{yyyymmdd-hhnnss}.csv, then terminate the app (scriptable). Skip
unavailable shells (reason=unavailable); exit code reflects success.

CSV is keyed by UniqueID + version (read from the exe version resource) so runs
can be diffed by version; a `reason` column (exit/timed/capped/unavailable) flags
truncated runs. All harness/batch/CSV logic lives in the demo; the plugin is
untouched.

Acceptance criteria:
- [ ] Interactive: pick a command, Run, see stats; result appended to the CSV
- [ ] Batch build auto-runs the full list and writes a timestamped CSV, then exits
- [ ] Bounded (exit-signaled) and timed (windowed) commands both report correctly

---
Title: Saved commands: fix ConPTY run and preview-to-edit mechanics
Issue: #82
Status: closed
Labels: bug, conpty
Body:
Tracing the palette execution path (`Delphi.Terminal.Plugin.DockForm.pas`
`HandleCommandPaletteRequested`, lines 479-584) into the frame surfaced two
correctness defects in the ConPTY backend that predate the 2.0 merge. Neither
is about the injection-hazard policy (see the separate suppression ticket) --
these are plain mechanical bugs.

1. Run-now sends the wrong line ending. `TframeCmdShell.SendUserCommand`
   (`source/Delphi.Terminal.Frame.CmdShell.pas:860`) unconditionally writes
   `AText + #13#10`. `RunCommandLine` (same unit, line 974) was already fixed
   to be backend-aware -- CR (`#13`) for ConPTY, CRLF (`#13#10`) for the legacy
   pipe -- as part of the #81 line-ending work, but `SendUserCommand` (the path
   the Ctrl+P palette uses for "run now", via `paRun`) was missed. In ConPTY the
   trailing LF is a second Enter at the prompt (stray blank line / re-prompt).

2. Preview-to-edit is a no-op in ConPTY. The palette's default action `paEdit`
   ("Enter previews the command in the input field for editing") calls
   `TframeCmdShell.InsertCommandText` (line 870), which sets `FEditInput.Text`
   and focuses it. But in ConPTY mode the entire input panel is hidden
   (`FPanelInput.Visible := FBackendKind <> tbConPty`, line 900), so the text
   lands in a control the user cannot see, edit, or submit. The "edit before
   running" affordance silently does nothing in ConPTY.

Implementation notes:
- Make `SendUserCommand` use the same backend-aware terminator as
  `RunCommandLine` (CR for `tbConPty`, CRLF otherwise). Consider routing both
  through a single private helper so the two paths cannot diverge again.
- Give `paEdit` a working ConPTY path: write the command text to the PTY line
  editor WITHOUT the terminating CR, so the shell's own line editor shows it and
  the user edits inline and presses Enter themselves. (The hidden `TEdit` cannot
  serve this in ConPTY.) The suppression ticket gates whether this is allowed
  when a foreground app is active.

Acceptance criteria:
- [ ] Saved-command run-now (Ctrl+P, paRun) submits exactly one line in ConPTY (no stray blank line) and is unchanged in legacy
- [ ] Saved-command preview-to-edit (paEdit) places an editable command at the ConPTY prompt that the user can modify and submit
- [ ] Legacy backend behavior for both paths is unchanged
- [ ] Existing tests continue to pass

---
Title: Auto-CD applies to the legacy pipe backend only
Issue: #83
Status: closed
Labels: enhancement, conpty
Body:
Auto-CD-on-project-switch injects a `cd` command into the shell whenever the
active IDE project changes (`Delphi.Terminal.Plugin.DockForm.pas`
`HandleActiveProjectChanged`, line 371, calling `TframeCmdShell.SetWorkingDirectory`).
This made sense in the 1.0 line-command model, where the shell always sits at a
prompt and the user's own typing went into a separate input `TEdit`. Under the
ConPTY backend `WriteInput` is raw keystroke injection into the foreground
program: if Claude Code, a REPL, or a full-screen TUI is running, the `cd ...`
plus Enter is typed into THAT app, not a shell prompt. Even at a bare ConPTY
prompt it clobbers a half-typed command line, because there is no separate input
box anymore.

Decision: Auto-CD is a legacy-pipe-only feature. In ConPTY it is suppressed
entirely, and the config screen states the scope. (A future, safe ConPTY Auto-CD
gated on the OSC 133 prompt-state predicate is explicitly out of scope for this
ticket -- see that ticket.)

Note: ConPTY is the default backend in 2.0, so out of the box most users will
not get Auto-CD. This is the intended trade against ever injecting into an
interactive session.

Implementation notes:
- Gate the automatic call site only: in `HandleActiveProjectChanged`, skip
  frames whose `BackendKind <> tbLegacyPipe` (the frame already exposes
  `property BackendKind`, `Frame.CmdShell.pas:133`). Applies to both the
  active-tab and all-tabs modes.
- Do NOT change the Project Dir / File Dir toolbar buttons
  (`Frame.CmdShell.pas:754,767`) -- those call `SetWorkingDirectory` on an
  explicit user click and should keep working in both backends.
- Config screen: annotate the Auto-cd control (`FCboAutoCd`,
  `Delphi.Terminal.Plugin.OptionsFrame.pas:190-198`), e.g. caption
  "Auto-cd on project switch (Legacy Pipes only):" or a note label beside it.

Acceptance criteria:
- [ ] Switching the active project changes the working directory in legacy-pipe tabs only
- [ ] ConPTY tabs receive no injected cd on project switch (no keystrokes sent to a running interactive app)
- [ ] Project Dir / File Dir toolbar buttons still change directory in both backends
- [ ] The config screen states that Auto-cd applies to the Legacy Pipes backend only
- [ ] Existing tests continue to pass

---
Title: Saved commands: suppress injection when a ConPTY foreground app is active
Issue: #84
Status: closed
Labels: enhancement, conpty
Body:
Saved commands run via the Ctrl+P palette are written straight to the PTY
(`TframeCmdShell.SendUserCommand`). Like Auto-CD, in ConPTY this is keystroke
injection into whatever owns the foreground: if Claude Code / a REPL / vim is
running, the saved command -- authored for a shell, and already run through
`TCmdUtils.GetCDAndRunCommand` to build shell-specific `cd && run` syntax -- is
typed into that app instead, and the trailing Enter submits garbage.

Unlike Auto-CD this is user-initiated (the user opened the palette and chose
run), so the correct response when the shell is busy is a visible WARNING, not
silent suppression, and the feature must keep working in ConPTY (running a build
in a fresh pwsh prompt is a prime ConPTY use case) -- so the legacy-only
treatment used for Auto-CD is wrong here.

This ticket consumes the shared "shell is idle at a prompt" predicate delivered
by the OSC 133 prompt-state ticket (dependency). All programmatic writes to the
shell should route through that one predicate with per-feature policy; this
ticket wires the saved-command paths.

Implementation notes:
- Depends on the OSC 133 prompt-state predicate ticket for the idle check.
- Run-now (`SendUserCommand`) and preview-to-edit (`InsertCommandText`, ConPTY
  path): when the predicate reports the shell is NOT idle (a foreground child /
  TUI is active), do not inject; instead surface a message via
  `TframeCmdShell.ShowMessage` (e.g. "[delphi-terminal] Cannot run a saved
  command while an interactive program is active -- stop it first").
- Do NOT gate ordinary paste (`PasteToShell`) -- pasting into whatever is focused
  is normal terminal behavior.
- Legacy backend is unaffected (always at a prompt; separate input box).

Acceptance criteria:
- [ ] Running a saved command at an idle ConPTY prompt works as today
- [ ] Running a saved command while an interactive app is foreground shows a warning and injects nothing
- [ ] The same guard applies to the preview-to-edit path
- [ ] Paste is not affected by the guard
- [ ] Legacy backend behavior is unchanged
- [ ] Unit tests cover the idle/busy decision where feasible

---
Title: ConPTY: OSC 133 shell-integration prompt-state predicate
Issue: #85
Status: closed
Labels: enhancement, conpty
Body:
delphi-terminal writes to the shell programmatically from several places, and in
ConPTY every one of them shares a single precondition: the shell prompt must be
the foreground, not an interactive child. This ticket builds ONE shared
"safe to inject" gate that all of those paths ride. It is NOT a "bring back
Auto-CD" ticket, and the predicate must not be implemented per feature -- each
consumer just calls the one gate.

Riders on the single gate (each keeps its own policy, but they share this one
idle check -- do not reimplement it):
- Saved-command run / preview (#84) -- user-initiated; warn + do not inject when
  not idle.
- Project Dir / File Dir toolbar buttons (#86) -- user-initiated; same policy as
  saved-command run.
- ConPTY Auto-CD -- #83 made Auto-CD legacy-only precisely because there was no
  idle gate. Once this gate exists, a ConPTY tab may auto-cd only when the gate
  reports idle. This is a rider on the gate, not a separate "re-enable Auto-CD"
  feature.
- Paste (`PasteToShell`) is deliberately NOT gated -- pasting into whatever is
  focused is normal terminal behavior.

Primary signal: OSC 133 semantic prompt markers (`ESC]133;A` prompt start,
`;B` command-input start, `;C` command executed, `;D` command finished). The VT
parser already buffers and dispatches OSC (`source/Delphi.Terminal.VTParser.pas`
`DispatchOSC`, line 261, handling Ps 0/2 titles); add a `133` case that tracks
prompt state on the screen model. "Idle at a prompt" == last saw A/B and not yet
C. Because the markers ride the PTY stream from inside the shell, this works for
WSL, where a Windows-side process/Job heuristic cannot see the in-VM child.

OSC 133 is emitted by a shell prompt hook, not by the shell inherently -- none of
cmd/pwsh/PowerShell/WSL send it out of the box (needs PSReadLine/profile snippet,
bashrc integration, clink, etc.). So define the fallback when no 133 markers have
been observed this session. Options to weigh during implementation:
- Job Object active-process count as a fallback busy check (query
  `JOBOBJECT_BASIC_ACCOUNTING_INFORMATION.ActiveProcesses` -- only
  `SetInformationJobObject` is currently declared in `source/WinAPI.ConPty.pas`;
  add `QueryInformationJobObject` + the accounting struct). Shell-only == 1 ==
  idle. Blind spot: WSL (child runs in the VM, not the Windows Job), so pair it
  with the alt-screen flag and prefer OSC 133 when present.
- Alt-screen active (`FScreen` `?1049`) is always a "busy" signal regardless of
  markers.

Expose the predicate as a single method/property the frame and dock form can
call (e.g. `TScreenBuffer`/`TVTParser` prompt-state + a frame-level
`ShellIsIdleAtPrompt`). Half-typed-line note: with 133 present we can also track
"has the user sent a keystroke since the last 133;B" for a stricter idle check;
capture that as a refinement, not a hard requirement.

Implementation notes:
- Parse OSC 133 A/B/C/D in `DispatchOSC`; store prompt state on the screen model.
- Decide and implement the no-marker fallback (Job accounting +/- alt-screen).
- Expose ONE shared idle gate and wire every injection path to it: the
  saved-command paths (#84), the Project/File Dir buttons (#86), and ConPTY
  Auto-CD. Do not duplicate the predicate per feature.

Acceptance criteria:
- [ ] OSC 133 A/B/C/D markers update a prompt-state on the screen model
- [ ] A single shared "shell idle at prompt" gate is exposed for the frame/dock form and consumed by all injection paths (no per-feature reimplementation)
- [ ] When markers are present, an executing command (post-133;C) reports not-idle; a fresh prompt reports idle -- including under WSL
- [ ] A defined fallback covers shells that emit no 133 markers
- [ ] Alt-screen active always reports not-idle
- [ ] Saved commands (#84), the Project/File Dir buttons (#86), and ConPTY Auto-CD all gate on this one predicate
- [ ] Unit tests cover marker parsing and the idle/busy predicate

---
Title: ConPTY: Project Dir / File Dir buttons inject into the foreground app (gate + line ending)
Issue: #86
Status: closed
Labels: bug, conpty
Body:
The Project Dir and File Dir toolbar buttons call
`TframeCmdShell.SetWorkingDirectory` (`source/Delphi.Terminal.Frame.CmdShell.pas`),
which writes the cd straight to the PTY: `FProcess.WriteInput(Cmd + #13#10)`
(around line 780). #83 made the AUTOMATIC project-switch auto-cd legacy-only, but
the manual buttons still call `SetWorkingDirectory` in ConPTY, so they remain a
live injection hazard: clicking Project Dir / File Dir while Claude Code, a REPL,
or any TUI is running types `cd ...` into that program instead of a shell prompt.

These buttons are user-initiated, so -- exactly like saved-command run (#84) --
the right response when the shell is busy is a visible warning, not silent
suppression, and they must keep working in ConPTY at an idle prompt. They are
another rider on the single "safe to inject" gate from #85, with the same policy
as saved-command run (they are NOT a legacy-only feature like automatic Auto-CD).

Second, a latent line-ending defect on the same path: `SetWorkingDirectory`
hardcodes `#13#10`. In ConPTY that CR+LF is a stray extra Enter -- the #82 class
of bug, which #82 did not touch because it only fixed the palette methods. Route
the write through the `SubmitLineToShell` helper added in #82 (CR for ConPTY,
CRLF for legacy) so the terminator is backend-correct. Auto-cd's legacy callers
are unaffected (CRLF is right there).

Depends on #85 (the shared idle gate). Sibling of #84; should land in the same
pass, before the 2.0 release.

Implementation notes:
- Gate the ConPTY write in `SetWorkingDirectory` on the #85 idle predicate: when
  not idle, do not inject; surface a message via `TframeCmdShell.ShowMessage`
  (e.g. "[delphi-terminal] Cannot change directory while an interactive program
  is active -- stop it first").
- Replace the hardcoded `Cmd + #13#10` write with `SubmitLineToShell(Cmd)` so the
  terminator is backend-correct.
- Legacy backend behavior is unchanged; the buttons still cd at an idle ConPTY
  prompt.

Acceptance criteria:
- [ ] Clicking Project Dir / File Dir at an idle ConPTY prompt changes directory with a single line (no stray blank line)
- [ ] Clicking either button while an interactive app is foreground shows a warning and injects nothing
- [ ] Legacy backend behavior is unchanged
- [ ] The buttons ride the same #85 gate as saved commands (no separate idle logic)
- [ ] Existing tests continue to pass

---
Title: ConPTY idle gate: three-state + fail-safe hardening (follow-up to #85)
Issue: #87
Status: closed
Labels: bug, conpty
Body:
Review follow-up to #85. As first implemented the gate was a Boolean
(`TScreenBuffer.IsIdleAtPrompt` / `TframeCmdShell.ShellIsIdleAtPrompt`), and its
no-marker fallback returned True (idle) on "no alt-screen and no Windows child".
That is a false-idle -- the exact hazard #83 exists to prevent: a line-oriented
program (a REPL, or Claude Code) sitting at its own prompt has no alt-screen and
(for WSL) no Windows child, so it is indistinguishable from an idle shell. The
fallback can prove BUSY but can never prove IDLE, so it must never return idle.

Governing principle: fail safe (bias to false-busy). False-busy (refuse to inject
when actually idle) is a mild annoyance; false-idle (inject when actually busy) is
the hazard. Every ambiguous/unknown state must resolve to not-idle.

Make the gate three-state -- IDLE / BUSY / UNKNOWN -- not Boolean:
- IDLE: positively established; only OSC 133/633 asserts it (input window: after B,
  before C).
- BUSY: positively established (alt-screen active, Job child count > 1, or 133/633 C).
- UNKNOWN: no markers this session and nothing proves busy. Never collapses to IDLE.

OSC 133 FSM tightened: idle window is "seen B, not yet C" -- require B
(command-input start), not A. A is prompt-start (still rendering the prompt);
injecting in the A->B window races the prompt. Robust mapping: A -> prompt-start
(not idle), B -> input-start (idle-eligible), C -> executing (busy), D -> finished
(not idle). Any BUSY signal overrides an idle marker.

Also:
- Handle OSC 633 (VS Code superset) with the same A/B/C/D semantics -- near-free
  alongside the 133 case; ignore its other subcommands (E command-line, P property).
- Tolerate trailing params: split on ';', switch on the first sub-parameter (A/B/C/D),
  ignore the rest (133;D;<exit>, 133;A;<key=val>).
- Fallback honesty: alt-screen (?1049) -> hard BUSY (rides the PTY stream, so it
  covers WSL full-screen apps too). Job ActiveProcesses > 1 -> BUSY (catches non-WSL
  child REPLs/agents). Neither ever asserts IDLE. WSL blind spot: a line-oriented
  program inside the VM is invisible to the Windows Job and sets no alt-screen, so a
  WSL tab with no 133 this session is UNKNOWN, never IDLE (this falls out of the
  three-state model automatically).

Rider policy mapping (implemented by #83/#84/#86, recorded here for reference):
- Auto-CD (unsolicited): inject only on IDLE; UNKNOWN stays suppressed (continuity
  with #83 -- Auto-CD stays off in ConPTY until shell integration is present).
- Saved-command run and Project/File Dir buttons (user-initiated): IDLE -> inject;
  UNKNOWN -> #84 policy (warn-and-proceed/refuse); BUSY -> refuse/warn.
- Paste: ungated.

Notes verified for this codebase:
- Threading: OSC parsing runs on the main thread (FlushOutputBuffer <- render timer),
  and consumers (button clicks, Auto-CD events, palette) are also main-thread, so the
  prompt-state write and the gate read are same-thread -- no cross-thread publication
  or TOCTOU window. (If parsing ever moves off the main thread, revisit.)
- Per-tab: the gate is per-frame (own parser/screen/Job/markers); consumers must query
  the target tab's gate (e.g. Auto-CD all-tabs checks each tab; a shell-targeted saved
  command checks that shell's tab).
- Half-typed-line ("any keystroke since last B") remains a deferred refinement.

Implementation notes:
- Replace the Boolean with `TShellIdleState = (sisUnknown, sisIdle, sisBusy)`.
- `TScreenBuffer.IdleState(AForegroundChild)`: BUSY if alt-screen or PromptState C or
  AForegroundChild; IDLE only if PromptState B; else UNKNOWN.
- Add `633` alongside `133` in `DispatchOSC`.
- `TframeCmdShell.ShellIdleState: TShellIdleState` (legacy -> sisIdle).

Acceptance criteria:
- [ ] Gate is three-state (Idle / Busy / Unknown), not Boolean
- [ ] Only OSC 133/633 asserts Idle; the alt-screen/Job fallback only asserts Busy or leaves Unknown, never Idle
- [ ] Idle only in the input window (after B, before C); A and D are not Idle
- [ ] Any Busy signal (alt-screen, 133/633 C, Job child > 1) overrides an Idle marker
- [ ] OSC 633 A/B/C/D handled like 133; trailing params tolerated
- [ ] A WSL tab with no markers this session never returns Idle
- [ ] Unit tests cover the three-state matrix and the tightened FSM

---
Title: Idle gate hardening: document load-bearing child-count OR; lock alt-screen 47/1047 (review follow-up to #87)
Issue: #88
Status: closed
Labels: conpty, test
Body:
Verification/hardening pass on the #87 idle gate. Two of the three review items
turned out already-correct in the code; the value here is a regression-guard
comment and test lock-ins so they stay correct.

1. Restart reset -- VERIFIED, no change. The one item that could produce a real
   false-idle: a reused screen/parser carrying a stale B (command-input) marker
   across a shell restart would read IDLE before the new shell is ready. Confirmed
   safe: RestartShell -> StartShell -> EnsureConPtyModel sets
   `FScreen.PromptState := spsUnknown` on every (re)start (and the constructor
   inits spsUnknown), so a restarted or never-marked session is UNKNOWN, never
   IDLE. ClearOutput (Ctrl+L) intentionally does not reset PromptState (clearing
   the display does not move the shell off its prompt), and it is moot regardless
   since restart always re-inits via StartShell.

2. Child-count OR is load-bearing -- add a comment. `TScreenBuffer.IdleState`
   marks BUSY on AForegroundChild even when PromptState = B. That looks redundant
   once markers exist but guards a partial integration that emits B and never C:
   PromptState would stay at B while a command runs, and only the child count
   flags it. Comment it so a maintainer does not "simplify" the OR away and
   silently reopen the #87 false-idle. Also name the accepted tradeoffs so they
   are not "fixed": a legitimate background job trips child-count -> BUSY at an
   otherwise-idle prompt (accepted false-busy); and a hand-rolled B-only
   integration on a WSL tab running a line-oriented child is the one uncoverable
   combination (child in the VM, no alt-screen, PromptState stuck at B) -- a
   documented assumption, not a handled case.

3. Alt-screen completeness -- already handled; add tests. The hard-BUSY override
   needs the alt-screen flag set for all three alternate-screen modes (?1049,
   ?1047, ?47). DispatchPrivateMode already sets it for `47, 1047, 1049`
   together, but only ?1049 is tested. Add tests for ?47 and ?1047 so the older
   modes cannot regress out of the BUSY override.

Deferred/notes (no action now):
- 633;E (VS Code command-line report) is the natural exact input for the future
  strict-idle refinement ("is the input line empty since B"), with the
  keystroke-since-B heuristic as the fallback for 133-only integrations. We
  already parse and ignore 633;E today.
- User-facing warnings should distinguish BUSY ("a program is running") from
  UNKNOWN ("can't confirm the shell is at a prompt"). #84 only warns on BUSY so
  its message is already BUSY-specific; #86 should do the same, and any future
  consumer that warns on UNKNOWN must use a distinct message.

Implementation notes:
- Strengthen the comment at the BUSY-override in `TScreenBuffer.IdleState`.
- Add VTParser tests: ?47 and ?1047 set/clear the alt-screen flag (parity with
  the existing ?1049 test).

Acceptance criteria:
- [ ] IdleState's BUSY-override comment explains the load-bearing child-count OR and the accepted tradeoffs
- [ ] Tests confirm ?47 and ?1047 (not only ?1049) set and clear the alt-screen flag
- [ ] Restart-reset behavior recorded as verified (no code change)
- [ ] All tests pass

---
Title: Retire the Auto-CD-on-project-switch feature
Issue: #89
Status: closed
Labels: enhancement, conpty
Body:
Remove the Auto-CD-on-active-project-switch feature entirely (reverses the
original #14). Rationale: as of 2.0 it is vestigial -- #83 made it legacy-pipe
only, and ConPTY is the default backend, so most users never get it. In the
legacy case it is noisy (a `cd` injected into every affected tab on every project
switch, e.g. constant switching in a project group). The Project Dir / File Dir
toolbar buttons already cover the use case on demand and are gate-safe (#86), so
the automatic behavior is redundant. Removing it also simplifies the config
screen and deletes a whole IDE notifier.

Scope (3 code files + docs; no test changes -- nothing tests auto-cd, and
ChangeDirectoryCommand stays because the buttons use it):
- DockForm: the IOTAIDENotifier (TDelphiTerminalIDENotifier) exists solely for
  auto-cd (BeforeCompile/AfterCompile are unused; FileNotification only calls
  HandleActiveProjectChanged), so remove the whole notifier, its register/
  unregister, HandleActiveProjectChanged, FLastProjectDir, FGroupOpening, and
  FNotifierIndex. Keep GetActiveProjectDir etc. (the palette's ${ProjectDir}
  resolution uses them).
- Settings: remove FAutoCdMode (property, init, registry load/save). Existing
  registry AutoCdMode values simply go unread -- harmless, no migration.
- OptionsFrame: remove FCboAutoCd, its label, the "Legacy Pipes only" note, and
  the LoadSettings/SaveSettings references.
- Docs: drop the auto-cd bullet from README.md and the auto-cd lifecycle mention
  from CLAUDE.md; note the removal in CHANGELOG.md.

Note: this also retires the potential future "gate-gated ConPTY Auto-CD" that
#85's idle gate could have enabled (it was listed as a possible rider). The gate
remains fully justified by saved commands (#84) and the working-dir buttons
(#86); if gate-gated auto-cd is ever wanted it would be re-added as a consumer.

Acceptance criteria:
- [ ] AutoCdMode removed from settings; no registry read/write of it remains
- [ ] The IDE notifier and HandleActiveProjectChanged are removed; switching the active project no longer injects cd into any tab
- [ ] The Auto-cd option (combo + note) is gone from Tools > Options
- [ ] Project Dir / File Dir buttons still work (both backends)
- [ ] Plugin builds; existing tests pass
- [ ] README, CLAUDE.md, and CHANGELOG updated

---
Title: Post-2.0 ConPTY idle-gate refinements (deferred from #84-#88)
Issue: #90
Status: closed
Labels: enhancement, conpty
Body:
Tracking ticket for refinements consciously deferred while building the ConPTY
injection-safety gate (#84-#88). Each item is independent and optional; captured
here so they are not lost in closed tickets. None blocks 2.0 -- the gate is
correct and verified as shipped.

1. Strict-idle refinement: "input line non-empty since last B" (from #85/#87/#88).
   Today IDLE is asserted for the whole 133/633 B..C window regardless of whether
   the user has started typing a command. A tighter signal would treat a non-empty
   input line as not-idle. Two data sources, in preference order:
   - OSC 633;E (VS Code integration) carries the current command line verbatim, so
     an empty E == empty prompt -- a clean, exact signal. We already parse and
     ignore 633;E today; this would consume it.
   - "any keystroke forwarded since the last B" as a coarse over-approximation
     (treats typed-then-deleted as still-typed -- the safe direction) for 133-only
     integrations without E.
   Lower value now that unsolicited Auto-CD is retired (#89); it only sharpens
   user-initiated refusals, where the user already chose the moment.

2. WSL-without-133 blind spot (from #84/#85/#87). A line-oriented program running
   inside the WSL VM (a REPL, WSL-hosted Claude Code) is invisible to the Windows
   Job Object (only wsl.exe is Windows-side) and sets no alt-screen, so a WSL tab
   with no 133 markers reads as UNKNOWN. For user-initiated actions (saved
   commands #84, working-dir buttons #86) UNKNOWN proceeds, so injection can reach
   that program. Accepted tradeoff (user chose the moment), but candidate
   mitigations to evaluate: document/recommend OSC 133 bash integration in the WSL
   ~/.bashrc (flips the gate to BUSY over the PTY stream), and/or a WSL-side
   foreground-process probe. Full-screen WSL apps (vim) are already caught by
   alt-screen.

3. Non-intrusive busy warning (from #84/#86). WarnShellBusy surfaces the refusal
   via ShowMessage, which feeds the text through the VT parser into the live
   screen at the cursor -- so under a running TUI (e.g. Claude Code) the message
   lands mid-frame and overlaps its rendering (cosmetic, self-healing on the app's
   next repaint; no keystrokes reach the app). A tidier version would surface the
   refusal outside the terminal surface: a MessageBeep, or a transient toolbar /
   status flash.

4. Distinguish UNKNOWN vs BUSY in warnings (from #88). The three-state gate allows
   a more precise message: BUSY -> "a program is running"; UNKNOWN -> "can't
   confirm the shell is at a prompt". Currently moot -- consumers only warn on
   BUSY (UNKNOWN proceeds) -- but if any future consumer warns on UNKNOWN it
   should use a distinct message rather than reusing the BUSY text.

Acceptance criteria:
- [ ] Each item is either implemented or explicitly declined with a rationale (they are independent; partial completion is fine)

---
Title: Document recommended WSL OSC 133 shell integration for the idle gate
Issue: #91
Labels: documentation, conpty
Body:
Follow-up to #90 item 2 (the WSL-without-133 blind spot), split out so the
deferred documentation work is not lost.

A line-oriented program running inside the WSL VM (a REPL, a WSL-hosted Claude
Code) is invisible to the Windows Job Object -- only `wsl.exe` is Windows-side --
and it sets no alternate screen, so a WSL tab with no OSC 133 markers reads as
UNKNOWN. For user-initiated injection (saved commands #84, working-dir buttons
#86) UNKNOWN proceeds, so a saved command / `cd` can reach that program even
though it is not actually at a shell prompt. This is an accepted tradeoff (the
user chose the moment), but it is avoidable with shell integration.

If the user's WSL bash emits OSC 133 prompt markers, those markers travel over
the same PTY stream delphi-terminal already parses, so the gate correctly flips
to BUSY while a foreground program runs inside WSL -- closing the blind spot
with no Windows-side probe.

Scope (documentation only -- no code change):
- Add a short section (README and/or docs/) recommending OSC 133 bash
  integration in the WSL `~/.bashrc`, with a copy-pasteable snippet (e.g. the
  standard `PROMPT_COMMAND` / `PS0`/`PS1` sequence that emits `133;A`/`B`/`C`/`D`,
  or a pointer to a maintained integration script).
- Explain the benefit in terms of the idle gate: with markers present, running a
  saved command or clicking Project Dir / File Dir while something is active in
  WSL is refused instead of injected.
- Note the limitation this does NOT cover (a WSL program that emits no markers
  and no alt screen remains UNKNOWN) and that full-screen WSL apps (vim) are
  already caught by alt-screen regardless.

Explicitly out of scope (declined in #90): a WSL-side foreground-process probe
(spawning into the VM to inspect the foreground PID) -- fragile, latency-prone,
and racy.

Acceptance criteria:
- [ ] README/docs describe recommended WSL OSC 133 bash integration with a working snippet
- [ ] The idle-gate benefit and its remaining limitation are stated
- [ ] No code change required
---
Title: ConPTY: resizing while on the alternate screen corrupts the main screen on exit
Issue: #92
Status: closed
Labels: bug, conpty
Body:
`TScreenBuffer.Resize` rebuilds `FCells` for the new geometry but never touches
`FMainCells`, the snapshot taken by `EnterAltScreen`. `ExitAltScreen` then does
`FCells := Copy(FMainCells, ...)`, restoring an array sized for the OLD
`FCols`/`FRows` while the buffer's current `FCols`/`FRows` are the new ones.
`IndexOf(ACol, ARow) = ARow * FCols + ACol` is then computed against the new
stride over an array built for the old one.

Repro:
1. ConPTY tab; run a full-screen app so it enters the alternate screen (`vim` or
   `htop` under WSL, or Claude Code).
2. Resize the dock panel or the IDE window while that app is on screen.
3. Quit the app so it leaves the alternate screen.

Result:
- Growing the panel: the restored array is shorter than `FCols * FRows`, so
  painting reads past its end (range error with range checks on, garbage or an
  access violation without).
- Shrinking the panel: no overflow, but the row stride changed, so the restored
  main screen renders sheared/offset.

Fix: rebuild `FMainCells` into the new geometry in `TScreenBuffer.Resize`,
preserving the overlapping top-left region exactly as the active buffer is
handled. When the alternate screen is not active `FMainCells` is empty and the
work is skipped.

Note, deliberately out of scope: `Resize` also resets the scroll region to the
full screen. That matches xterm, which drops DECSTBM margins on resize, so it is
intentional and should NOT be changed as part of this ticket.

Acceptance criteria:
- [ ] `TScreenBuffer.Resize` rebuilds `FMainCells` into the new geometry when the alternate screen is active
- [ ] Unit test: enter alt screen, resize larger, exit -- `Length(FCells) = Cols * Rows` and preserved main content is intact
- [ ] Unit test: same for a resize smaller, asserting rows are not sheared
- [ ] Manual: resize the panel with a full-screen app running, quit it, main screen renders correctly

---
Title: ConPTY: reply to cursor-position (DSR) and device-attributes (DA) queries
Issue: #93
Labels: enhancement, conpty
Body:
`TVTParser` parses input only -- it has no path to write back to the shell, so
terminal queries go unanswered. Two are common enough to matter:

- `CSI 6 n` (DSR, Device Status Report) -- reply `CSI <row> ; <col> R`
- `CSI c` (DA, primary Device Attributes) -- reply `CSI ? 1 ; 2 c`

Programs that probe the terminal (readline/bash line editing, TUI frameworks
measuring the cursor, progress renderers) either block waiting for a reply or
fall back to degraded rendering.

Implementation note: keep `TVTParser` VCL-free and process-free. Add a response
callback on the parser (an event or `TProc<string>`) that the frame wires to
`FProcess.WriteInput`. The parser must not reference the process directly -- that
separation is what keeps the unit testable in the console test host.

Report the cursor position 1-based from the parser's `FScreen`, reflecting the
active screen when the alternate screen is in use.

Acceptance criteria:
- [ ] `TVTParser` exposes a response callback and gains no dependency on the process or the VCL
- [ ] `CSI 6 n` emits `ESC[<row>;<col>R` with 1-based coordinates
- [ ] `CSI c` emits `ESC[?1;2c`
- [ ] The frame wires the callback to `FProcess.WriteInput` for the ConPTY backend
- [ ] Unit tests in `Test.Delphi.Terminal.VTParser` assert both replies, including after cursor movement
- [ ] Nothing is emitted when the callback is unassigned

---
Title: ConPTY: extend key-to-VT translation (function keys, modifiers, DECCKM)
Issue: #94
Labels: enhancement, conpty
Body:
`KeyToVT` in `Delphi.Terminal.KeyInput.pas` covers Enter, Backspace, Tab /
Shift+Tab, Esc, the arrows, Home/End, Insert/Delete, PgUp/PgDn and Ctrl+letter.
Keys an interactive program expects that we do not send today:

- F1..F12. Conventional xterm encoding: F1-F4 as `ESC O P/Q/R/S`, F5-F12 as
  `ESC [ 15/17/18/19/20/21/23/24 ~`.
- Modifier-encoded cursor and navigation keys. xterm passes the modifier as a
  parameter: `ESC [ 1 ; <m> <letter>` for arrows/Home/End and
  `ESC [ <n> ; <m> ~` for the tilde keys, where
  `m = 1 + 1*Shift + 2*Alt + 4*Ctrl`. Without this, Ctrl+Left, Shift+Arrow and
  Ctrl+Delete -- word motion and selection in most line editors -- do nothing.
- DECCKM, application cursor keys (DEC private mode 1). When the app sets it,
  arrows must be sent as `ESC O A..D` rather than `ESC [ A..D`. The mode is not
  tracked at all today: `DispatchPrivateMode` handles 25, 2004 and 47/1047/1049
  only. Add an `ApplicationCursorKeys` flag to `TScreenBuffer`, set from mode 1.
- Shift+Enter as `ESC CR`, and Ctrl/Alt+Backspace as `ESC DEL` (delete previous
  word) -- both common in CLI input boxes.

Keep `KeyInput.pas` pure: pass the DECCKM state in as a parameter rather than
letting the translation reach into the screen buffer.

Acceptance criteria:
- [ ] F1..F12 translated with the xterm encodings above
- [ ] Modifier-encoded arrows/Home/End and tilde keys, with `m = 1 + Shift + 2*Alt + 4*Ctrl`
- [ ] DEC private mode 1 tracked on `TScreenBuffer` and honored for the arrows
- [ ] Shift+Enter and Ctrl/Alt+Backspace translated
- [ ] `KeyToVT` stays VCL-free; DECCKM state arrives as a parameter
- [ ] Unit tests in `Test.Delphi.Terminal.KeyInput` cover each group in both cursor-key modes

---
Title: Terminal view: paste file paths, paste clipboard images, and accept dropped files
Issue: #95
Labels: enhancement, conpty
Body:
`TTerminalView` pastes plain text only. Three clipboard/drop behaviors that a
command-line workflow -- especially an interactive CLI that takes file arguments
-- leans on:

1. `CF_HDROP` paste: files copied in Explorer. Paste types their paths,
   space-separated, each quoted when it contains a space.
2. Clipboard image paste: a screenshot or snip. Save it to a temp `.png` and type
   that path. Tools that accept images take a path, not clipboard bytes, so this
   turns Win+Shift+S into a usable input step.
3. `WM_DROPFILES`: dragging files from Explorer onto the terminal types their
   paths, formatted as in (1).

All three should funnel through one "send these paths as text" helper and honor
bracketed paste (`FScreen.BracketedPaste`) the same way `BuildPasteSequence`
does, so the receiving program can distinguish paste from typing.

Notes:
- `DragAcceptFiles(Handle, True)` in `CreateWnd`, `False` in `DestroyWnd`.
- Image encoding needs `Vcl.Imaging.pngimage`. Keep it in `TerminalView` (the VCL
  layer) so the pure units stay VCL-free and headlessly testable.
- Reading the clipboard can fail while another process holds it open -- guard and
  no-op rather than letting an exception escape into the message loop.
- Temp PNG files accumulate. Decide whether to clean them up (e.g. on shell
  restart) or leave them to the OS temp sweep, and state the choice in the code.

Acceptance criteria:
- [ ] Pasting Explorer-copied files types their quoted, space-separated paths
- [ ] Pasting an image writes a temp `.png` and types its path
- [ ] Dropping files onto the terminal types their paths
- [ ] All three paths respect bracketed-paste mode
- [ ] Clipboard failures are swallowed, not raised
- [ ] `DragAcceptFiles` registered in `CreateWnd` and released in `DestroyWnd`
- [ ] No pure unit gains a VCL dependency

---
Title: ConPTY: report mouse events to the child (DEC modes 1000/1002/1003, SGR 1006)
Issue: #96
Labels: enhancement, conpty
Body:
The terminal never sends mouse input to the running program. `MouseDown` in
`TTerminalView` starts a local selection anchor and nothing else, and
`DispatchPrivateMode` handles only 25, 2004 and 47/1047/1049 -- the mouse
tracking modes are not recognised, so clicking inside `vim` (`:set mouse=a`),
`htop`, `nano -m` or any other mouse-aware TUI does nothing.

Confirming the gap: in a WSL tab run `printf '\033[?1000h\033[?1006h'; cat -v`
and click. A terminal that reports the mouse prints `^[[<0;12;5M` on press and
`^[[<0;12;5m` on release; we print nothing.

Modes to support:
- `1000` normal tracking -- press and release only
- `1002` button-event tracking -- adds motion while a button is held
- `1003` any-event tracking -- adds all motion
- `1006` SGR encoding (set/reset alongside the tracking mode)
- `1015` urxvt encoding is optional; `1005` (UTF-8) can be skipped

Encoding:
- Legacy/X10: `ESC [ M Cb Cx Cy`, each byte written as `32 + value`, coordinates
  1-based. Unrepresentable beyond column/row 223 -- drop the report rather than
  emit a wrapped coordinate.
- SGR (1006): `ESC [ < Cb ; Cx ; Cy M` for press and `... m` for release. No
  coordinate limit; prefer this whenever the app enabled it.
- Button codes: 0 left, 1 middle, 2 right; 3 is release in the legacy encoding
  (SGR reports the real button and distinguishes press/release by the final
  character). Wheel up 64, wheel down 65. Motion reports add 32.
- Modifier bits added to the button code: Shift 4, Alt 8, Ctrl 16.

Interaction with our local selection -- this is the part that needs care:
- While tracking is active, a left drag belongs to the program, not to our
  selection. Holding Shift must force local selection anyway, which is what
  xterm and Windows Terminal do and what users will reach for to copy text out
  of a TUI.
- Wheel: only route the wheel to the program when the alternate screen is
  active, and let Shift+wheel always scroll our scrollback. Programs that run on
  the main screen and enable tracking only to catch clicks ignore buttons 64/65,
  and forwarding the wheel to them makes our scrollback appear frozen.

Implementation shape:
- `TScreenBuffer` gains the tracking mode and an SGR-encoding flag; `TVTParser`
  sets them from modes 1000/1002/1003/1006.
- Put the encoding in a pure, VCL-free function (extend
  `Delphi.Terminal.KeyInput.pas` or add `Delphi.Terminal.MouseInput.pas`) taking
  button, modifiers, 1-based col/row, press/release and the encoding flag, and
  returning the byte sequence. That is what makes it unit-testable.
- `TTerminalView` does the pixel-to-cell hit test and surfaces the bytes through
  an event, following the existing Paste/Clear/Stop pattern; the frame writes
  them via `FProcess.WriteInput`.
- Legacy pipe backend is unaffected -- mouse reporting is ConPTY only.

Related: the same frame-side write-back wiring is needed for the DSR/DA reply
ticket.

Acceptance criteria:
- [ ] DEC modes 1000, 1002, 1003 and 1006 tracked on `TScreenBuffer` via `TVTParser`
- [ ] Press, release, drag and wheel reports emitted in both legacy and SGR encodings
- [ ] Modifier bits (Shift 4, Alt 8, Ctrl 16) applied to the button code
- [ ] Legacy encoding drops reports beyond column/row 223 rather than wrapping
- [ ] Shift forces local selection while tracking is active
- [ ] Wheel goes to the program only on the alternate screen; Shift+wheel always scrolls scrollback
- [ ] Encoding lives in a pure unit with a DUnitX fixture covering each mode, button and modifier combination
- [ ] Manual: `printf '\033[?1000h\033[?1006h'; cat -v` prints `^[[<...M`/`^[[<...m` on click; `vim` with `:set mouse=a` moves the cursor to the clicked line
