A ledger of API changes that are worth making but cannot be made now: the public
surface (config API, MCP tools, UINode) is additive-only within 2.x, so a
change that renames, removes or reshapes anything waits for 3.0. This is the
place to write such an idea down at the moment it is noticed, with the reasoning
attached, so the next major release starts from a reasoned list instead of a
memory.
Relationship to the GitHub issues: an issue tracks work that can be done now; an entry here is blocked by the compatibility policy itself, not by effort or priority. When a major release is actually planned, entries graduate into issues.
Each entry records: what changes, why the current shape is wrong, why it must wait, and what the migration looks like.
describe_screen → describe_windowWhat. Rename the MCP tool. No behavior change.
Why. The name says “screen”; the contract is one window: it resolves a
single window via app=/title= (focused window by default) and dumps that
window’s element tree. Its own description and the tool table in
ai-integration.md both say “a window’s element tree”.
describe_window states the contract and pairs naturally with list_windows.
Why it waits. The tool is released public surface. A clean rename breaks
both bundled skills, the docs, the tests, and any user-side prompts or notes
that name the tool. The additive alternative — shipping describe_window as an
alias — costs a permanent extra entry in every session’s tool list (context
tokens on every request) and a which-one-do-I-call ambiguity, which is worse
than the misnomer. Mitigating the wait: in MCP the description travels with the
name, so callers see “Read a window’s element tree” on every call — the
imprecision does not mislead in practice — and the old name does match the
authoring vocabulary (“read the screen before you write”) used throughout the
docs and skills.
Migration. Rename in keyhac/mcp/tools.py; update both skills,
ai-integration.md (§tool table, log examples) and
tests/test_mcp.py in the same commit. Keep the authoring-workflow prose
(“open the screen the action will work against”) — it describes operator
intent, not the tool name.
find_all / find_elements: an optional truncation-stats out-parameterWhat. An optional stats= out-parameter on UINode.find_all and
uitree.find_elements — a public TruncationStats (reported / cut_points
/ by_depth / by_budget / deepest) filled from the same walk that produced
the matches. The MCP find_elements no-match reply can then say whether the
search was cut short by the bounds, instead of today’s deliberately plain
no element matching ... (issue #76).
Why. An empty result cannot distinguish “absent” from “not within the
walk’s bounds”. The runtime diagnostic that re-walked the tree to recover the
truncation marks was reverted (PR #77): it doubled the AX work on every
no-match, and it described a second snapshot the search never saw. A stats
out-parameter has neither defect — one walk, describing the tree that was
actually searched. stats=None keeps every existing caller unchanged, and
UINode.find_all forwards **criteria, so it flows through without a
signature change there.
Why it waits. Not breaking — the parameter is additive — but the same
underlying cost this ledger exists for: publication freezes a public stats
class whose field meanings (under the DAG dedupe, roles= filtering, prune)
settle the moment they are documented, all to serve one MCP error message.
Until then the ambiguity is taught statically in the max_depth schema
description and pinned by test_a_no_match_stays_plain_even_when_the_walk_was_cut.
Two events would justify building it: a second consumer appearing (actions
using the deep-search pattern wanting “did my search see everything?” without
paying their own reread), or a major release settling the shape deliberately.
Issue #76 was closed in favor of this entry.
Migration. None — additive. Ship the class and parameter in
keyhac/core/uitree.py, thread them through UINode.find_all, and teach the
MCP no-match branch in keyhac/mcp/tools.py to read the stats; the pinning
test is replaced deliberately in the same change.
Action class — only ThreadedActionEverything user-facing calls the concept an action: the authoring skill is
keyhac-action-authoring, the reference is doc/action-api.md, the MCP tools
are list_actions / start_action / get_action_result. The only class is
ThreadedAction, and “action” is otherwise a role — whatever a key table
accepts, which is a string, a sequence of strings, a KeyTable, or any
callable. StartRecordingKeys, ToggleRecordingKeys and InputText are
actions in that sense and subclass nothing.
So “which subclass is enumerated?” is a question the vocabulary invites and cannot answer from its own names. Two shapes would fix it, and they are not the same change:
ThreadedAction → Action, on the grounds that the threaded
part is an implementation detail of the only shape that needs a base class.
Smallest, and it makes the enumerated set self-describing.Action as a base above ThreadedAction, so a fast action
that needs no worker thread has somewhere to live and can still be
enumerated. Larger, and it needs an answer for what stops a slow one being
written that way — the main thread services the hook and every PuiKit
window, so an action that blocks it freezes the keyboard.Why it waits. ThreadedAction is released public surface that every
existing config and every authored action subclasses by name. Either change
breaks all of them at once, and neither is worth a compatibility shim: an
alias would leave two names for one thing in a vocabulary whose problem is
already that its names do not line up.
Migration. Rename in keyhac/core/action.py, re-export both names for one
release, update the two skills, doc/configuration.md, doc/action-api.md’s
generator list and the _is_action base-name match in
keyhac/mcp/extensions.py. The AST scan matches on the trailing base name,
so a config that still writes ThreadedAction keeps being discovered
throughout.