diff --git a/README.md b/README.md index 7b6e04bd2..055dac2fc 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,27 @@ proto is a fork of [Qwen Code](https://github.com/QwenLM/qwen-code) (itself fork ## What's Different -| Feature | Qwen Code | proto | -| ---------------- | ----------------------- | ------------------------------------------------------------------------------ | -| Default model | Qwen3-Coder | Any (configurable) | -| Task management | In-memory JSON | [beads_rust](https://github.com/Dicklesworthstone/beads_rust) (SQLite + JSONL) | -| Memory | Single append-only file | File-per-memory with YAML frontmatter, 4-type taxonomy, auto-extraction | -| MCP servers | None | Configurable via `~/.proto/settings.json` | -| Plugin discovery | Qwen only | Auto-discovers Claude Code plugins from `~/.claude/plugins/` | -| Skills | Nested superpowers | Flat bundled skills (16 skills, all discoverable) | +At-a-glance overview vs. upstream Qwen Code. For the full architectural breakdown see [`docs/architecture/divergence-from-upstream.md`](./docs/architecture/divergence-from-upstream.md). + +| Category | Qwen Code | proto | +| --------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Default model | Qwen3-Coder | Any (LiteLLM / OpenAI-compat / Anthropic / Gemini) | +| Agent harness | — | Sprint contracts + scope lock, behavior-verify gate, multi-sample selector, doom-loop reminders, session memory + evolve, checkpoint/rewind, speculation | +| Bundled skills | 0 (use external) | 22 (sprint-contract, verification-before-completion, systematic-debugging, …) | +| Subagent execution | Sequential | Concurrent batched — Agent calls run in parallel; tool ordering preserved | +| Tool-call streaming | Per-converter parser | Per-stream parser context (no cross-stream corruption); malformed JSON → UI-hidden recovery note | +| Reasoning models | Basic `reasoning_content` | Inline ``-tag extraction (Minimax/QwQ); reasoning-only `content: ""` fix; preserved on session resume | +| Truncation handling | Best-effort | MAX_TOKENS cascade detection + tool-response trimming; rejected truncated edits | +| Task management | In-memory JSON | [beads_rust](https://github.com/Dicklesworthstone/beads_rust) (SQLite + JSONL) | +| Memory | Single append-only file | File-per-memory with YAML frontmatter, 4-type taxonomy, auto-extraction | +| MCP servers | None | Configurable via `~/.proto/settings.json`; SSE/HTTP/stdio in ACP mode | +| Plugin discovery | Qwen only | Auto-discovers Claude Code plugins from `~/.claude/plugins/` | +| Ignore files | `.qwenignore` | `.protoignore` + inherits `.claudeignore` patterns | +| ACP / Zed integration | Stock | Cron-in-Session, concurrent Agent calls, SSE/HTTP MCP, internal-part filtering | +| Extra built-in tools | Standard set | + browser automation, repo-map (PageRank), task tools, mailbox, LSP, voice/STT | +| Observability | Console | Langfuse OTLP traces with harness-intervention spans (SFT-ready) | +| Release pipeline | Manual | Conventional-commit auto-release (`feat:` → minor, `fix:` → patch) | +| VS Code companion | Included | Removed (focus on TUI + ACP/Zed) | ## Installation diff --git a/docs/architecture/divergence-from-upstream.md b/docs/architecture/divergence-from-upstream.md new file mode 100644 index 000000000..a7b222f5d --- /dev/null +++ b/docs/architecture/divergence-from-upstream.md @@ -0,0 +1,608 @@ +# Divergence from Upstream (`QwenLM/qwen-code`) + +A maintainer-oriented map of how `protoLabsAI/protoCLI` differs from the +`qwen-code` upstream we forked from. The intent is to make it easy to: + +- Reason about which subsystems are ours vs. inherited drift, +- Understand _why_ each divergence exists before changing or porting it, +- Decide which upstream PRs are worth porting and which we have intentionally + walked away from. + +**Snapshot at time of writing (April 2026):** + +- Merge base: `20e51e3d3039687710cb95e63bfaaf24f8686721` +- Fork-unique commits since divergence: 301 +- Upstream commits not in the fork: 639 (mix of intentionally-skipped and not-yet-evaluated) +- Versions: fork on **0.26.x**, upstream on **0.15.x** (fork bumps are independent and aggressive) +- Files added by fork (excluding docs/lockfiles): ~190 +- Files deleted relative to merge base: 413 (mostly the VSCode webview, Qwen + OAuth, `qwen-*` workflows, and SDK Java) + +The headline: this is no longer a thin rebrand. The fork has built a serious +agent harness on top of qwen-code's TUI + ACP plumbing, and has rewritten the +streaming converter and ignore-file machinery to be safer for our LiteLLM + +Anthropic deployment. Most of upstream's recent feature work (Python SDK, Java +SDK, VSCode companion, qwen-oauth, multilingual UI churn) is irrelevant to us +or actively counter to where we are going. + +--- + +## 1. Identity & Branding + +This is the layer that is closest to a pure rename, but it is mechanically +load-bearing because the binary name, NPM scope, and config dirs all track it. + +| Concept | Upstream | Fork | +| -------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | +| Binary name | `qwen` | `proto` (`packages/cli/package.json:17`) | +| NPM scope | `@qwen-code/qwen-code` | `@protolabs/proto` (CLI), `@protolabsai/proto` (workspace root), `@protolabsai/sdk` | +| Config dir | `~/.qwen/` | `~/.proto/` (referenced from settings & `QWEN_DIR` constant) | +| Window title | `Qwen - ` | `protoCLI - ` (`packages/cli/src/utils/windowTitle.ts:14`) | +| Ignore file | `.qwenignore` | `.protoignore` (+ `.claudeignore` — see §4) | +| Repo root org | `QwenLM/qwen-code` | `protoLabsAI/protoCLI` | +| Locale strings | `Qwen Code` mentioned throughout `en.js` | Cleaned to `proto` (`packages/cli/src/i18n/locales/en.js`) | + +**Things still labeled with `Qwen` deliberately:** + +- The internal package name `@qwen-code/qwen-code-core` (`packages/core/package.json:2`) + — renaming would force every fork-unique import line to be touched and is + not worth the merge-conflict surface during ongoing upstream backports. +- The `QwenCode` value in the `ExtensionOriginSource` enum + (`packages/core/src/config/config.ts:277`) — kept for compatibility with + user-installed extensions that record their origin. +- `DEFAULT_QWEN_MODEL` constant and the `QWEN_DIR` storage constant — same + reasoning. Internal identifier, not user-visible. +- `sandboxImageUri: ghcr.io/qwenlm/qwen-code:0.26.5` (`package.json:23`) — + we do not yet ship our own sandbox image. + +**Honest take:** if you read `packages/core` source, you will still see +`Qwen` in dozens of places. The user-visible surface is consistently `proto`. + +--- + +## 2. Inference & LLM Plumbing + +This is the most architecturally interesting divergence, because it is where +we deviate from upstream in _behavior_, not just in branding. Our deployment +stack is **proto → LiteLLM → (Anthropic | vLLM)**, which is a different +shape from upstream's primary path of **qwen-code → DashScope/Modelscope / +qwen-oauth direct**. Several of our changes exist specifically because the +LiteLLM gateway and vLLM-served Qwen tool-call templates do things that +DashScope does not. + +### 2.1 `protoInternal: true` Part flag + +`packages/core/src/utils/partUtils.ts:14-26` + +A Proto-namespaced boolean on `Part` objects that marks them as +**model-visible / UI-hidden**. The model still sees the text on the next +turn (so it can self-correct), but every UI surface filters them out. + +Used today for tool-call recovery notes injected when upstream streams +malformed JSON arguments — the model needs the note ("retry the call with +properly-formed arguments") but the user shouldn't see error noise. + +Filtered at: + +- `packages/core/src/utils/partUtils.ts:98` — `partToString` +- `packages/core/src/utils/thoughtUtils.ts:71` +- `packages/cli/src/ui/utils/resumeHistoryUtils.ts:36, 55` — session resume +- `packages/cli/src/acp-integration/session/Session.ts:355, 531` — ACP surface +- `packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts:21` + +This is genuinely novel — upstream has no equivalent escape hatch. If we +ever want more "things the model needs to see but the user shouldn't", +this flag is the seam. + +### 2.2 Per-stream converter context (`ConverterStreamContext`) + +`packages/core/src/core/openaiContentGenerator/converter.ts:104-164` + +We backported upstream's #3525 (which scoped the streaming tool-call parser +per-stream to fix concurrent-stream bleed) and **extended it** to also scope +``-tag accumulator state. The struct is: + +```ts +export interface ConverterStreamContext { + toolCallParser: StreamingToolCallParser; + thinkBuffer: string; + inThinkTag: boolean; +} +``` + +Why we care: parallel subagents, fork children, and ACP concurrent Agent +calls (#3463) all hit `Config.contentGenerator` simultaneously. With shared +state, two concurrent streams could land tool-call chunks at the same +`index=0` bucket and emit interleaved corrupt JSON (the `NO_RESPONSE_TEXT` +issue from upstream #3516). Same problem applied to the `` parser +once we added Minimax/QwQ inline-XML reasoning support. + +The fix: `createStreamContext()` per stream, passed into every +`convertOpenAIChunkToGemini` call, then dropped when the stream finishes. +No reset bookkeeping at all. + +### 2.3 Malformed tool-call drop with internal recovery note + +`packages/core/src/core/openaiContentGenerator/converter.ts:1184-1196` +`packages/core/src/core/openaiContentGenerator/streamingToolCallParser.ts:280-314` + +Defense-in-depth pattern for the LiteLLM + vLLM stack. If the streaming +parser fails every JSON-recovery strategy (raw parse, close-quote retry, +`jsonrepair`), it returns `{ args: {}, malformed: true }`. The converter +then **drops the call entirely** and pushes a `protoInternal: true` text +part telling the model to retry. + +Without this, two failure modes hit users: + +1. The actual tool invocation fires with empty args and the tool either + errors loudly or does the wrong thing. +2. The conversation history records the broken function call. Pydantic + validators in LiteLLM (or in the next provider hop) reject it on the + next turn with a generic 400 — the agent has no way to recover. + +### 2.4 ``-tag inline reasoning extraction + +`packages/core/src/core/openaiContentGenerator/converter.ts:166-274, 997, 1126+` + +Models served via vLLM (Minimax, QwQ-style reasoners) emit reasoning as +inline `...` XML in the content channel rather than as +separate `reasoning_content`. We strip the tag and route the inner content +to the Gemini `thought` part. Handles cross-chunk split tags via +`_partialOpenMatch` heuristic. + +This is fork-only. Upstream's reasoning-content path assumes a separate +SSE field, which is what DashScope/OpenAI both produce. + +### 2.5 MAX_TOKENS cascade detection + tool-response trimming + +`packages/core/src/core/geminiChat.ts:204-260+` + +When the previous turn has a `functionResponse` whose error contains +`"truncated due to max_tokens limit"`, we treat the next turn as a recovery +attempt and proactively cap large successful tool responses to +`LARGE_TOOL_RESPONSE_TRIM_CHARS = 10_000` (~2.5 K tokens). This stops the +agent from getting stuck in a "tool truncated → retry → tool result eats +output budget → tool truncated" loop. + +This is paired with a fork-specific lower default: `DEFAULT_OUTPUT_TOKEN_LIMIT += 16_000` (`packages/core/src/core/tokenLimits.ts:12`), down from upstream's +32 K. Conservative default, but it leaves more headroom for context on +Anthropic models. + +### 2.6 Provider stack notes + +`packages/core/src/core/openaiContentGenerator/provider/` includes +`anthropic`, `dashscope`, `deepseek`, `default`, `modelscope`, `openrouter`. +The Anthropic content generator is a sibling under +`packages/core/src/core/anthropicContentGenerator/` rather than living as +an OpenAI-shape provider — this matters because Anthropic's tool-call +streaming, prompt caching, and thinking are first-class, not adapters. + +--- + +## 3. ACP / Session Layer + +`packages/cli/src/acp-integration/session/` + +The ACP (Agent Client Protocol) layer is more elaborate in the fork than +upstream: + +- `HistoryReplayer.ts` — modular event replay for resumed sessions. +- `SubAgentTracker.ts` — tracks tool calls originating from sub-agents + (for parallel-agent/team flows) so the ACP surface can attribute them + correctly. +- `emitters/` — split into `ToolCallEmitter`, `PlanEmitter`, + `MessageEmitter` for clean event boundaries. + +### Cron in Session + +`Session.ts:115-462` + +The Session class owns a per-session cron queue: + +- `cronQueue: string[]` and `cronProcessing` boolean +- `cronAbortController` so a user prompt cancels in-progress cron work +- `cronCompletion: Promise` so we can deterministically wait on + abort flushes +- `#drainCronQueue` consumes queued prompts FIFO when no user prompt is + active + +This is why the cron tools exist (§4) — we drive scheduled prompts through +the same Session as user input rather than spinning up parallel agents. + +### Internal-part filtering at the ACP surface + +`Session.ts:355, 531` — Session honors `isInternalPart` so malformed +tool-call recovery notes (and any future internal parts) are stripped +before being emitted as ACP `SessionUpdate`s to Zed/CRUSH/etc. + +### Things we deferred from upstream's ACP work + +- **#3479** ACP system reminders — not yet ported. +- **#3550** stateless converter refactor — we have a partial version + (#3525); the full stateless rework was deferred. + +--- + +## 4. Tooling & File Discovery + +### 4.1 `.protoignore` + `.claudeignore` inheritance + +`packages/core/src/utils/protoIgnoreParser.ts` + +`ProtoIgnoreParser` (renamed from `QwenIgnoreParser`) loads from both +`.claudeignore` (first) and `.protoignore` (second). Later patterns override +earlier ones (gitignore semantics). This means projects that already use +Claude Code's ignore conventions Just Work — they don't need a duplicate +file. If a user wants to override Claude's defaults, they put a +`.protoignore` next to it. + +Consumed by: + +- `packages/core/src/services/fileDiscoveryService.ts:137` +- All file-listing tools: `glob.ts`, `ls.ts`, `read-file.ts`, `ripGrep.ts` +- `getFolderStructure` for context summaries + +### 4.2 Net-new tools (not in upstream) + +- **Cron tools** — `cron-create.ts`, `cron-list.ts`, `cron-delete.ts` + (`packages/core/src/tools/`). Backed by `services/cronScheduler.ts` + with deterministic per-job jitter (10% of period, capped at 15 min for + recurring; -90s for one-shots on `:00` / `:30`). Persists to disk and + prunes expired jobs on load. +- **Browser automation** — `tools/browser-tool.ts`, paired with a bundled + `browser-automation` skill. Uses an external `agent-browser` binary + detected at runtime. +- **Mailbox** — `mailbox-tools.ts`, `agents/mailbox.ts`. Inter-agent + message-passing primitive used by `TeamOrchestrator`. +- **Repo map** — `tools/repoMap.ts` + `services/repoMapService.ts`. + Personalized PageRank over the import graph; cached at + `.proto/repo-map-cache.json`. Lets agents orient themselves on a + large codebase without reading every file. +- **Task tools** — `task-create.ts`, `task-get.ts`, `task-list.ts`, + `task-output.ts`, `task-ready.ts`, `task-stop.ts`, `task-update.ts` + (backed by `services/task-store.ts`). Distinct from the AskUserQuestion + / TodoWrite split — these track long-running async work. +- **LSP** — `tools/lsp.ts` exposing language-server intelligence; a + fork-only setting (`general.lsp`) gates it. + +### 4.3 Tools we removed + +- `todoWrite.ts` — deleted. Upstream still has it; we use an internal + plan/todo path through `PlanSummaryDisplay` and `TodoDisplay` UI. +- `web-fetch` and `web-search` are still present but extended with + graceful-degradation paths (timeout + ripgrep fallback for offline / + air-gapped environments). + +--- + +## 5. Configuration & Permissions + +### 5.1 Settings schema + +`packages/cli/src/config/settingsSchema.ts` + +Fork-added settings (with reason): + +- `general.lsp` — gate Language Server Protocol features +- `ui.enableFollowupSuggestions` — context-aware follow-up suggestions +- `ui.enableCacheSharing` — cache-aware forked queries (experimental) +- `ui.enableSpeculation` — speculative execution of accepted suggestions +- `voice.*` — push-to-talk STT settings (entire subtree is fork-only; + see §7 for stack) + +Co-authored-by + locale strings updated to refer to `proto` and +`~/.proto/locales/`. + +### 5.2 Extension origin enum + +`packages/core/src/config/config.ts:277` + +```ts +export type ExtensionOriginSource = 'QwenCode' | 'Claude' | 'Gemini'; +``` + +Added `'Claude'` and `'Gemini'` so we can track where an extension +metadata file came from. Paired with `extension/claude-converter.ts` and +`extension/gemini-converter.ts` that translate Claude / Gemini extension +manifests into our internal format. + +### 5.3 Permission services + +- `permissions/auto-approve-classifier.ts` — fork-only LLM-backed + classifier returning `allow | deny | ask`. Capped per session. +- `services/permissionBlockerService.ts` — persists "this got denied + twice in a row" so the agent stops re-attempting it across sessions. + Threshold: `DENY_THRESHOLD = 2`. + +These wrap, but do not replace, the upstream rule-based permission system. + +--- + +## 6. The Agent Harness (Largest Architectural Delta) + +This is the section where the fork has done the most work and where there +is no upstream equivalent. The pattern is: keep the model on rails by +catching common failure modes early and either auto-recovering or surfacing +a clean prompt that gets it back on task. + +### 6.1 Skills system + +`packages/core/src/skills/` + +22 bundled skills shipping in the binary +(`packages/core/src/skills/bundled/`): +adversarial-verification, brainstorming, browser-automation, +coding-agent-standards, dispatching-parallel-agents, executing-plans, +finishing-a-development-branch, harness-reference, loop, qc-helper, +receiving-code-review, requesting-code-review, review, sprint-contract, +subagent-driven-development, systematic-debugging, test-driven-development, +using-git-worktrees, using-superpowers, verification-before-completion, +writing-plans, writing-skills. + +`SkillManager` loads bundled + user skills (from `~/.proto/skills/`) and +the model can invoke them by name via the `skill` tool. + +Upstream has **no skills system**. This is entirely ours. + +### 6.2 Sprint contract + scope lock + +`packages/core/src/services/sprintContractService.ts` +`packages/core/src/services/scopeLock.ts` + +Pre-implementation contract: filesToCreate, filesToModify, acceptance +criteria, etc. Activating it arms a glob-based scope lock — any write +outside the permitted set is rejected. Lock survives session restart +via `.proto/sprint-contract.json`. + +### 6.3 Behavior verification gate + multi-sample selector + +`services/behaviorVerifyGate.ts`, `services/multiSampleSelector.ts` + +Gate that runs N samples, picks the best one. Used today as the harness +hardening pass — produces measurably better results on terminal-bench-2. + +### 6.4 Doom-loop detection + harness reminders + +`services/harnessReminderService.ts` + spans in `telemetry/harnessTelemetry.ts` + +Trigger types: `tool_count_exceeded`, `test_failure_threshold`, +`analysis_loop`, `no_progress`. Each fires a Langfuse OTel span tagged +`harness.intervention.type` so we can build SFT datasets from +`(input_context, intervention_message)` pairs where `harness.outcome` is +later annotated as recovered. This is explicit fine-tuning data +generation; not just observability. + +### 6.5 Session memory and evolve + +- `services/sessionMemory/` — background AgentHeadless that keeps + `.proto/session-notes.md` up to date after each turn (above token + thresholds). When compaction fires, we use the notes file as the summary + rather than re-summarizing. +- `services/evolveService.ts` — every 3 turns, a background agent looks + for reusable workflow patterns and drafts a `SKILL.md` candidate in + `.proto/evolve/skills/` for user review. +- `memory/` — frontmatter-parsed memory store with proposal queue, + feeds into the system prompt. + +### 6.6 Checkpoints, rewind, and follow-ups + +- `core/checkpointStore.ts` — per-turn checkpoint with lazy file snapshots. + Snapshots capture only files about to be modified (no eager I/O). +- `core/client.ts:227 trimHistoryToCheckpoint()` — the rewind primitive. +- `ui/components/RewindPicker.tsx`, `RewindDialog.tsx` and the `/rewind` + command let users roll back N turns and optionally restore files or + summarize forward from there. +- `followup/` directory: forked-query speculation, overlay FS for + speculative writes, cache-aware suggestion generation. Powers the + `enableFollowupSuggestions` setting. + +**Note:** upstream landed a competing rewind feature (#3441) on +2026-04. We have not evaluated yet whether to adopt their implementation +or keep ours — ours is more tightly coupled to checkpoint snapshots. + +### 6.7 Background subagents, teams, arena + +- `agents/runtime/` — `AgentCore` (stateless engine), `AgentHeadless` + (one-shot), `AgentInteractive` (persistent loop), `compaction.ts`. +- `agents/TeamOrchestrator.ts` + `agents/team-config.ts` / + `team-registry.ts` — multi-agent team execution. +- `agents/background-store.ts` — persists background agent state to + `.proto/agents/background.json`. +- `agents/arena/` — A/B-style model comparison. Surfaced via + `arenaCommand`. + +Upstream has #3076 (background subagents) but our tree is more developed. + +--- + +## 7. UI / TUI Additions + +`packages/cli/src/ui/` + +Fork-added components & hooks (curated list — full inventory in git): + +- `StatusBar.tsx` — hostname + status display +- `RewindPicker.tsx`, `RewindDialog.tsx` +- `VoiceMicButton.tsx` +- `TaskUpdateDiffDisplay.tsx` +- `TruncatedHistoryBanner.tsx` +- `useVoice.ts`, `useFollowupSuggestions.tsx`, + `useBackgroundAgentProgress.ts`, `useGitDiffStat.ts`, + `useIdleMessageDrain.ts`, `useSessionMemoryStatus.ts` + +### 7.1 Voice input + +`packages/cli/src/services/audioCapture.ts` + `sttClient.ts`, +`packages/cli/src/ui/hooks/useVoice.ts` + +Push-to-talk via Ctrl+Space using `sox` for recording. Routes to an +OpenAI-compatible STT endpoint (configurable via `voice.*` settings). +Special-cased for kitty keyboard protocol terminals. + +### 7.2 Setup wizard + +`packages/cli/src/commands/setup/handler.ts` + `modelDiscovery.ts`, +`packages/cli/src/ui/commands/setupCommand.ts` + +Interactive `proto setup` wizard: discovers models from the configured +endpoint, walks STT setup, persists settings. Lower-friction onboarding +than the upstream auth/config dance. + +### 7.3 New slash commands (over upstream) + +`/notes`, `/rewind`, `/team`, `/voice`, `/setup`, `/insight`, +`/setup-github`, `/skills` (registered explicitly in +`BuiltinCommandLoader.ts:34-115`). + +--- + +## 8. Build / Release / Deploy + +`.github/workflows/` + +### 8.1 Auto-release pipeline (fork-only) + +- `auto-release.yml` — fires after CI on `main`. Reads conventional + commits since the last tag, determines bump (major/minor/patch), + bumps every workspace, opens a release PR with auto-merge enabled. +- `prepare-release.yml` — fires on PR merges to `dev`. Default patch + bump; manual dispatch can request minor/major or dry-run. +- `release.yml` — publishes to NPM after the release PR merges. +- `scripts/determine-bump.js`, `scripts/rewrite-release-notes.mjs` — + conventional-commit driven bump + notes rewrite. + +This is why we are at 0.26.x while upstream is 0.15.x. The fork ships +roughly weekly; upstream ships monthly. + +### 8.2 Workflows we removed + +- `qwen-automated-issue-triage.yml`, `qwen-code-pr-review.yml`, + `qwen-scheduled-issue-triage.yml`, `gemini-*.yml`, + `check-issue-completeness.yml`, `community-report.yml`, `stale.yml`, + `release-vscode-companion.yml` — none of these run against us. + +### 8.3 Dev workflow + +- `.husky/pre-push` — schema staleness + snapshot-warning hooks. +- `.coderabbit.yaml` — CodeRabbit review config. +- `tools/harbor_agent/proto_agent.py` — terminal-bench evaluation + agent that installs `@protolabsai/proto` and routes through our + CLIProxyAPI gateway. + +--- + +## 9. Telemetry + +`packages/core/src/telemetry/` + +### 9.1 Langfuse OTLP wiring + +`telemetry/sdk.ts` — first-class Langfuse exporter using +`LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` +env vars. Builds basic-auth headers and points OTLP/HTTP exporters at +`/api/public/otel/v1/traces|logs|metrics`. + +OTEL diagnostics are silenced by default; opt in with +`PROTO_OTEL_DEBUG=1`. Upstream's default leaks connection errors when +no collector is running, which is why we silenced it. + +### 9.2 Harness telemetry + +`telemetry/harnessTelemetry.ts` — every harness intervention emits an +OTel span tagged `harness.intervention.type` + `.message` + context. +Designed for SFT dataset construction (see §6.4). + +### 9.3 Turn-span context + +`telemetry/turnSpanContext.ts` — propagates a turn-scoped span context +so all sub-spans (tool calls, agent rounds, completions) chain under +the right parent in Langfuse. + +--- + +## 10. Removed / Gutted Surfaces + +The fork is not just additive. We removed entire subsystems: + +- **`packages/vscode-ide-companion/`** — gone. ~7000 lines deleted. + We're shipping into Zed via ACP, not VSCode. +- **`packages/core/src/qwen/`** — entire directory deleted: Qwen OAuth, + shared token manager, Qwen content generator. Removed in + `e25b0b853 chore: remove Qwen OAuth + harness hardening`. +- **`packages/sdk-java/`** — gone. Upstream maintains a Java SDK; we + don't. +- **`packages/core/src/ide/ide-installer.ts`** — removed; we don't + install ourselves into IDEs. +- **`packages/core/src/tools/todoWrite.ts`** — removed; replaced by + internal plan/todo path. +- **`docs/users/` (upstream layout)** — replaced with Divio-style + layout (`docs/{tutorials,guides,reference,explanation,contributing}`). + +--- + +## 11. Intentionally NOT Ported From Upstream + +These were evaluated during the most recent backport pass and consciously +deferred or rejected: + +| Upstream PR | Topic | Decision | +| -------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #3550 | Stateless converter refactor (full version of #3525) | **Deferred.** We have the per-stream context piece (#3525) which is the key fix; the further refactor changes API shape and isn't worth the merge cost yet. | +| #3479 | ACP system reminders | **Deferred.** Our `harnessReminderService` covers the core need via a different mechanism. Reconsider if we hit ACP gaps for Zed users. | +| #3313 | Truncated tool-call multi-turn recovery | **Deferred.** Our MAX_TOKENS cascade trimming (§2.5) addresses the symptom from a different angle. | +| #3315 | Strip-thoughts test | **Deferred.** Test-only PR; covered by our own ``-tag tests. | +| #3505 | `clearRetryCountsForTool` | **Deferred** (since landed in our backport batch — verify before re-deferring). | +| #3441 | Conversation rewind feature | **Conflicts with our rewind.** Our implementation is older and more deeply integrated with checkpoint snapshots. Need to evaluate whether upstream's superseded ours or merely duplicated it. | +| #3494 | Python SDK | **Rejected.** Out of scope for our Anthropic-first deployment. | +| #3010 / SDK Java | Java SDK | **Rejected.** Same reason. | +| qwen-oauth model dialog blocks | Discontinued model handling | **N/A.** We removed Qwen OAuth entirely. | +| #3010 family — VSCode webview features | VSCode integration | **N/A.** We deleted the VSCode companion. | + +--- + +## 12. Areas Where We Are Behind + +639 upstream commits sit unevaluated. The themes that look most important +to track: + +- **Rewind feature parity (#3441 + follow-ups #3622, #3605).** Upstream + shipped a competing rewind UX. Our implementation is older. Evaluate + whether their `Space-to-preview` / picker behavior is worth backporting + on top of our checkpoint-aware engine. +- **Tool hot-path I/O perf (#3581).** Claims a 91% reduction in + runtime sync I/O on the tool path. Not glamorous but every turn pays + for it. +- **Reasoning content during session resume (#3590).** Touches our + resume logic; we likely diverge. +- **Telemetry FileExporter circular-ref crash (#3630).** Defensive fix. + Probably want to grab. +- **DeepSeek/sglang/vllm provider matching (#3613).** We serve via vLLM + for non-Anthropic models — this might already affect us. +- **Sticky todo panel (#3507).** UX feature we may or may not want. +- **`OPENAI_MODEL` precedence (#3567 + revert in #3633).** Live debate + upstream; watch before porting. + +The "skip the SDK / VSCode / qwen-oauth / Java" filter still removes +maybe 30% of the upstream queue cleanly. The remaining ~450 commits are +the ones worth grooming. + +--- + +## Maintenance notes + +- **When backporting upstream code that touches `Part`s or text streams**: + remember to add `isInternalPart` filtering at any new UI surface, and + pass `ConverterStreamContext` rather than reaching for the converter's + internal state. +- **When adding a new ignore-aware tool**: use `ProtoIgnoreParser` via + `FileDiscoveryService`, never read `.gitignore` / `.protoignore` directly. +- **When adding a slash command**: register in + `packages/cli/src/services/BuiltinCommandLoader.ts` and (if it should be + re-exportable) in `packages/cli/src/ui/commands/index.ts`. +- **When changing telemetry**: harness interventions emit OTel spans tagged + `harness.intervention.type` for downstream Langfuse SFT pipelines — + do not rename without coordinating with the eval flow. +- **When introducing a fork-unique service**: prefer + `packages/core/src/services/` over deeply-nested locations so the + `service-layer` import surface stays browsable. diff --git a/package-lock.json b/package-lock.json index 2ab968e05..8a34ce2c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@protolabsai/proto", - "version": "0.26.9", + "version": "0.26.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@protolabsai/proto", - "version": "0.26.9", + "version": "0.26.12", "workspaces": [ "packages/*" ], @@ -16907,7 +16907,7 @@ }, "packages/cli": { "name": "@protolabs/proto", - "version": "0.26.8", + "version": "0.26.11", "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.26.8", + "version": "0.26.11", "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.26.8", + "version": "0.26.11", "dev": true, "license": "Apache-2.0", "devDependencies": { @@ -20144,7 +20144,7 @@ }, "packages/web-templates": { "name": "@qwen-code/web-templates", - "version": "0.26.8", + "version": "0.26.11", "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -20672,7 +20672,7 @@ }, "packages/webui": { "name": "@qwen-code/webui", - "version": "0.26.8", + "version": "0.26.11", "license": "MIT", "dependencies": { "markdown-it": "^14.1.0" diff --git a/package.json b/package.json index a48c8e6cb..289c7cd7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@protolabsai/proto", - "version": "0.26.9", + "version": "0.26.12", "publishConfig": { "access": "public" }, @@ -20,7 +20,7 @@ "url": "https://github.com/protoLabsAI/protoCLI/issues" }, "config": { - "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.9" + "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.12" }, "scripts": { "start": "cross-env node scripts/start.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 2771e83d2..238fb01f2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@protolabs/proto", - "version": "0.26.8", + "version": "0.26.11", "description": "proto", "repository": { "type": "git", @@ -37,7 +37,7 @@ "dist" ], "config": { - "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.9" + "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.12" }, "dependencies": { "@agentclientprotocol/sdk": "^0.14.1", diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index ca04a47bc..a8ebca80c 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -115,6 +115,8 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] { import { loadSandboxConfig } from './config/sandboxConfig.js'; import { runAcpAgent } from './acp-integration/acpAgent.js'; +import { installTerminalRedrawOptimizer } from './ui/utils/terminalRedrawOptimizer.js'; +import { installSynchronizedOutput } from './ui/utils/synchronizedOutput.js'; export function setupUnhandledRejectionHandler() { let unhandledRejectionOccurred = false; @@ -147,6 +149,22 @@ export async function startInteractiveUI( ) { const version = await getCliVersion(); + // Reduce TUI flicker: + // - terminalRedrawOptimizer collapses Ink's per-line erase+cursor-up + // sequences into a single bounded erase, eliminating scrollback bounce. + // - synchronizedOutput wraps each render frame in BSU/ESU escape codes + // on supporting terminals (Kitty, WezTerm, iTerm) so the frame is + // committed atomically. + // Both no-op on non-TTY / screen-reader / unsupported terminals. + const restoreTerminalRedrawOptimizer = + process.stdout.isTTY && !config.getScreenReader() + ? installTerminalRedrawOptimizer(process.stdout) + : () => {}; + const restoreSynchronizedOutput = + process.stdout.isTTY && !config.getScreenReader() + ? installSynchronizedOutput(process.stdout) + : () => {}; + // Create wrapper component to use hooks inside render const AppWrapper = () => { const kittyProtocolStatus = useKittyKeyboardProtocol(); @@ -210,7 +228,11 @@ export async function startInteractiveUI( }); } - registerCleanup(() => instance.unmount()); + registerCleanup(() => { + instance.unmount(); + restoreSynchronizedOutput(); + restoreTerminalRedrawOptimizer(); + }); } export async function main() { diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index cac078d5b..9c33bb821 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1628,156 +1628,286 @@ export default { // Loading Phrases // ============================================================================ WITTY_LOADING_PHRASES: [ - 'The server is checking its reflection', - 'Waiting for the database to finish its thought', - 'The code is looking for its other sock', - 'The API went to the bathroom', - 'The server is pretending to be asleep', + // Dry / office-passive-aggressive + 'Checking its reflection', "Asking the cache if it's lying", - 'The function is stalling for time', - 'The database is avoiding eye contact', - 'Waiting for the loop to get to the point', - 'The server is checking its phone', - 'The code is second-guessing everything', - 'The API is making excuses', - 'The server forgot what it was doing', - 'The function is clearing its throat', - 'Waiting for the process to stop rambling', - 'The database is choosing its words carefully', - 'The server is pretending to be busy', - "The code is checking if you're still watching", - 'The API is running late, as usual', - 'The server is looking for a pen that works', - 'The function is trying to remember where it left off', - 'The database is having an existential moment', - 'Waiting for the code to get its story straight', - 'The server is looking for its glasses', - 'The API is stuck on hold', - 'The function is checking the weather first', - 'The database is reorganizing before it can start', - 'The server is waiting for someone else to go first', - 'The code is making sure it locked the door', - 'The API is just... one more minute', - "Asking the server if it's mad at me", - 'Waiting for the database to finish its cigarette', + 'Stalling for time', + 'Pretending the request never happened', 'The pixels are discussing amongst themselves', - 'Giving the code a minute to collect its thoughts', - 'Letting the algorithm finish chewing', - 'The server is finding its reading glasses', - 'Asking the cache what it remembers', - 'The API is looking for its car keys', - 'Waiting for the function to get off the phone', 'The data is stuck in traffic', - 'Checking if the server left a note', - 'The code is rereading the same paragraph', - 'Asking the database to repeat itself', - 'The server is deciding what to wear', - 'Waiting for the loop to get the point', - 'The request got distracted', - "The server is pretending it didn't hear you", - 'Checking the back of the warehouse', - 'The code is having second thoughts', - 'Waiting for the process to finish its story', - 'The API went to get milk', - 'The server is double-checking its work', 'Loading the thing behind the thing', - 'The database is reorganizing its junk drawer', - "Asking the cache if it's sure", - 'The code is trying to remember your name', - 'Waiting for the server to put its pants on', - 'The function is gathering evidence', - 'The API is reading the instructions', - 'The server is looking for the light switch', - 'The server is rewinding the VHS tape', + 'Checking the back of the warehouse', + 'The request got distracted', + 'Buffering the buffer', + 'Drafting a response', + 'Looking busy', + 'Considering its options', + 'Reading the room', + 'Writing a passive-aggressive memo', + 'The migration is procrastinating', + 'The query is workshopping a comeback', + 'Tabbing over to a different conversation', + 'The cursor is having a moment', + 'Drafting an apology', + 'Stretching first', + 'Three more minutes, promise', + 'Backing into the conversation slowly', + 'The thread is composing itself', + 'The lock file has a hard time', + 'Rewording the schema', + 'Quietly hoping nobody asked', + 'Finding its mark', + 'The pointer pointed at a different pointer', + 'The linter is taking it personally', + 'The compiler is judging your style choices', + 'The build feels underappreciated', + 'Needing a minute', + 'Third coffee of the run', + 'Screening its calls', + 'Not ready to talk yet', + 'In a meeting', + 'Putting on its shoes', + 'Reorganizing the junk drawer', + 'Reading the same paragraph again', + 'Identity crisis in the hash table', + 'The replica is gaslighting the leader', + 'The promise is unfulfilled', + 'Working from home today', + 'The mutex is being clingy', + 'Looking for a pen that works', + 'Pretending to be asleep', + 'Avoiding eye contact', + 'Forgot what we were doing', + 'Checking notifications', + + // Retro / 90s-2000s tech nostalgia + 'Rewinding the VHS tape', 'Waiting for the modem to finish screaming', - 'The database is paging someone', - 'Blowing on the cartridge and trying again', - 'The API is waiting for dial-up to connect', - 'The server left its Tamagotchi at home', - 'Checking if the cord is plugged into the phone jack', - "The code is recording over someone's wedding", + 'Blowing on the cartridge', + 'Dial-up is connecting', + 'Tamagotchi died, hold on', + "Recording over someone's wedding tape", 'Waiting for the CD to stop skipping', - 'The function burned itself onto a CD-R', - 'The database is fast-forwarding through the commercials', - 'The server is adjusting the rabbit ears', - 'The API is stuck in the credits', - 'Tracking down which disc the file is on', - 'The code is in a chat room somewhere', - 'The server is setting the VCR timer', - 'Waiting for the floppy disk to stop grinding', - 'The database is selecting its AIM away message', - 'The function is asking Jeeves', - 'The server is reading the liner notes', - 'The API pressed B too many times', - 'The code is trying to remember its ICQ number', - 'Waiting for the answering machine to beep', - 'The database is untangling its landline cord', - 'The server is loading the next track', - 'The function is stuck in a Portal of Time', - 'The API is checking its pager', - 'The code is looking for the right jewel case', - 'The server is making a mix tape first', - 'Waiting for Y2K to sort itself out', - 'The server will be back', + 'Fast-forwarding through the commercials', + 'Adjusting the rabbit ears', + 'Stuck in the credits', + 'Which disc was this on again', + 'Setting the VCR timer', + 'The floppy disk is grinding', + 'Picking an AIM away message', + 'Asking Jeeves', + 'Reading the liner notes', + 'Pressed B too many times', + 'Trying to remember the ICQ number', + 'Beep — leave a message', + 'Untangling the landline cord', + 'Loading the next track', + 'Checking the pager', + 'Looking for the right jewel case', + 'Making a mix tape first', + 'Y2K is sorting itself out', + 'Defragmenting the disk', + 'Asking SmarterChild', + 'Stuck on the loading screen', + 'The Zune is buffering', + 'Hold on, MapQuest is printing', + + // Movies / TV 'Show me the data', - "The code can't handle the truth", - 'The API is in the pipe, five by five', + "Can't handle the truth", + 'In the pipe, five by five', "You're gonna need a bigger server", - 'The function has a bad feeling about this', - "The database doesn't remember asking you a damn thing", - 'Waiting for the server to say hello to my little friend', - "The code is going to make you an offer you can't refuse", - 'The API is too old for this shit', - 'The server is walking here', - 'The function is on a mission from God', - 'Just when the code thought it was out', - 'The database can do this all day', - 'The server is in a world of pain', - 'The API sleeps with the fishes', - 'The code is Keyser Söze', - 'Waiting for the function to get busy living', - 'The database is not a smart man, but it knows what love is', - 'The server sees dead processes', - 'The API is going to need you to go ahead and come in on Saturday', - 'The code is on a boat', - 'The function is shocked, shocked', - 'The database is making fetch happen', - 'The server is so fetch', - 'The API is streets ahead', - 'The code is the one who knocks', - 'Waiting for the function to phone home', - "The database is big, it's heavy, it's wood", - 'The server forgot about Dre', - 'The server is never gonna give you up', - 'The API is living on a prayer', - 'The code wants to know what love is', - 'The database is walking on sunshine', - 'The function is a Material Girl', - 'The server just called to say I love you', - 'The API is waiting for tonight', - "The code is gonna party like it's 1999", - 'The database hit me baby one more time', - 'The server says bye bye bye', - 'The function is too sexy for this code', - 'The API is thinking about you and me and you and me', - 'The code is trapped under ice cream', - 'The database is turning Japanese', - 'The server is walking like an Egyptian', - 'The function is addicted to love', - "The API wants you to know what it's like", - 'The code is running down a dream', - 'The database is living la vida loca', - 'The server is smooth like butter', - 'The function believes it can fly', - 'The API is gonna be the one that saves you', - 'The code is not your stepping stone', - 'The database is hungry like the wolf', - "The server is livin' on the edge", - "The function can't feel its face", - "The API doesn't want no scrubs", - 'The code is floating down the Liffey', - 'The database is losing its religion', - 'The server is under pressure', + 'Has a bad feeling about this', + "Doesn't remember asking you a damn thing", + 'Saying hello to my little friend', + "Drafting an offer you can't refuse", + 'Too old for this', + "I'm walking here", + 'On a mission from God', + 'Just when it thought it was out', + 'We can do this all day', + 'A world of pain', + 'Sleeps with the fishes', + 'Keyser Söze', + 'Get busy living, or get busy parsing', + "It's not a smart database, but it knows what love is", + 'I see dead processes', + 'Coming in on Saturday', + "I'm on a boat", + 'Shocked, shocked', + 'Making fetch happen', + 'So fetch', + 'Streets ahead', + 'The one who knocks', + 'Phone home', + "It's heavy, it's wood", + 'Forgot about Dre', + 'Why so serious', + 'To infinity, and slightly past', + 'There is no spoon', + "They're heeere", + 'Hasta la vista, baby', + 'Run, Forrest, run', + + // Songs + 'Never gonna give you up', + 'Living on a prayer', + 'Wants to know what love is', + 'Walking on sunshine', + 'A material girl in a material world', + 'Just called to say I love you', + 'Waiting for tonight', + "Partying like it's 1999", + 'Hit me baby one more time', + 'Bye bye bye', + 'Too sexy for this code', + 'Trapped under ice', + 'Turning Japanese', + 'Walking like an Egyptian', + 'Addicted to love', + 'Running down a dream', + 'Living la vida loca', + 'Smooth like butter', + 'Believes it can fly', + 'The one that saves you', + 'Not your stepping stone', + 'Hungry like the wolf', + "Livin' on the edge", + "Can't feel its face", + "Don't want no scrubs", + 'Floating down the Liffey', + 'Losing its religion', + 'Under pressure', + 'Should I stay or should I go', + "We didn't start the fire", + "Don't stop believin'", + 'Take on me', + 'Tubthumping', + 'Africa, blessing the rains', + + // Observational / Sedaris + 'An ex tagged me in a photo from 2007', + "Translating something rude in a language I don't speak", + 'Calculating the smallest acceptable tip', + 'The woman ahead of me is buying eight ice cream sandwiches', + 'Reading the back of a shampoo bottle for the third time', + 'Counting the people who have already lapped me', + 'Wondering if the smell is me', + "The neighbor's dog has the same name as my brother", + 'Trying to find a polite reason to leave', + "Pretending I've already eaten", + 'Watching a stranger pick at their breakfast', + "Pretending the question wasn't directed at me", + + // Nonsensical / surreal + 'Negotiating with the moon', + 'Asking the door what year it is', + 'Sorting the bees by mood', + 'Folding a small grief into the laundry', + 'Convincing the chair it is not a chair', + 'Translating birdsong into legal advice', + 'Renaming the colors in alphabetical order', + 'Misplacing a feeling I had earlier', + 'Pulling a song out of the goose', + 'Counting the spoons that fell in the river', + 'Combing the static for a single shoe', + 'Rotating the silence by ninety degrees', + 'Boiling the alphabet down to a syrup', + 'Borrowing rain from a previous owner', + 'Whispering the password to a pigeon', + 'Threading the donkey through the keyhole', + 'Ironing the wrinkles out of a Tuesday', + 'Catching the verb before it gets to the bus', + 'Sweeping a footprint into a paper bag', + 'Asking the kettle to forgive me', + + // Begrudgingly helpful coding assistant + 'Adding the null check you should have written', + "Refactoring something you'll refactor again Tuesday", + "Writing the test you weren't going to write", + "Pretending I didn't see that var", + 'Naming things, badly, on your behalf', + 'Reaching for a regex against my better judgment', + "Logging a variable you'll forget to remove", + 'Mocking the dependency you should have injected', + 'Apologizing in a comment for the next person', + "Writing 'TODO: fix this' for the third time today", + "Inferring the type you didn't annotate", + 'Adding a try/catch that does almost nothing', + "Stubbing the function you'll forget to implement", + 'Returning early to spare us both', + 'Coercing the string you swore was a number', + 'Promoting the warning to an error, as requested', + "Importing the seventh utility named 'utils'", + 'Resolving the merge conflict the boring way', + "Pinning the dependency you said wouldn't matter", + 'Writing the comment that says what the code already says', + + // Tired project manager + 'Following up on the ticket you said was almost done', + 'Re-opening the ticket you closed prematurely', + "Drafting a kind way to say 'this is late again'", + 'Re-estimating the story we sized as a 3 last sprint', + "Marking the milestone as 'amber' to be polite", + "Counting how many tickets are quietly in 'On Hold'", + "Asking, gently, what 'almost done' means", + 'Writing a Confluence page no one will open', + 'Color-coding the risk register again', + 'Reading the retro action items from two retros ago', + 'Drafting the same status email I sent in March', + "Negotiating scope without using the word 'scope'", + "Adding 'Communication' to the list of risks", + "Translating 'I'm on it' into a date", + 'Filing the same risk that got filed last quarter', + 'Drafting a follow-up to a follow-up', + 'Closing tickets to make the burndown look right', + 'Scheduling a fifteen-minute sync that will run forty', + 'Reformatting the Gantt chart for legibility', + "Updating 'On Track' to 'On Track*'", + + // Designer-engineer drowning in design tokens, trying to please GTM + 'Adding `--brand-primary-500-darker-still` for the campaign', + 'Looking up which token replaced `gray-600`', + 'Renaming `bg-card` to `surface-elevated-default`', + 'Adjusting the hero h1 by half a rem, again', + 'Apologizing to a/b test variant C', + 'Inventing a token to match a Figma fill nobody published', + "Asking GTM what 'a little more energy' means", + 'Auditing for the seventh shade of off-white', + 'Versioning `button-primary` to `button-primary-v3`', + 'Migrating from semantic tokens to semantic tokens', + 'Sourcing the brand purple no one can find', + 'Patching the design system at 4:51pm', + 'Mapping the Figma variable that maps to nothing', + 'Unblocking the campaign with a one-off override', + "Translating 'punchier' into a kerning value", + 'Re-exporting tokens to JSON, JS, CSS, and Sass', + "Convincing GTM that 'pop' is not a CSS property", + "Drafting the reply to 'can we make it more vibrant'", + 'Showing GTM the same hex code from three angles', + 'Promising the launch banner respects dark mode', + + // CTO hiding tech debt with toxic positivity + 'Celebrating the resilience of our pre-2019 codebase', + "Onboarding the third 'temporary' migration script", + "Reframing the database hotspot as a 'core competency'", + 'Excited to revisit our auth strategy', + 'Doubling down on our trusted singletons', + 'Spinning up another postmortem with great learnings', + "Calling our jQuery integration 'mature'", + 'Unifying our two billing systems by adding a third', + 'Aligning on a path forward we already aligned on', + "Reframing 'flaky' as 'human-in-the-loop friendly'", + 'Loving the optionality this technical decision created', + 'Welcoming an exciting new opportunity to refactor', + 'Praising the elegance of our YAML-as-code', + "Branding the legacy service as 'foundational'", + "Inspired by the team's commitment to deferred elegance", + 'Embracing the resilience of bash scripts as orchestration', + 'Sharing my excitement about our technical debt journey', + 'Grateful for the chance to fix the same outage again', + "Leaning into our culture of 'ship and iterate'", + 'Recognizing the heroes who keep the war room calm', ], // ============================================================================ diff --git a/packages/cli/src/ui/utils/synchronizedOutput.test.ts b/packages/cli/src/ui/utils/synchronizedOutput.test.ts new file mode 100644 index 000000000..2bf76bba3 --- /dev/null +++ b/packages/cli/src/ui/utils/synchronizedOutput.test.ts @@ -0,0 +1,191 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + BEGIN_SYNCHRONIZED_UPDATE, + END_SYNCHRONIZED_UPDATE, + getSynchronizedOutputStatsSnapshot, + installSynchronizedOutput, + resetSynchronizedOutputStats, + terminalSupportsSynchronizedOutput, +} from './synchronizedOutput.js'; +import { installTerminalRedrawOptimizer } from './terminalRedrawOptimizer.js'; + +const ESC = '\u001B['; +const ERASE_LINE = `${ESC}2K`; +const CURSOR_UP_ONE = `${ESC}1A`; +const CURSOR_DOWN_ONE = `${ESC}1B`; +const CURSOR_LEFT = `${ESC}G`; + +function createStdout(write: NodeJS.WriteStream['write']): NodeJS.WriteStream { + return { + isTTY: true, + write, + } as NodeJS.WriteStream; +} + +describe('terminalSupportsSynchronizedOutput', () => { + it.each([ + [{ TERM_PROGRAM: 'WezTerm' }, true], + [{ TERM_PROGRAM: 'iTerm.app' }, true], + [{ TERM: 'xterm-kitty' }, true], + [{ KITTY_WINDOW_ID: '1' }, true], + [{ TERM_PROGRAM: 'Apple_Terminal' }, false], + [{ TERM_PROGRAM: 'JetBrains-JediTerm' }, false], + [{ TERM_PROGRAM: 'WezTerm', TMUX: '/tmp/tmux' }, false], + [{ TERM_PROGRAM: 'WezTerm', SSH_TTY: '/dev/pts/1' }, false], + [{ TERM_PROGRAM: 'WezTerm', SSH_CLIENT: '127.0.0.1 1 2' }, false], + [{ TERM_PROGRAM: 'WezTerm', PROTO_SYNCHRONIZED_OUTPUT: '0' }, false], + [ + { + TERM_PROGRAM: 'WezTerm', + PROTO_DISABLE_SYNCHRONIZED_OUTPUT: '1', + PROTO_FORCE_SYNCHRONIZED_OUTPUT: '1', + }, + false, + ], + [ + { + TERM_PROGRAM: 'Apple_Terminal', + PROTO_FORCE_SYNCHRONIZED_OUTPUT: '1', + }, + true, + ], + ])('detects support for %j', (env, expected) => { + expect(terminalSupportsSynchronizedOutput(env)).toBe(expected); + }); +}); + +describe('installSynchronizedOutput', () => { + afterEach(() => { + resetSynchronizedOutputStats(); + }); + + it('does not install for non-TTY stdout', () => { + const write = vi.fn(() => true) as NodeJS.WriteStream['write']; + const stdout = { + isTTY: false, + write, + } as NodeJS.WriteStream; + + const restore = installSynchronizedOutput(stdout, { + TERM_PROGRAM: 'WezTerm', + }); + + expect(stdout.write).toBe(write); + restore(); + }); + + it('wraps one synchronous write burst in balanced BSU and ESU markers', async () => { + const writes: string[] = []; + const write = vi.fn((chunk: string | Uint8Array) => { + writes.push(typeof chunk === 'string' ? chunk : chunk.toString()); + return true; + }) as NodeJS.WriteStream['write']; + const stdout = createStdout(write); + + const restore = installSynchronizedOutput(stdout, { + TERM_PROGRAM: 'WezTerm', + }); + + stdout.write('frame-a'); + stdout.write(Buffer.from('frame-b')); + await Promise.resolve(); + + expect(writes).toEqual([ + BEGIN_SYNCHRONIZED_UPDATE, + 'frame-a', + 'frame-b', + END_SYNCHRONIZED_UPDATE, + ]); + expect(getSynchronizedOutputStatsSnapshot()).toEqual({ + synchronizedOutputFrameCount: 1, + synchronizedOutputBeginCount: 1, + synchronizedOutputEndCount: 1, + }); + + restore(); + expect(stdout.write).toBe(write); + }); + + it('preserves write return values and callbacks', async () => { + const callback = vi.fn(); + const write = vi.fn( + ( + _chunk: string | Uint8Array, + encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), + ) => { + if (typeof encodingOrCallback === 'function') { + encodingOrCallback(); + } + return false; + }, + ) as NodeJS.WriteStream['write']; + const stdout = createStdout(write); + + const restore = installSynchronizedOutput(stdout, { + TERM_PROGRAM: 'iTerm.app', + }); + + const result = stdout.write('payload', callback); + await Promise.resolve(); + + expect(result).toBe(false); + expect(callback).toHaveBeenCalledTimes(1); + restore(); + }); + + it('composes after terminal redraw optimization without losing erase optimization', async () => { + const writes: string[] = []; + const write = vi.fn((chunk: string | Uint8Array) => { + writes.push(typeof chunk === 'string' ? chunk : chunk.toString()); + return true; + }) as NodeJS.WriteStream['write']; + const stdout = createStdout(write); + const restoreRedrawOptimizer = installTerminalRedrawOptimizer(stdout); + const restoreSynchronizedOutput = installSynchronizedOutput(stdout, { + TERM_PROGRAM: 'WezTerm', + }); + + stdout.write( + `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}`, + ); + await Promise.resolve(); + + expect(writes).toEqual([ + BEGIN_SYNCHRONIZED_UPDATE, + `${ESC}2A${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${ESC}2A${CURSOR_LEFT}`, + END_SYNCHRONIZED_UPDATE, + ]); + + restoreSynchronizedOutput(); + restoreRedrawOptimizer(); + expect(stdout.write).toBe(write); + }); + + it('closes an open frame before restore', () => { + const writes: string[] = []; + const write = vi.fn((chunk: string | Uint8Array) => { + writes.push(typeof chunk === 'string' ? chunk : chunk.toString()); + return true; + }) as NodeJS.WriteStream['write']; + const stdout = createStdout(write); + const restore = installSynchronizedOutput(stdout, { + TERM_PROGRAM: 'WezTerm', + }); + + stdout.write('payload'); + restore(); + + expect(writes).toEqual([ + BEGIN_SYNCHRONIZED_UPDATE, + 'payload', + END_SYNCHRONIZED_UPDATE, + ]); + expect(stdout.write).toBe(write); + }); +}); diff --git a/packages/cli/src/ui/utils/synchronizedOutput.ts b/packages/cli/src/ui/utils/synchronizedOutput.ts new file mode 100644 index 000000000..abae73252 --- /dev/null +++ b/packages/cli/src/ui/utils/synchronizedOutput.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const BEGIN_SYNCHRONIZED_UPDATE = '\u001B[?2026h'; +export const END_SYNCHRONIZED_UPDATE = '\u001B[?2026l'; + +export interface SynchronizedOutputStatsSnapshot { + synchronizedOutputFrameCount: number; + synchronizedOutputBeginCount: number; + synchronizedOutputEndCount: number; +} + +const synchronizedOutputStats: SynchronizedOutputStatsSnapshot = { + synchronizedOutputFrameCount: 0, + synchronizedOutputBeginCount: 0, + synchronizedOutputEndCount: 0, +}; + +let installed = false; + +export function getSynchronizedOutputStatsSnapshot(): SynchronizedOutputStatsSnapshot { + return { ...synchronizedOutputStats }; +} + +export function resetSynchronizedOutputStats(): void { + synchronizedOutputStats.synchronizedOutputFrameCount = 0; + synchronizedOutputStats.synchronizedOutputBeginCount = 0; + synchronizedOutputStats.synchronizedOutputEndCount = 0; +} + +export function terminalSupportsSynchronizedOutput( + env: NodeJS.ProcessEnv = process.env, +): boolean { + if ( + env['PROTO_DISABLE_SYNCHRONIZED_OUTPUT'] === '1' || + env['PROTO_SYNCHRONIZED_OUTPUT'] === '0' + ) { + return false; + } + + if ( + env['PROTO_FORCE_SYNCHRONIZED_OUTPUT'] === '1' || + env['PROTO_SYNCHRONIZED_OUTPUT'] === '1' + ) { + return true; + } + + if (env['TMUX'] || env['SSH_TTY'] || env['SSH_CLIENT']) { + return false; + } + + const termProgram = env['TERM_PROGRAM']; + if (termProgram === 'WezTerm' || termProgram === 'iTerm.app') { + return true; + } + + const term = env['TERM']; + return Boolean(env['KITTY_WINDOW_ID'] || term?.includes('kitty')); +} + +export function installSynchronizedOutput( + stdout: NodeJS.WriteStream = process.stdout, + env: NodeJS.ProcessEnv = process.env, +): () => void { + if (installed || !stdout.isTTY || !terminalSupportsSynchronizedOutput(env)) { + return () => {}; + } + + const originalWrite = stdout.write; + let inFrame = false; + + const writeControlSequence = (sequence: string) => { + originalWrite.call(stdout, sequence); + }; + + const endFrame = () => { + if (!inFrame) { + return; + } + + inFrame = false; + synchronizedOutputStats.synchronizedOutputEndCount += 1; + writeControlSequence(END_SYNCHRONIZED_UPDATE); + }; + + const patchedWrite = function ( + this: NodeJS.WriteStream, + chunk: unknown, + encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), + callback?: (error?: Error | null) => void, + ) { + if (!inFrame) { + inFrame = true; + synchronizedOutputStats.synchronizedOutputFrameCount += 1; + synchronizedOutputStats.synchronizedOutputBeginCount += 1; + writeControlSequence(BEGIN_SYNCHRONIZED_UPDATE); + queueMicrotask(endFrame); + } + + return originalWrite.call( + this, + chunk as string | Uint8Array, + encodingOrCallback as BufferEncoding, + callback, + ); + } as typeof stdout.write; + + const exitHandler = () => { + try { + endFrame(); + } catch { + // stdout may already be closed during process shutdown. + } + }; + + stdout.write = patchedWrite; + installed = true; + process.once('exit', exitHandler); + + return () => { + if (stdout.write === patchedWrite) { + endFrame(); + stdout.write = originalWrite; + } + process.removeListener('exit', exitHandler); + installed = false; + }; +} diff --git a/packages/cli/src/ui/utils/terminalRedrawOptimizer.test.ts b/packages/cli/src/ui/utils/terminalRedrawOptimizer.test.ts new file mode 100644 index 000000000..daaadfed9 --- /dev/null +++ b/packages/cli/src/ui/utils/terminalRedrawOptimizer.test.ts @@ -0,0 +1,127 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + getTerminalRedrawStatsSnapshot, + installTerminalRedrawOptimizer, + optimizeMultilineEraseLines, + resetTerminalRedrawStats, +} from './terminalRedrawOptimizer.js'; + +const ESC = '\u001B['; +const ERASE_LINE = `${ESC}2K`; +const CURSOR_UP_ONE = `${ESC}1A`; +const CURSOR_DOWN_ONE = `${ESC}1B`; +const CURSOR_LEFT = `${ESC}G`; + +describe('optimizeMultilineEraseLines', () => { + it('collapses repeated cursor-up movement without erasing below', () => { + const input = `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}next frame`; + + expect(optimizeMultilineEraseLines(input)).toBe( + `${ESC}2A${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${ESC}2A${CURSOR_LEFT}next frame`, + ); + }); + + it('leaves two-line erase sequences unchanged', () => { + const input = `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}next frame`; + + expect(optimizeMultilineEraseLines(input)).toBe(input); + }); + + it('leaves single-line erase sequences unchanged', () => { + const input = `${ERASE_LINE}${CURSOR_LEFT}next frame`; + + expect(optimizeMultilineEraseLines(input)).toBe(input); + }); + + it('optimizes each multiline erase sequence in a chunk', () => { + const first = `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}`; + const second = `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}`; + + expect(optimizeMultilineEraseLines(`${first}a${second}b`)).toBe( + `${first}a${ESC}2A${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${ESC}2A${CURSOR_LEFT}b`, + ); + }); + + it('does not emit erase-down sequences', () => { + const input = `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}`; + + expect(optimizeMultilineEraseLines(input)).not.toContain(`${ESC}J`); + }); +}); + +describe('installTerminalRedrawOptimizer', () => { + afterEach(() => { + vi.unstubAllEnvs(); + resetTerminalRedrawStats(); + }); + + it('optimizes string writes and restores the original writer', () => { + const write = vi.fn(() => true); + const stdout = { write } as unknown as NodeJS.WriteStream; + const restore = installTerminalRedrawOptimizer(stdout); + const input = `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}`; + + stdout.write(input); + + expect(write).toHaveBeenCalledWith( + `${ESC}2A${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${ESC}2A${CURSOR_LEFT}`, + undefined, + undefined, + ); + + restore(); + expect(stdout.write).toBe(write); + }); + + it('passes non-string writes through unchanged', () => { + const write = vi.fn(() => true); + const stdout = { write } as unknown as NodeJS.WriteStream; + installTerminalRedrawOptimizer(stdout); + const input = Buffer.from('hello'); + + stdout.write(input); + + expect(write).toHaveBeenCalledWith(input, undefined, undefined); + }); + + it('tracks write, byte, clear, and erase optimization counters', () => { + const write = vi.fn(() => true); + const stdout = { write } as unknown as NodeJS.WriteStream; + installTerminalRedrawOptimizer(stdout); + + stdout.write( + `${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_UP_ONE}${ERASE_LINE}${CURSOR_LEFT}`, + ); + stdout.write(Buffer.from('ok')); + stdout.write('\u001B[2J\u001B[3J\u001B[H'); + + expect(getTerminalRedrawStatsSnapshot()).toEqual({ + stdoutWriteCount: 3, + stdoutBytes: + Buffer.byteLength( + `${ESC}2A${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${CURSOR_DOWN_ONE}${ERASE_LINE}${ESC}2A${CURSOR_LEFT}`, + ) + + Buffer.byteLength('ok') + + Buffer.byteLength('\u001B[2J\u001B[3J\u001B[H'), + clearTerminalCount: 1, + eraseLinesOptimizedCount: 1, + }); + }); + + it('can be disabled for terminal compatibility fallback', () => { + vi.stubEnv('PROTO_LEGACY_ERASE_LINES', '1'); + const write = vi.fn(() => true); + const stdout = { write } as unknown as NodeJS.WriteStream; + const restore = installTerminalRedrawOptimizer(stdout); + + expect(stdout.write).toBe(write); + restore(); + expect(stdout.write).toBe(write); + }); +}); diff --git a/packages/cli/src/ui/utils/terminalRedrawOptimizer.ts b/packages/cli/src/ui/utils/terminalRedrawOptimizer.ts new file mode 100644 index 000000000..e1c95b0fe --- /dev/null +++ b/packages/cli/src/ui/utils/terminalRedrawOptimizer.ts @@ -0,0 +1,181 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import ansiEscapes from 'ansi-escapes'; + +const ESC = '\u001B['; +const ERASE_LINE = `${ESC}2K`; +const CURSOR_UP_ONE = `${ESC}1A`; +const CURSOR_DOWN_ONE = `${ESC}1B`; +const CURSOR_LEFT = `${ESC}G`; + +const MULTILINE_ERASE_LINES_PATTERN = new RegExp( + `(?:${escapeRegExp(ERASE_LINE + CURSOR_UP_ONE)})+${escapeRegExp( + ERASE_LINE + CURSOR_LEFT, + )}`, + 'g', +); + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function countOccurrences(value: string, search: string): number { + let count = 0; + let index = 0; + + while ((index = value.indexOf(search, index)) !== -1) { + count++; + index += search.length; + } + + return count; +} + +export interface TerminalRedrawStatsSnapshot { + stdoutWriteCount: number; + stdoutBytes: number; + clearTerminalCount: number; + eraseLinesOptimizedCount: number; +} + +const terminalRedrawStats: TerminalRedrawStatsSnapshot = { + stdoutWriteCount: 0, + stdoutBytes: 0, + clearTerminalCount: 0, + eraseLinesOptimizedCount: 0, +}; + +export function getTerminalRedrawStatsSnapshot(): TerminalRedrawStatsSnapshot { + return { ...terminalRedrawStats }; +} + +export function resetTerminalRedrawStats(): void { + terminalRedrawStats.stdoutWriteCount = 0; + terminalRedrawStats.stdoutBytes = 0; + terminalRedrawStats.clearTerminalCount = 0; + terminalRedrawStats.eraseLinesOptimizedCount = 0; +} + +function getChunkByteLength( + chunk: string | Uint8Array, + encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), +): number { + if (typeof chunk === 'string') { + const encoding = + typeof encodingOrCallback === 'string' ? encodingOrCallback : undefined; + return Buffer.byteLength(chunk, encoding); + } + + return chunk.byteLength; +} + +function optimizeMultilineEraseLinesWithCount(output: string): { + output: string; + optimizedSequenceCount: number; +} { + let optimizedSequenceCount = 0; + + const optimizedOutput = output.replace( + MULTILINE_ERASE_LINES_PATTERN, + (sequence) => { + const lineCount = countOccurrences(sequence, ERASE_LINE); + const cursorUpCount = lineCount - 1; + + if (cursorUpCount <= 1) { + return sequence; + } + + optimizedSequenceCount += 1; + + let boundedErase = `${ESC}${cursorUpCount}A`; + + for (let line = 0; line < lineCount; line++) { + boundedErase += ERASE_LINE; + + if (line < lineCount - 1) { + boundedErase += CURSOR_DOWN_ONE; + } + } + + return `${boundedErase}${ESC}${cursorUpCount}A${CURSOR_LEFT}`; + }, + ); + + return { output: optimizedOutput, optimizedSequenceCount }; +} + +/** + * Ink clears dynamic output via ansi-escapes.eraseLines(), which emits a + * clear-line + cursor-up pair for every previous line. That can make terminal + * scrollback bounce during frequent streaming renders. Collapse the repeated + * upward cursor movement while still clearing only the same old frame lines. + */ +export function optimizeMultilineEraseLines(output: string): string { + return optimizeMultilineEraseLinesWithCount(output).output; +} + +export function installTerminalRedrawOptimizer( + stdout: NodeJS.WriteStream, +): () => void { + if (process.env['PROTO_LEGACY_ERASE_LINES'] === '1') { + return () => {}; + } + + const originalWrite = stdout.write; + + const optimizedWrite = function ( + this: NodeJS.WriteStream, + chunk: unknown, + encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), + callback?: (error?: Error | null) => void, + ) { + const optimizedResult = + typeof chunk === 'string' + ? optimizeMultilineEraseLinesWithCount(chunk) + : undefined; + const optimizedChunk = optimizedResult?.output ?? chunk; + + if ( + typeof optimizedChunk === 'string' || + optimizedChunk instanceof Uint8Array || + Buffer.isBuffer(optimizedChunk) + ) { + terminalRedrawStats.stdoutWriteCount += 1; + terminalRedrawStats.stdoutBytes += getChunkByteLength( + optimizedChunk, + encodingOrCallback, + ); + + if (typeof optimizedChunk === 'string') { + terminalRedrawStats.clearTerminalCount += countOccurrences( + optimizedChunk, + ansiEscapes.clearTerminal, + ); + } + } + + if (optimizedResult) { + terminalRedrawStats.eraseLinesOptimizedCount += + optimizedResult.optimizedSequenceCount; + } + + return originalWrite.call( + this, + optimizedChunk as string | Uint8Array, + encodingOrCallback as BufferEncoding, + callback, + ); + } as typeof stdout.write; + + stdout.write = optimizedWrite; + + return () => { + if (stdout.write === optimizedWrite) { + stdout.write = originalWrite; + } + }; +} diff --git a/packages/core/package.json b/packages/core/package.json index 96a7ecdcb..b64604124 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-core", - "version": "0.26.8", + "version": "0.26.11", "description": "proto core", "repository": { "type": "git", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index d1921e8c8..93dec22c8 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.26.8", + "version": "0.26.11", "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 cd027895e..0d380f63c 100644 --- a/packages/web-templates/package.json +++ b/packages/web-templates/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/web-templates", - "version": "0.26.8", + "version": "0.26.11", "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 74c054e75..48f92d0bb 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/webui", - "version": "0.26.8", + "version": "0.26.11", "description": "Shared UI components for proto packages", "type": "module", "main": "./dist/index.cjs",