From 4146d4b5777d88653f940f3166dc7308758812af Mon Sep 17 00:00:00 2001 From: Automaker Date: Tue, 14 Apr 2026 16:22:17 -0700 Subject: [PATCH 1/4] =?UTF-8?q?ci:=20dev=E2=86=92main=20release=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prepare-release.yml: fires on PR merge to dev (not main); version bump PR targets dev instead of main - release.yml: triggers on dev→main PR merge instead of commit message on push; adds sync-back step to keep dev aligned with main after release Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/prepare-release.yml | 12 +++++------ .github/workflows/release.yml | 29 ++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 70b2c1cda2..25c6a0edd3 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -1,16 +1,16 @@ name: 'Prepare Release' -# Bumps version on main via a PR with auto-merge. When the PR merges, -# release.yml picks up the commit automatically via commit message filter. +# Bumps version on dev via a PR with auto-merge. When the PR merges, +# the dev→main promotion PR can be opened and release.yml will fire on merge. # -# Fires automatically when any non-release PR merges to main (patch bump). +# Fires automatically when any non-release PR merges to dev (patch bump). # Can also be triggered manually via Actions → Prepare Release to choose bump type. on: pull_request: types: [closed] branches: - - main + - dev workflow_dispatch: inputs: bump: @@ -50,7 +50,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: main + ref: dev fetch-depth: 0 token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} @@ -98,7 +98,7 @@ jobs: git push origin "${BRANCH}" PR_URL=$(gh pr create \ - --base main \ + --base dev \ --head "${BRANCH}" \ --title "chore: release v${VERSION}" \ --body "Version bump to v${VERSION} (${TRIGGER})." \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c713fd116b..aabd863421 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,14 @@ name: 'Release' -# Fires when a version bump commit lands on main (pushed by prepare-release.yml). +# Fires when the dev→main promotion PR is merged. # Builds the bundle, publishes @protolabsai/proto to npm, tags, and creates a GitHub Release. +# +# Flow: feature PRs → dev → prepare-release.yml bumps version on dev +# → dev→main promotion PR merges → this workflow tags and releases. on: - push: + pull_request: + types: [closed] branches: - main workflow_dispatch: @@ -16,8 +20,13 @@ jobs: runs-on: ubuntu-latest if: >- github.repository == 'protoLabsAI/protoCLI' && - (github.event_name == 'workflow_dispatch' || - startsWith(github.event.head_commit.message, 'chore: release v')) + ( + github.event_name == 'workflow_dispatch' || + ( + github.event.pull_request.merged == true && + github.event.pull_request.head.ref == 'dev' + ) + ) timeout-minutes: 30 permissions: contents: write @@ -119,8 +128,18 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} DISCORD_RELEASE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} + - name: Sync main back to dev + if: steps.version.outputs.already_tagged != 'true' + continue-on-error: true + run: | + # After tagging on main, merge back to dev so the branches stay aligned. + git fetch origin dev + git checkout dev + git merge origin/main --no-edit -m "chore: sync main back to dev after release" + git push origin dev + - name: Alert on already-tagged if: steps.version.outputs.already_tagged == 'true' run: | - echo "::warning::${{ steps.version.outputs.tag }} is already tagged. Run prepare-release.yml first to bump the version." + echo "::warning::${{ steps.version.outputs.tag }} is already tagged. Ensure prepare-release.yml ran on dev first." exit 1 From 078affc9204c47f196eac58b05e32241f480a8ab Mon Sep 17 00:00:00 2001 From: Josh Mabry <31560031+mabry1985@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:21:04 -0700 Subject: [PATCH 2/4] fix(ui): remove Static windowing that caused messages to disappear (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): apply ASCII logo gradient by X column, not string index ink-gradient maps colors by character index across the whole string, so the p descender (last two lines) always got the tail/pink color regardless of its leftward visual position. Fix: render each logo line separately with its own , padded to logoWidth so column X maps to the same gradient fraction on every line. Co-Authored-By: Claude Sonnet 4.6 * fix(ui): remove Static windowing that caused messages to disappear Ink's tracks rendered items by array INDEX, not React key. It stores the last array length and slices from that index on each render. When the array stops growing (constant length), the index overshoots and nothing new is printed — causing streamed messages to vanish. PR #45 introduced two patterns that broke this invariant: 1. STATIC_HISTORY_WINDOW=200 in MainContent.tsx — sliding window kept the array at a constant 204 items (3 fixed + 200 history + banner), so after the 201st history item nothing was ever printed by Static. 2. MAX_HISTORY_ITEMS=500 in useHistoryManager.ts — pruning the front of the array kept it at exactly 500 items, same effect. 3. Same AGENT_STATIC_HISTORY_WINDOW=200 windowing in AgentChatView.tsx. Fix: pass all history items to Static (array only ever grows). Remove TruncatedHistoryBanner from within Static (it can't update once committed to the terminal anyway, and its conditional insertion shifted existing indices on first appearance). Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Automaker Co-authored-by: Claude Sonnet 4.6 --- packages/cli/src/ui/components/Header.tsx | 13 +++++-- .../cli/src/ui/components/MainContent.tsx | 39 ++++--------------- .../components/agent-view/AgentChatView.tsx | 35 +++-------------- .../cli/src/ui/hooks/useHistoryManager.ts | 17 +------- 4 files changed, 24 insertions(+), 80 deletions(-) diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx index 9648a2126d..f0817c7880 100644 --- a/packages/cli/src/ui/components/Header.tsx +++ b/packages/cli/src/ui/components/Header.tsx @@ -114,10 +114,15 @@ export const Header: React.FC = ({ {/* Left side: ASCII logo (only if enough space) */} {showLogo && ( <> - - - {displayLogo} - + + {displayLogo + .split('\n') + .slice(1) // trim leading empty line from template literal + .map((line, i) => ( + + {line.padEnd(logoWidth)} + + ))} {/* Fixed gap between logo and info panel */} diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx index 04896e81fc..e650d7304a 100644 --- a/packages/cli/src/ui/components/MainContent.tsx +++ b/packages/cli/src/ui/components/MainContent.tsx @@ -7,7 +7,6 @@ import { Box, Static } from 'ink'; import { useMemo } from 'react'; import { HistoryItemDisplay } from './HistoryItemDisplay.js'; -import { TruncatedHistoryBanner } from './TruncatedHistoryBanner.js'; import { ShowMoreLines } from './ShowMoreLines.js'; import { Notifications } from './Notifications.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; @@ -22,18 +21,6 @@ import { DebugModeNotification } from './DebugModeNotification.js'; // usage. const MAX_GEMINI_MESSAGE_LINES = 65536; -/** - * Maximum number of history items to keep in the Static render window. - * Items before this window have already been printed to the terminal and - * do not need to be held in the React tree. Ink's Static identifies - * already-printed items by React key, so the slice does not cause - * re-printing — only genuinely new items at the tail are emitted. - * - * On historyRemountKey change (terminal clear + view switch), only the - * windowed items are reprinted instead of the full unbounded history. - */ -const STATIC_HISTORY_WINDOW = 200; - export const MainContent = () => { const { version } = useAppContext(); const uiState = useUIState(); @@ -45,25 +32,16 @@ export const MainContent = () => { availableTerminalHeight, } = uiState; - const staticItems = useMemo(() => { - const history = uiState.history; - const truncatedCount = Math.max(0, history.length - STATIC_HISTORY_WINDOW); - const visibleHistory = - truncatedCount > 0 ? history.slice(-STATIC_HISTORY_WINDOW) : history; - - return [ + // NOTE: Ink's tracks rendered items by array INDEX (not React key). + // It stores the last-rendered length and slices from that index on each + // render. If the array ever shrinks or stays the same length, the index + // overshoots and nothing new is printed. The array passed to Static must + // therefore only ever grow — never shrink or stay constant length. + const staticItems = useMemo(() => [ , , , - ...(truncatedCount > 0 - ? [ - , - ] - : []), - ...visibleHistory.map((h) => ( + ...uiState.history.map((h) => ( { commands={uiState.slashCommands} /> )), - ]; - }, [ + ], [ uiState.history, uiState.slashCommands, version, diff --git a/packages/cli/src/ui/components/agent-view/AgentChatView.tsx b/packages/cli/src/ui/components/agent-view/AgentChatView.tsx index 255babfbb9..c09e48b0e4 100644 --- a/packages/cli/src/ui/components/agent-view/AgentChatView.tsx +++ b/packages/cli/src/ui/components/agent-view/AgentChatView.tsx @@ -42,11 +42,6 @@ import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { agentMessagesToHistoryItems } from './agentHistoryAdapter.js'; import { AgentHeader } from './AgentHeader.js'; -import { TruncatedHistoryBanner } from '../TruncatedHistoryBanner.js'; - -// How many committed items to keep in the Static render window. -// Mirrors STATIC_HISTORY_WINDOW in MainContent.tsx. -const AGENT_STATIC_HISTORY_WINDOW = 200; // ─── Main Component ───────────────────────────────────────── @@ -209,17 +204,9 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => { // Build the Static items array. Must be called unconditionally (before any // early return) to satisfy the Rules of Hooks. - const staticItems = useMemo(() => { - const truncatedCount = Math.max( - 0, - committedItems.length - AGENT_STATIC_HISTORY_WINDOW, - ); - const visibleItems = - truncatedCount > 0 - ? committedItems.slice(-AGENT_STATIC_HISTORY_WINDOW) - : committedItems; - - return [ + // NOTE: Ink's tracks rendered items by array INDEX (not React key). + // The array must only ever grow — never shrink or stay constant length. + const staticItems = useMemo(() => [ { workingDirectory={agentWorkingDir} gitBranch={agentGitBranch} />, - ...(truncatedCount > 0 - ? [ - , - ] - : []), - ...visibleItems.map((item) => ( + ...committedItems.map((item) => ( { mainAreaWidth={contentWidth} /> )), - ]; - }, [ + ], [ committedItems, agentModelId, agent?.modelName, @@ -270,8 +248,7 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => { {/* Committed message history. key includes historyRemountKey: when refreshStatic() clears the terminal it bumps the key, forcing Static to remount and re-emit - all items on the cleared screen. The windowed slice limits - reprint cost to AGENT_STATIC_HISTORY_WINDOW items max. */} + all items on the cleared screen. */} {(item) => item} diff --git a/packages/cli/src/ui/hooks/useHistoryManager.ts b/packages/cli/src/ui/hooks/useHistoryManager.ts index dc4a055db5..cc7ed5575e 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.ts @@ -7,15 +7,6 @@ import { useState, useRef, useCallback } from 'react'; import type { HistoryItem } from '../types.js'; -/** - * Maximum number of history items to retain in React state. - * Items beyond this cap are pruned from the front — they have already been - * printed to the terminal by Ink's Static and do not need to be reconciled. - * Must be >= STATIC_HISTORY_WINDOW (200) in MainContent.tsx so the render - * window always has items available. - */ -const MAX_HISTORY_ITEMS = 500; - // Type for the updater function passed to updateHistoryItem type HistoryItemUpdater = ( prevItem: HistoryItem, @@ -70,13 +61,7 @@ export function useHistory(): UseHistoryManagerReturn { return prevHistory; // Don't add the duplicate } } - const next = [...prevHistory, newItem]; - // Prune the oldest items once the cap is exceeded. Items that are - // removed have already been committed to the terminal by Ink's Static - // and no longer need to live in React state. - return next.length > MAX_HISTORY_ITEMS - ? next.slice(-MAX_HISTORY_ITEMS) - : next; + return [...prevHistory, newItem]; }); return id; // Return the generated ID (even if not added, to keep signature) }, From aade6b03a191069095127ab94c73d4ec0ac7fa0f Mon Sep 17 00:00:00 2001 From: Automaker Date: Tue, 14 Apr 2026 19:24:24 -0700 Subject: [PATCH 3/4] fix(ci): use direct merge in prepare-release (no auto-merge without protection) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/prepare-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 25c6a0edd3..4aedecc5c9 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -105,5 +105,5 @@ jobs: ) echo "Created PR: ${PR_URL}" - gh pr merge "${PR_URL}" --auto --squash - echo "Auto-merge enabled on ${PR_URL}" + gh pr merge "${PR_URL}" --squash --admin + echo "Merged ${PR_URL}" From d43af062e8d706d8887570ddbd82aedd1e5d897c Mon Sep 17 00:00:00 2001 From: Automaker Date: Tue, 14 Apr 2026 19:28:01 -0700 Subject: [PATCH 4/4] chore: release v0.25.12 Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 14 +++++++------- package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/test-utils/package.json | 2 +- packages/web-templates/package.json | 2 +- packages/webui/package.json | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd878d5997..341c3a534a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@protolabsai/proto", - "version": "0.25.11", + "version": "0.25.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@protolabsai/proto", - "version": "0.25.11", + "version": "0.25.12", "workspaces": [ "packages/*" ], @@ -16907,7 +16907,7 @@ }, "packages/cli": { "name": "@protolabs/proto", - "version": "0.25.11", + "version": "0.25.12", "dependencies": { "@agentclientprotocol/sdk": "^0.14.1", "@google/genai": "1.30.0", @@ -17261,7 +17261,7 @@ }, "packages/core": { "name": "@qwen-code/qwen-code-core", - "version": "0.25.11", + "version": "0.25.12", "hasInstallScript": true, "dependencies": { "@anthropic-ai/sdk": "^0.36.1", @@ -20089,7 +20089,7 @@ }, "packages/test-utils": { "name": "@qwen-code/qwen-code-test-utils", - "version": "0.25.11", + "version": "0.25.12", "dev": true, "license": "Apache-2.0", "devDependencies": { @@ -20144,7 +20144,7 @@ }, "packages/web-templates": { "name": "@qwen-code/web-templates", - "version": "0.25.11", + "version": "0.25.12", "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -20672,7 +20672,7 @@ }, "packages/webui": { "name": "@qwen-code/webui", - "version": "0.25.11", + "version": "0.25.12", "license": "MIT", "dependencies": { "markdown-it": "^14.1.0" diff --git a/package.json b/package.json index c28424ad4f..410232d76d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@protolabsai/proto", - "version": "0.25.11", + "version": "0.25.12", "publishConfig": { "access": "public" }, diff --git a/packages/cli/package.json b/packages/cli/package.json index ff4302130b..16ddd12e19 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@protolabs/proto", - "version": "0.25.11", + "version": "0.25.12", "description": "proto", "repository": { "type": "git", diff --git a/packages/core/package.json b/packages/core/package.json index 43d3940765..352e6a4586 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-core", - "version": "0.25.11", + "version": "0.25.12", "description": "proto core", "repository": { "type": "git", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index d6f14cf421..c06f650885 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-test-utils", - "version": "0.25.11", + "version": "0.25.12", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/web-templates/package.json b/packages/web-templates/package.json index e134c22d2e..d176a860e0 100644 --- a/packages/web-templates/package.json +++ b/packages/web-templates/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/web-templates", - "version": "0.25.11", + "version": "0.25.12", "description": "Web templates bundled as embeddable JS/CSS strings", "repository": { "type": "git", diff --git a/packages/webui/package.json b/packages/webui/package.json index f93ff80353..1ee731cccf 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/webui", - "version": "0.25.11", + "version": "0.25.12", "description": "Shared UI components for proto packages", "type": "module", "main": "./dist/index.cjs",