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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .llm/skills/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Agent Skills ([SKILL.md format](https://agentskills.io)) for specific tasks. Inv
| [context-aware-completion](./context-aware-completion/SKILL.md) | Register context-aware commands with CommandDefinition, gate execution by CommandExecutionContexts, and attach CommandCompletionProvider argument completion (staged providers, TryComplete, TerminalUI token cycling) in DxCommandTerminal. Use when writing commands that need argument completion or execution-context gating, wiring chained completions like item name then item action, or debugging why a command does not run or complete. |
| [custom-argument-parsing](./custom-argument-parsing/SKILL.md) | Parse CommandArg values with TryGet<T>, register custom CommandArgParser functions, and control input cleaning, delimiters, and quote handling sets. Use when writing command handlers that read arguments, adding parsers for new types (e.g. JSON), or fixing "failed to parse" behavior. |
| [input-system-integration](./input-system-integration/SKILL.md) | Wire DxCommandTerminal input - configurable keyboard hotkeys, Unity's new Input System / PlayerInput bindings, the HandlePrevious/HandleNext/ToggleSmall/ToggleFull/CompleteCommand/EnterCommand messages, and input precedence. Use when adding or changing terminal keybindings, PlayerInput wiring, or fixing input handling bugs (including WebGL). |
| [log-echo-contract](./log-echo-contract/SKILL.md) | Preserve the DxCommandTerminal log echo contract - both console surfaces write the typed line into Terminal.Buffer as TerminalLogType.Input before a command handler runs, so a handler that reads the log sees its own echo as the newest entry. Covers OutputLength, why a fixed -1/-2 offset is wrong, why the palette and the copy commands filter differently, why the log view must still SHOW echoes, and the test-helper trap that hides all of it. Use when writing or reviewing a command that reads the log, when a command returns its own name or a command line instead of a message, when touching trace/copy-last/copy-log/CommandPaletteUI.CollectOutput, or when a log-reading test passes against code the product never runs. |
| [log-echo-contract](./log-echo-contract/SKILL.md) | Preserve the DxCommandTerminal log echo contract - both console surfaces write the typed line into Terminal.Buffer as TerminalLogType.Input before a command handler runs, so a handler that reads the log sees its own echo as the newest entry, and a command that answers in the log writes console text that later readers must not count as output. Covers OutputLength, why a fixed -1/-2 offset is wrong, why the palette and the copy commands filter differently, why the log view must still SHOW echoes, why a search excludes the console's own replies by text rather than by type, why EnterCommand overwrites a handler's view state, and the test-helper traps that hide all of it. Use when writing or reviewing a command that reads or counts the log, when a command returns its own name or a command line instead of a message, when touching trace/copy-last/copy-log/find/clear-filter/CommandPaletteUI.CollectOutput, or when a log-reading test passes against code the product never runs. |
| [register-terminal-command](./register-terminal-command/SKILL.md) | Register terminal commands in DxCommandTerminal via RegisterCommandAttribute or Terminal.Shell.AddCommand, including name inference, arg count bounds, hint text, editor/development-only flags, and ignoring built-in commands. Use when adding console commands/cheats, changing command signatures, debugging why a command is not recognized, or extending the typed builder and argument-spec internals. |
| [run-terminal-tests](./run-terminal-tests/SKILL.md) | Write and run DxCommandTerminal PlayMode tests (Unity Test Runner, Tests/Runtime/, NUnit) following the repo's test style for CommandArg, CommandShell, and Terminal behavior. Use when adding tests, running the test suite, or debugging failing terminal tests. |
| [session-facade](./session-facade/SKILL.md) | Explain and preserve the DxCommandTerminal backend session nullability contract - TerminalSession.Current is never null, the Terminal facade getters (Buffer/Shell/History/AutoComplete) read null before bootstrap and after play-session reset, TerminalUI and CommandPaletteUI apply or bootstrap the session on enable, and command registration stays deferred to first use. Use when touching TerminalUI/CommandPaletteUI lifecycle, adding session consumers, writing bootstrap code, reviewing null-chain questions on Terminal accessors, or debugging why Terminal.Log or Terminal.Shell is null. |
Expand Down
79 changes: 78 additions & 1 deletion .llm/skills/log-echo-contract/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: log-echo-contract
description: Preserve the DxCommandTerminal log echo contract - both console surfaces write the typed line into Terminal.Buffer as TerminalLogType.Input before a command handler runs, so a handler that reads the log sees its own echo as the newest entry. Covers OutputLength, why a fixed -1/-2 offset is wrong, why the palette and the copy commands filter differently, why the log view must still SHOW echoes, and the test-helper trap that hides all of it. Use when writing or reviewing a command that reads the log, when a command returns its own name or a command line instead of a message, when touching trace/copy-last/copy-log/CommandPaletteUI.CollectOutput, or when a log-reading test passes against code the product never runs.
description: Preserve the DxCommandTerminal log echo contract - both console surfaces write the typed line into Terminal.Buffer as TerminalLogType.Input before a command handler runs, so a handler that reads the log sees its own echo as the newest entry, and a command that answers in the log writes console text that later readers must not count as output. Covers OutputLength, why a fixed -1/-2 offset is wrong, why the palette and the copy commands filter differently, why the log view must still SHOW echoes, why a search excludes the console's own replies by text rather than by type, why EnterCommand overwrites a handler's view state, and the test-helper traps that hide all of it. Use when writing or reviewing a command that reads or counts the log, when a command returns its own name or a command line instead of a message, when touching trace/copy-last/copy-log/find/clear-filter/CommandPaletteUI.CollectOutput, or when a log-reading test passes against code the product never runs.
metadata:
category: Feature
---
Expand Down Expand Up @@ -66,6 +66,74 @@ belongs on screen. The contract above is about reading the log programmatically
for a result, not about what is displayed. Do not "fix" the view by filtering
echoes out.

## The console's own replies are not output either

The other writer of non-game text is the console answering itself. A command
that must answer in the log (`#186`) writes ordinary log text, and that text is
in the window every later reader sees.

This bit the log search: a query that happened to be a word in the search's own
answer - `search`, `log`, `line`, `clear-filter` - matched the answer, so a
search that hit nothing reported a hit and every repeat added another. The
echo exclusion above does not help; a reply is a `Message` or a `Warning`, not
an `Input`.

**A search excludes the console's own lines by exact text, not by type.** The
types belong to the game - a `Warning` is what the developer is looking for, and
a `ShellMessage` is any `Terminal.Log` the game made - so no type means "the
console said this". `LogFilter.IgnoreOwnReply` is called from the same door
that logs (`TerminalUI.LogFindReply` / `LogFindWarning`), so a new reply cannot
be added without registering it. Register the exact string that is logged: a
message with format arguments reaches the log formatted and the filter holding
the format, and the two stop being the same line.

**Apply the exclusion to the denominator as well as the numerator.** Excluding
a line from the matches but leaving it in the total reports "20 of 240" and then
"20 of 241" on two runs of the same search. The matches hold; the number the
developer reads moves because the search said something. `LogFilter.Apply` skips
own replies before both counters for exactly this reason, and both halves are
asserted separately, because checking only the matches passes the broken shape.

Do not exclude the whole `Warning` type, and do not exclude trailing replies
positionally - a positional exclusion makes the count depend on whether
anything has been logged since, so the same search reports a different number
on the frame the developer runs their next command.

`copy-log` keeps console replies in its transcript on purpose (see the table
above): a transcript wants the record of what you did. The difference is that
`copy-log` reports no count the developer decides anything from, and the search
does.

## A handler's UI intent is overwritten by the code that ran it

`TerminalUI.EnterCommand` re-attaches the log tail and asks for a scroll to the
end **after** the handler returns, because running a command is a request for
its output:

```
Terminal.Log(Input, text); shell.RunCommand(text); // <- the handler sets the view
_logTail.Attach(); _needsScrollToEnd = true; // <- and then overrides it
```

A handler whose whole purpose is where the view ends up has to survive that.
`EnterCommand` skips the re-assert when a search jump is queued, and it is the
only site that can know, so the decision belongs there.

Two consequences for tests:

- **Dispatch through `EnterCommand`, not the shell**, for anything about where
the view ends up. `shell.RunCommand` never re-attaches, so a test that uses
it cannot see this class of bug at all.
- **Assert the state synchronously.** The jump is dropped once its budget runs
out, so a test that yields a frame or two before looking finds the flag
cleared on a *correct* build and passes straight over a broken one. Expose
the flag (`TerminalUI.FindScrollQueued`, `WantsScrollToEnd`) and read it in
the frame the call returns.

Do not expose the follower's `Detached` for this: it only flips when a scroll
is actually placed, so on a host with no laid-out view it reads the same whether
or not anything is wrong.

## The test-helper trap (this is how the bug survived two review rounds)

A test helper that calls `shell.RunCommand(line)` directly exercises a path the
Expand Down Expand Up @@ -99,6 +167,15 @@ When a command starts reading the log:
4. Check the other two consumers still agree: `CommandTrace`,
`CommandPaletteUI.CollectOutput`, `TerminalUI.RefreshLogs`.

When a command starts **counting** the log, or reading it to drive a view:

5. Which of the lines in the window are the console's own? Echoes (`Input`) and
the command's own replies. A count that includes either is a number the
developer will act on and be wrong by - and a count that includes either in
only one of its two halves is the same defect wearing a pass.
6. Does the handler's effect on the view survive the call that ran it? See
"A handler's UI intent is overwritten by the code that ran it".

## Related

- [session-facade](../session-facade/SKILL.md) - the nullability boundary on
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- `find <text>` searches the log view: it keeps only the lines holding the text, jumps to the first match, and reports how many lines matched out of how many the search ranged over. `find` with no argument steps to the next match and wraps at the end, F3 and Shift+F3 do the same from the keyboard, and `clear-filter` shows every line again. Until now a log too long to read had no way in but dragging a 10px scrollbar: with the default 256-entry buffer, finding one error meant paging through every line that was not it, and the log view is the only place a message ever appears. The count is the answer to "did my search hit", which a view showing some lines cannot give. A search that matched nothing reports that and stays set, so a typo does not silently put the whole log back on screen, and the line the search writes to say so is never one of its own results - a query that happened to be a word in that line (`search`, `log`, `clear-filter`) otherwise matched it, and the count grew with every repeat. A query is the arguments joined back into one string, so `find "two words"` searches for two words, and an empty or whitespace-only query is refused so it cannot drop the search already in place. The search runs over the text as the log shows it, so what is on screen is what can be found, and it ignores case. It does not find commands you ran: both console surfaces echo the typed line into the log as an `Input` entry before the handler runs, and a search that matched its own echo reported a hit for every query, including one that appears nowhere. `clear-console` does not drop the search - the view goes empty until the next `clear-filter` - and the query is recorded only as the `find` line in the log, so it is not recoverable once that line rotates out of the buffer.
- `copy-last` and `copy-log [n]` put the console log on the system clipboard: the newest line, or the last N, joined with newlines. Until now nothing could be read out of the console at all - the log view is the only place a message, a command echo, or a failing value ever appears, and none of it could be selected, so a developer who saw `Command 'give' threw ArgumentException` in a device build had to retype it or screenshot the game window. Both surfaces echo the line you typed into the log before the command runs, so a copy skips the echoes at the newest end: `copy-last` returns the line you were reading, not the word `copy-last`. An echo further back stays, because a transcript of a session wants the commands in it as much as the output. A copied line is the line the log shows, so the stack trace is not included (`trace` is how you read one) and copying 256 lines does not carry 256 traces. A clipboard write is a request a platform can decline - tvOS has none, and a browser may refuse without a user gesture - so the write is read back and compared, and a copy that did not happen says so instead of silently doing nothing. An empty log, a log holding only commands, a count that is not a number, and a count below one are each reported rather than ignored.
- The log scrolls with the keyboard while the command line holds focus. Page Up and Page Down page it by one viewport, and Ctrl+Home (Cmd+Home on macOS) and Ctrl+End reach its oldest and newest lines, so a developer looking for one error in a full 256-entry buffer no longer has to drag a 10px scrollbar. Home and End without the modifier stay with the command line, where they move the caret as a text field should, and every other key is untouched. Paging back detaches the tail exactly as scrolling back does, so new output no longer pulls the view to the end while it is being read; Ctrl+End is the way back to following. A page that reaches the end leaves the log following rather than parked a line short of an end that is still growing.
- `CommandLog.CopyTo(LogItem[] destination)` copies the log's visible window, oldest first, into a caller-owned array and returns how many entries it wrote. Reading `CommandLog.Logs` counts and then indexes as two separate reads, so a log written from another thread between them can be missed or, if the buffer was cleared or shrunk, throw. One `CopyTo` call is one consistent view. A destination shorter than the window truncates to its oldest entries, so size it for the largest buffer the session configures.
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,10 +479,31 @@ The command line holds panel focus for as long as the console is open, so the te
- **Page Up / Page Down** page the log by one viewport.
- **Ctrl+Home** and **Ctrl+End** (Cmd on macOS) jump to its oldest and newest lines.

Home and End without the modifier stay with the command line, where they move the caret as a text field should. Every other key is untouched - typing, history recall, completion, and closing work exactly as they did.
Home and End without the modifier stay with the command line, where they move the caret as a text field should. Every other key is untouched - typing, history recall, completion, and closing work exactly as they did - except F3 and Shift+F3, which step a log search while one is set (see [Searching the log](#searching-the-log)).

Paging back detaches the tail exactly as scrolling back does, so new output does not pull the view to the end while you are reading. Ctrl+End is the way back to following. A log that has not been laid out - a headless editor, or a view with no rendered Game view - has nothing to page through and stays where it is.

## Searching the log

The log holds the newest 256 lines by default and nothing else, so reaching one of them means reading past the rest. Two commands narrow the view and one steps through it:

- `find NullRef` shows only the lines holding the text, and jumps to the first one.
- `find` on its own steps to the next match, wrapping at the end; **F3** and **Shift+F3** do the same from the keyboard.
- `clear-filter` shows every line again.

Each reports what it did - `Showing 20 of 240 log lines.`, or `Match 3 of 80.` - so a search that hit nothing says so instead of leaving you looking at an empty log, and the search stays set so a typo does not silently put everything back. A query is the arguments joined back into one string, so `find "two words"` searches for two words. An empty query is refused and leaves the search you had alone. The count does not move because the search talked: its own answer is not one of the results, so the same search reports the same number however many times you run it.

The search is over the text as the log shows it, so what you can see is what you can find, and it ignores case: `nullref` reaches `NullReferenceException`.

Four limits worth knowing before you rely on it:

- It does not find the commands you ran, or the console's own answers. Both surfaces echo the line you typed into the log, and a search that matched its own echo would report a hit for every query, including one that appears nowhere; the same goes for the line the search writes when it tells you what it found. Use `copy-log` for a transcript with your commands in it.
- The query is only recorded as the `find` line in the log. Once that line rotates out of the buffer - or you run `clear-console` - nothing on screen says the view is filtered, and the query is gone. `clear-filter` is how you put the log back.
- `clear-console` does not drop the search. The view goes empty until the next `clear-filter` or a new `find`.
- F3 and Shift+F3 need the command line to hold focus, as the paging keys do, and the jump to a match needs a log view that has been laid out. A view with no rendered Game view filters but does not scroll.

Binding a hotkey to `f3` fires that binding as well while a search is set; the terminal reads the key independently of your bindings.

## Typing wins over a character binding

A binding that presses a character key is left to the field being typed into. While the command line or the palette search bar has focus, pressing the key types the character and does not run the binding - so with the defaults `` ` `` (toggle) and `` #` `` (full), those characters are typeable and the console key no longer closes an open console.
Expand Down
Loading
Loading