Commit graph

369 commits

Author SHA1 Message Date
deseltrus
064389146c test(tui): add regression tests for render debounce and spinner batching
- DynamicBorder: verify lastExternalRender tracking suppresses redundant
  renders during streaming, and standalone renders fire when idle
- TUI clearOnShrink: verify debounce flag lifecycle — deferred shrink
  preserves maxLinesRendered, flag resets when content grows back

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:22:22 +02:00
deseltrus
73f9434d11 fix(tui): eliminate pinned output duplication and reduce render overhead
rebuildChatFromMessages() called populatePinnedFromMessages() which
re-populated the pinned zone with text already present in the chat
history, causing visible duplication during session state changes.
Additionally, the spinner interval at 80ms generated ~12.5 renders/s
for a purely cosmetic animation, and clearOnShrink triggered
unnecessary full redraws during pinned-zone transitions.

- Remove populatePinnedFromMessages() from rebuildChatFromMessages()
  and add pinnedMessageContainer.clear() instead — the streaming
  lifecycle in chat-controller manages pinned content during active work
- Reduce spinner interval 80ms→200ms with render-batching that skips
  redundant renders when streaming already triggers requestRender()
- Debounce clearOnShrink: defer full redraw by one render tick so
  pinned-clear→new-streaming transitions avoid a wasted full redraw
- Increase notification widget safety-net timer 5s→30s since the
  store subscription already handles push-based updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:17:18 +02:00
Jeremy McSpadden
4ab053d9ba Merge pull request #4156 from jeremymcs/fix/4144-claude-code-subturn-regression
fix(tui): reset segment state on claude-code sub-turn shrink
2026-04-13 20:45:36 -05:00
Jeremy
2bf2313395 test(tui): finalize sub-turn regression tests to stop pinned spinner
The two new sub-turn shrink regression tests created a pinned
DynamicBorder (via message_update with pinnable text + tool) but never
emitted message_end, so the spinner's setInterval kept the test process
alive until CI timed out after 15 minutes. Append a message_end to
each test so the module-level pinnedBorder is torn down.
2026-04-13 20:36:52 -05:00
Jeremy
03b7142400 fix(tui): reset segment state on claude-code sub-turn shrink
Commit c8c416802 (#4144) introduced module-level renderedSegments state
to track interleaved text/tool components per assistant turn, but never
reset it when an adapter shrinks streamingMessage.content[] back to 0/1
at a provider sub-turn boundary within one assistant lifecycle (the
claude-code adapter does this). Consequence chain: the segment walker
finds the stale text-run entry at startIndex=0, calls updateContent on
it with the new (shrunk) message, and the in-place edit destroys the
prior sub-turn's visible text. New tool blocks at contentIndex=1 then
collide with stale registrations, causing visual ordering corruption.
hasToolsInTurn stays sticky-true and lastPinnedText never clears, so
the pinned "Working - Latest Output" mirror freezes on the pre-shrink
snapshot.

Track lastContentLength explicitly. On shrink, clear renderedSegments,
reset lastPinnedText, and reset lastProcessedContentIndex so the
walker treats the new sub-turn as fresh segments that append after
prior sub-turn children. Prior history stays rendered as frozen
components; pendingTools and the spinner border are untouched.

Adds two regression tests in chat-controller-ordering.test.ts: one
verifies prior sub-turn components are not overwritten and new tools
append in content[] order after a shrink, the other verifies the
pinned markdown updates from the first sub-turn's text to the second
sub-turn's text across a shrink boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 19:58:11 -05:00
Jeremy McSpadden
fc7d195e09 Merge pull request #4150 from jeremymcs/claude/debug-tui-auto-mode-vCnxA
Split Container.clear() into clear() and detachChildren()
2026-04-13 18:39:33 -05:00
Claude
33d9a26dd7 fix(tui): keep AUTO-mode widgets alive and drop duplicate health panel
InteractiveMode.renderWidgets() called Container.clear() on the
widgetContainerAbove/Below render mounts, which disposed every mounted
extension widget and then re-added the now-dead components. In AUTO mode
updateProgressWidget re-registers gsd-progress on every unit dispatch,
so gsd-notifications and gsd-health had their refresh timers and store
subscriptions killed after the first dispatch. Renders kept returning
the widgets' frozen cachedLines, making them look alive but never update
(/gsd notifications clear appeared to do nothing, belowEditor last-commit
went stale while the top-of-screen dashboard stayed correct).

Split detach from dispose: add Container.detachChildren() and use it from
the two widget-mount call sites. clear() still disposes for every other
caller (chat, editor, status, pinned-message containers). The
extensionWidgets* maps remain the single owner of widget disposal via
removeExisting() and clearExtensionWidgets().

While in AUTO, gsd-progress duplicates gsd-health on last commit, cost/
budget, and the health signal. Make gsd-progress the single source of
truth: hide gsd-health from auto-start and re-register it from every
exit point in auto.ts (lock-lost stop, cleanupAfterLoopExit !paused
guard, stopAuto, pauseAuto). gsd-notifications stays visible — it is
independent state and, with the detach fix, its subscription + 5s
refresh actually work again.

Tests: Container.detachChildren()/clear() contract guards added to
packages/pi-tui/src/__tests__/tui.test.ts. health-widget,
notification-{store,widget,overlay}, notifications-handler, notifications,
and auto-paused-ui-cleanup suites all pass.
2026-04-13 23:30:25 +00:00
Jeremy McSpadden
cdd257e59a Merge pull request #4059 from mastertyko/fix/4054-compaction-safe-role-markers
fix(pi-coding-agent): use safe compaction role markers
2026-04-13 17:58:04 -05:00
Jeremy
c8c416802f fix(tui): render assistant tool calls inline with text instead of grouped at end
Previously the chat-controller created one AssistantMessageComponent per
assistant message and removed/re-appended it to the chat container's tail
on every tool block, forcing all narration after every tool execution
regardless of stream order. Users had to scroll up to read text that was
written before each tool call.

Replace the reorder hack with a stream-order segment walker that walks
content[] left-to-right, collapses contiguous text/thinking blocks into
text-run segments, emits one segment per tool block, and append-only adds
new segments to chatContainer. AssistantMessageComponent gains a
ContentRange API so a single message can spawn multiple text-run
components, plus a separate showMetadata flag so timestamp/error footers
render only on the trailing segment without duplicating earlier text.

Adds a regression test that streams [text, tool, text, tool, text] and
asserts both interleaved order and per-segment rendered text content.

Closes #4144
2026-04-13 17:23:17 -05:00
github-actions[bot]
01df12f14d release: v2.73.1 2026-04-13 17:00:39 +00:00
Jeremy
cce3bc6828 fix(model-resolver): gate saved default restore on provider readiness
Restore the isProviderRequestReady() guard lost during the main merge.
Tests in model-resolver.test.ts and model-resolver-initial-model-auth.test.ts
require findInitialModel() to skip an unauth'd saved default and fall
through to the first available model.
2026-04-13 10:26:28 -05:00
Jeremy
bafa4e483d Merge remote-tracking branch 'upstream/main' into claude/model-agnostic-selection-rmDX3
# Conflicts:
#	packages/pi-coding-agent/src/core/model-resolver.ts
#	src/cli.ts
2026-04-13 10:22:16 -05:00
Jeremy McSpadden
4fcf5d6e6b Merge pull request #4117 from NilsR0711/fix/localhost-custom-provider-compaction-auth
fix(pi-coding-agent): skip localhost dummy key when fallback resolver provides a configured key
2026-04-13 09:12:17 -05:00
Claude
0ed576ac00 Make model selection model-agnostic
Remove hard-coded Anthropic/Claude defaults and silent provider swaps so
the app honors whatever model/provider the user has configured.

- src/cli.ts: drop the anthropic->claude-code auto-migration blocks that
  were rewriting the user's saved defaultProvider on every startup.
- packages/pi-coding-agent/src/core/model-resolver.ts: delete the
  defaultModelPerProvider table, drop the "recommended variant" swap
  that silently upgraded e.g. claude-opus-4-6 to -extended, and replace
  the provider-iteration first-available fallback with provider-sticky
  (user's saved provider first, then first registry entry).
- src/startup-model-validation.ts: replace the openai/anthropic-first
  fallback chain with Pi-default -> same-provider -> first-available.
- src/help-text.ts: use a generic provider/model-id example for --model
  instead of claude-opus-4-6.
- src/tests/startup-model-validation.test.ts: update the fallback test
  to assert provider stickiness rather than a specific Claude model id.

https://claude.ai/code/session_01CvuUuzuVjRcQN25263nG6V
2026-04-13 14:03:35 +00:00
Jeremy McSpadden
3adafde442 Merge pull request #4121 from jeremymcs/fix/4120-pinned-output-duplication
fix(tui): stop pinned latest-output from duplicating streaming text
2026-04-13 08:30:08 -05:00
Jeremy
9ffde91020 test(tui): regression test for pinned latest-output duplication
Extract the post-tool text-block selection logic into a small pure
helper (`findLatestPinnableText`) so the regression scenario can be
covered without standing up the full interactive controller harness.
The new test pins the bug from #4120: when content blocks are
`[text1, tool1, text2_streaming]`, the helper must return `text1`
(not `text2`), because `text2` is still streaming live into the chat
container and mirroring it would render the same tokens twice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:20:24 -05:00
Jeremy
dc84694c65 fix(tui): stop pinned latest-output mirror from duplicating streaming text
The pinned `Working · Latest Output` border above the editor mirrors
the assistant's latest text block while tools run, so prose stays
visible after a tool's output scrolls it off-screen. The mirror walked
content blocks from the end and picked the last text block — but when
the assistant streams a *new* text block after a tool call (sequence
`[text1, tool1, text2_streaming]`), it picked `text2`, which was also
being streamed live into the chat container. Result: identical tokens
rendered in two places at once.

Restrict the search to text blocks whose index is strictly less than
the index of the most recent tool call. Text after the last tool call
stays in the chat container only; earlier prose (e.g. `text1`) remains
mirrored the entire time the new text streams, so context isn't lost
and the loading-animation handoff is undisturbed.

Fixes #4120

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:16:16 -05:00
github-actions[bot]
4733cf7bed release: v2.73.0 2026-04-13 13:04:12 +00:00
Nils Reeh
1ef6ba16f9 fix(pi-coding-agent): skip localhost dummy key when fallback resolver provides a configured key
Custom OpenAI-compatible providers running on localhost (e.g. a local proxy)
with an explicit apiKey in models.json received 'local-no-key-needed' during
compaction instead of their configured key, causing 401 errors.

The localhost shortcut in AuthStorage.getApiKey() was unconditional. Normal
dispatch calls getApiKeyForProvider() which skips the baseUrl check entirely,
so the fallback resolver was reached and the real key was used. Compaction
calls getApiKey(model) which passes baseUrl, hitting the shortcut first.

Closes #4106
2026-04-13 14:32:16 +02:00
NilsR0711
ddff956a91 feat(pi-ai): add Alibaba DashScope as standalone provider (#3891)
* feat(pi-ai): add Alibaba DashScope as standalone provider

Adds `alibaba-dashscope` for users with a regular DashScope API key,
separate from the existing `alibaba-coding-plan` free-tier provider.

- types.ts: register `alibaba-dashscope` as KnownProvider
- env-api-keys.ts: map to DASHSCOPE_API_KEY
- models.custom.ts: add qwen3-max, qwen3.5-plus, qwen3.5-flash,
  qwen3-coder-plus with international endpoint and real pricing
- model-resolver.ts: default model qwen3.5-plus
- key-manager.ts: add alibaba-coding-plan and alibaba-dashscope
  to PROVIDER_REGISTRY so /gsd keys add works for both

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(pi-ai): add qwen3.6-plus to alibaba-dashscope provider

qwen3.6-plus is available on DashScope international endpoint.
Pricing: $0.5/M input, $3/M output (base tier, 0-256K tokens).
Supports thinking mode (reasoning: true).

Source: https://www.alibabacloud.com/help/en/model-studio/model-pricing

Co-Authored-By: Claude Code <noreply@anthropic.com>

* test(pi-ai): add tests for alibaba-dashscope provider and key-manager regression

- packages/pi-ai/src/models.test.ts: add describe block covering all 5
  alibaba-dashscope models (presence, base URL, API, provider field,
  context window, paid pricing, per-model reasoning/cost assertions,
  independence from alibaba-coding-plan, failure path for unknown model)
- src/resources/extensions/gsd/tests/key-manager.test.ts: add regression
  tests for #3891 — alibaba-coding-plan was missing from PROVIDER_REGISTRY,
  causing /gsd keys add alibaba-coding-plan to fail silently; also covers
  alibaba-dashscope registration, env var separation, and getAllKeyStatuses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-13 08:04:39 -04:00
Rebecca Chernoff
110c01b8c6 fix: flush extension provider registrations before model resolution (#1923)
Extension-based providers like pi-claude-cli register their models
during extension loading, but registrations were queued and not flushed
until after model resolution ran. This caused findInitialModel() and
the startup model validation to see extension models as nonexistent,
permanently overwriting the user's saved model selection on every launch.

- Flush pendingProviderRegistrations in createAgentSession() before
  findInitialModel() so extension models are visible in the registry
- Move model validation to after createAgentSession() in both print
  and interactive code paths
- Load extensions before --list-models so extension models appear
2026-04-13 07:06:16 -04:00
Jeremy McSpadden
3a529f7a95 Merge pull request #4100 from jeremymcs/claude/cleanup-mcp-stream-output-9uCeK
Improve MCP tool rendering with name parsing and compact args
2026-04-13 00:54:38 -05:00
Claude
2d1081f1cc fix: clean up MCP tool rendering in Claude Code CLI stream
Strip the `mcp__<server>__` prefix from tool_use blocks emitted by the
Claude Agent SDK so registered GSD extension renderers (gsd_plan_milestone,
gsd_task_complete, etc.) match instead of falling through to the generic
JSON-dump fallback. The original server name is preserved on the toolCall
block under `mcpServer` for downstream rendering.

Tighten the generic ToolExecutionComponent fallback for any remaining
prefixed names (third-party MCP servers): show a muted `server·tool`
title, render primitive args as compact `key=value` pairs, and truncate
output to 10 lines when collapsed.
2026-04-13 05:46:35 +00:00
github-actions[bot]
f188b94761 release: v2.72.0 2026-04-13 05:13:11 +00:00
Jeremy McSpadden
c189b2152e Merge pull request #4092 from jeremymcs/fix/openrouter-credit-retry
fix(auto): recover from OpenRouter affordability 402 errors
2026-04-12 23:04:58 -05:00
Jeremy
724464c7ae fix(auto): recover from OpenRouter credit affordability errors 2026-04-12 22:48:55 -05:00
Claude
701ab18d81 fix(models): block unconfigured models from selection surfaces
Filter models whose provider has no working API key or OAuth out of
every user-facing selection path. Previously, stale defaults and scoped
sets could leak unconfigured models into /model, /gsd model, and auto
run — the user could "pick" a model that immediately threw on use.

- model-selector: filter scopedModels via isProviderRequestReady;
  default to "all" scope when no scoped model is ready.
- model-controller: same filter for getModelCandidates, so exact-match
  resolution from /model <term> can't return an unauth'd scoped model.
- model-resolver: gate findInitialModel step 3 on provider readiness so
  a stale saved default falls through to the available-models path.
- startup-model-validation: check configuredExists against getAvailable
  instead of getAll, so a configured-but-unauth default triggers the
  fallback picker and thinking-level reset.
- auto-start: validate resolveDefaultSessionModel against the live
  registry + auth before snapshotting, and warn when PREFERENCES.md
  names an unconfigured model.

https://claude.ai/code/session_015q6b23ap9Pyqdogzz2FXGh
2026-04-12 17:25:06 -05:00
Jeremy McSpadden
c3aa3a3bf0 Merge pull request #4067 from jeremymcs/fix/gsd-model-session-override 2026-04-12 12:50:12 -05:00
Jeremy McSpadden
17e3ef6a28 Merge pull request #4064 from mastertyko/fix/3402-full-oauth-login-url
fix(pi-coding-agent): show full OAuth login URLs
2026-04-12 12:26:25 -05:00
Jeremy
c96d01acb7 fix(model): require provider readiness for saved default selection 2026-04-12 12:24:49 -05:00
mastertyko
f15938ea4c fix(pi-coding-agent): show full OAuth login URLs 2026-04-12 18:45:28 +02:00
mastertyko
761177c8c4 fix(pi-coding-agent): use safe compaction role markers 2026-04-12 18:14:03 +02:00
Jeremy McSpadden
564a71da37 Merge pull request #4053 from jeremymcs/fix/auto-session-credential-cooldown
fix(auto): survive transient 429 credential cooldown
2026-04-12 09:42:37 -05:00
Jeremy
4f2e90e1e8 test(auto): add tests for credential cooldown fix
- auth-storage.test.ts: 8 tests for getEarliestBackoffExpiry()
- sdk.test.ts: 12 tests for CredentialCooldownError class
- infra-errors-cooldown.test.ts: 35 tests for isTransientCooldownError(),
  getCooldownRetryAfterMs(), and exported constants

Required by CI lint (require-tests.sh) per CONTRIBUTING.md.

Closes #4052
2026-04-12 09:30:52 -05:00
Jeremy
d0afe018eb fix(auto): add structured cooldown error and bounded retry budget
Address Codex adversarial review findings:

- Replace string-matched cooldown detection with typed
  CredentialCooldownError (code: AUTH_COOLDOWN, retryAfterMs)
- Add MAX_COOLDOWN_RETRIES (5) cap so cooldown retries can't spin for
  hours on persistent quota exhaustion
- Auto-loop uses retryAfterMs from structured error when available,
  falls back to 35s default
- Export CredentialCooldownError from pi-coding-agent package
- Retain regex fallback for cross-process error propagation

Closes #4052
2026-04-12 09:16:05 -05:00
Jeremy
cd86e8a7d0 feat(tui): improve gsd overlays, shortcuts, and notification flows 2026-04-12 09:13:46 -05:00
Jeremy
1ae93e9822 fix(auto): survive transient 429 credential cooldown in auto sessions
getApiKey() retry loop (3 attempts, ~12s) couldn't outlast the 30s
rate-limit backoff window, causing cooldown errors to cascade through
the auto-loop and trigger a hard stop after 3 consecutive failures.

- Add AuthStorage.getEarliestBackoffExpiry() to expose when the next
  credential becomes available
- Update getApiKey() to sleep until backoff expiry (up to 60s) instead
  of fixed 2s/4s/6s delays
- Add isTransientCooldownError() detector in infra-errors.ts
- Auto-loop now waits 35s on cooldown errors without incrementing the
  consecutive error counter

Closes #4052
2026-04-12 09:04:41 -05:00
mastertyko
d2ed5a91a6 fix(pi-coding-agent): match renderable tools case-insensitively 2026-04-12 14:05:30 +02:00
github-actions[bot]
cf6f0613dd release: v2.71.0 2026-04-11 23:19:57 +00:00
Jeremy
b488961609 fix(tui): clear pinned output on message_end to prevent duplicate display
The pinned "Latest Output" zone was only cleared at agent_end, but during
flows with form elicitation (e.g. discuss-phase), there is a gap between
message_end and agent_end where the agent waits for user input. During this
gap, the same content was visible in both the chat history and the pinned
zone. Clear the pinned zone at message_end when the assistant message is
finalized in the chat container.
2026-04-11 17:41:50 -05:00
Jeremy
5531538e0d fix(tui): clear pinned latest output on turn completion 2026-04-11 16:58:48 -05:00
Git-Scram
1d1e47e78b fix(tui): restore pinned output above editor during tool execution
Restores the pinned assistant output zone that shows the latest narration
during tool execution. Adds markdown rendering, animated spinner, height
capping to prevent render flashing, and session rebuild support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 15:53:29 -04:00
Jeremy
2d531720f7 fix(tui): mask secure extension input values in interactive mode 2026-04-11 13:28:17 -05:00
Jeremy
74fee9ed48 fix(interactive): keep MCP tool output ordered and restore secure prompt fallback 2026-04-11 12:47:41 -05:00
Jeremy McSpadden
5d48038816 Merge pull request #3992 from jeremymcs/fix/mcp-output-stream-order
fix(interactive): preserve MCP tool output stream ordering
2026-04-11 11:46:23 -05:00
Jeremy
ba6c1f5d7e test(interactive): cover MCP tool output ordering in chat controller 2026-04-11 11:32:28 -05:00
Jeremy
58d729894e fix(interactive): preserve MCP tool output stream ordering 2026-04-11 11:26:08 -05:00
github-actions[bot]
26696be2fa release: v2.70.1 2026-04-11 04:22:31 +00:00
Jeremy McSpadden
19cbb17683 Merge pull request #3961 from jeremymcs/fix/windows-portability-sweep
fix: harden Windows portability across runtime and tooling
2026-04-10 21:53:10 -05:00
Jeremy
61204ce771 fix(windows): harden portability across runtime and tooling 2026-04-10 20:33:18 -05:00