From 627485ecf12991c08340c69c999a117dfb24eebb Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 7 Apr 2026 09:01:46 +0300 Subject: feat: reverse alias IDs for better shell tab-completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alias IDs are now stored in reversed form (e.g. id=37 → "10" instead of "01") so that the first character varies quickly across consecutive IDs, making shell auto-completion more effective. The counter logic is unchanged; only the string representation is reversed via a new reverseString helper in encodeTaskAliasID/decodeTaskAliasID. The cache file is bumped to task-aliases-v2.json so existing mappings are abandoned and all aliases are regenerated with the new format. Also fix TestDispatcher_CompleteUUIDsSubcommand to use an isolated temp dir for the alias cache, preventing flakiness from cross-test pollution. Co-Authored-By: Claude Sonnet 4.6 --- internal/askcli/task_alias_cache.go | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) (limited to 'internal/askcli/task_alias_cache.go') diff --git a/internal/askcli/task_alias_cache.go b/internal/askcli/task_alias_cache.go index 3ea5dff..21967d8 100644 --- a/internal/askcli/task_alias_cache.go +++ b/internal/askcli/task_alias_cache.go @@ -107,7 +107,10 @@ func taskAliasCachePath() (string, error) { if err != nil { return "", fmt.Errorf("resolve cache dir: %w", err) } - return filepath.Join(dir, "ask", "task-aliases-v1.json"), nil + // v2 uses reversed alias strings (e.g. "10" instead of "01") so that the + // first character varies more often, improving shell auto-completion. The + // old v1 file is intentionally abandoned so the mapping starts fresh. + return filepath.Join(dir, "ask", "task-aliases-v2.json"), nil } func (c *taskAliasCache) validate() error { @@ -273,6 +276,21 @@ func (e taskAliasCacheEntry) lastTouchedAt() time.Time { return e.CreatedAt } +// reverseString returns s with its bytes in reverse order. Alias strings only +// ever contain ASCII characters so byte-level reversal is correct. +func reverseString(s string) string { + b := []byte(s) + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + return string(b) +} + +// encodeTaskAliasID converts a monotonically-increasing counter to a short +// alphanumeric string and then reverses it. The reversal ensures that the +// first character of the alias varies as quickly as possible, which makes +// shell tab-completion more effective (e.g. "1", "2", ... "z", "00" becomes +// "1", "2", ..., "z", "00"; then "10", "20", ... instead of "00", "10", ...). func encodeTaskAliasID(id uint64) string { const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -290,9 +308,14 @@ func encodeTaskAliasID(id uint64) string { buf[i] = alphabet[remaining%uint64(len(alphabet))] remaining /= uint64(len(alphabet)) } - return string(buf) + // Reverse so that the least-significant digit comes first, keeping the + // leading character diverse across consecutive IDs. + return reverseString(string(buf)) } +// decodeTaskAliasID is the inverse of encodeTaskAliasID. It reverses the alias +// string to restore the canonical (most-significant-digit-first) form before +// decoding. func decodeTaskAliasID(alias string) (uint64, bool) { const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -300,7 +323,9 @@ func decodeTaskAliasID(alias string) (uint64, bool) { return 0, false } - width := len(alias) + // Reverse the alias back to the canonical form before decoding. + canonical := reverseString(alias) + width := len(canonical) var id uint64 blockSize := uint64(len(alphabet)) for i := 1; i < width; i++ { @@ -309,7 +334,7 @@ func decodeTaskAliasID(alias string) (uint64, bool) { } var value uint64 - for _, r := range alias { + for _, r := range canonical { index := int64(-1) for i, candidate := range alphabet { if r == candidate { -- cgit v1.2.3