Skip to content

feat(question): tighten dock cap, schema bounds, and tool description#245

Merged
Astro-Han merged 6 commits into
devfrom
worktree-claude+question-tool-pr-a
Apr 26, 2026
Merged

feat(question): tighten dock cap, schema bounds, and tool description#245
Astro-Han merged 6 commits into
devfrom
worktree-claude+question-tool-pr-a

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #216 and #188 with four reversible commits:

  • C1 feat(question) — schema bounds (question.max(200), header.max(30), option.label.max(50), option.description.max(50), options.min(2).max(4), questions.min(1).max(4)), Info.refine(custom !== false || options.length > 0), every bound carries an actionable hint message so the LLM gets a structured repair path on rejection. Tool description rewritten with When / NOT / Examples / Format / Content routing sections (inspired by Claude Code's EnterPlanMode); Content routing teaches the model to stream long framing as normal output before invoking the tool.
  • C2 fix(ui) — replace the dynamic measure() (which fell back to 100dvh when sticky-class detection failed, the v0.2.12 footer-off-screen repro) with a static max-height: min(70dvh, calc(100dvh - 180px)). The 180px buffer is the PawWork session top chrome stack: 44 titlebar + 48 tabs + 48 sticky session title + 40 safety margin. No JS measurement, no ResizeObserver, no CSS variables.
  • C3a feat(ui) — honor Question.Info.custom flag: wrap the type-your-own-answer slot in <Show when={q.custom !== false}>, update count() / pickFocus() / selectOption(), and clear cache for tabs whose schema disallows custom (avoids stale focus on remount).
  • C3b feat(ui) — inline mode chip in header: {n} of {total} questions · {Pick 1 | Pick multiple}. Mode renders weight 600 with flex-shrink:0 so it survives ellipsis on narrow widths. Drop the dedicated [data-slot="question-hint"] row. Tighten en + zh hint copy: Pick 1 / Pick multiple / 选 1 个 / 可选多个. Other 16 locales follow upstream sync.

Why

#216 reported the question dock blocking the footer at small viewport heights (the measure() fallback made it render at full viewport when sticky-class detection failed). #188 asked for clearer triggers and tighter formatting so the model invokes the tool more confidently and writes shorter, more clickable questions. Both ship together because they share the dock TSX surface and the schema-versus-prompt discipline; splitting into separate PRs would have meant rebasing the dock changes twice.

Related Issue

Closes #216
Closes #188

How To Verify

bun --cwd packages/opencode test test/question/schema.test.ts   # 16 pass / 0 fail
bun --cwd packages/opencode typecheck                            # 0 errors
bun --cwd packages/app typecheck                                 # 0 errors
bun --cwd packages/app test                                      # 577 pass / 0 fail

Manual UI smoke (passed locally on macOS arm64 dev:desktop):

  • Dock cap (C2) — default window: dock fits ~70dvh, footer visible without scroll. ~480px-tall window: footer still visible (100dvh - 180 = 300px budget). 1080px tall: dock caps at ~70dvh ≈ 756px, internal scroll for overflow options.
  • Header inline mode (C3b) — single-choice question: 2 of 4 questions · Pick 1 (zh: 第 2 题 / 共 4 题 · 选 1 个). Multi-choice: ... · Pick multiple / 可选多个. ~480px-wide window: title ellipsizes, mode chip remains fully visible (flex-shrink:0), progress dots stay aligned right. No dedicated hint row below the question text.
  • Custom flag (C3a) — default request keeps the type-your-own-answer slot. Request with custom: false renders no textarea; keyboard navigation does not focus an empty trailing slot.
  • Schema enforcement (C1) — model with the new schema naturally writes 33–178 char questions on first try (verified via session DB inspection); a deliberately-long 250x payload was rejected with Question is too long (max 200 chars)... and the model self-corrected. 5-option request rejected with Each question allows at most 4 options....

Performance check from session export: average reasoning per turn 96 tok across 12 follow-up turns; first-call schema orientation 615 tok one-time, paid into the prompt cache. No simplification warranted.

Screenshots or Recordings

Verified on macOS dev:desktop. Header structure changes captured in spec at the linked issue comment; dock cap behavior is non-visual (footer-visible regression check).

Checklist

  • I linked the related issue, or stated why there is no issue
  • This PR has type, scope, and priority labels, or I requested maintainer labeling
  • I listed the relevant verification steps, including tests when behavior changed
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for desktop, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, or generated/local file changes when relevant
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • New Features

    • Moved progress indicator and mode hint into the question header.
  • Bug Fixes

    • Custom-answer control is only shown, focusable, and editable when supported; opening is blocked when not allowed.
    • Validation tightened for question text, headers, option labels/descriptions, and question counts (1–4 / 2–4 limits).
  • Documentation

    • Updated question tool guidance with clearer decision criteria and input rules.
  • UI/Style

    • Removed dynamic prompt resizing, refined header layout, and updated selection hint text.
  • Tests

    • Added schema and tool tests covering the new validation rules.
  • Chores

    • Enforced minimum main window dimensions.

@Astro-Han Astro-Han added enhancement New feature or request P1 High priority ui Design system and user interface harness Model harness, prompts, tool descriptions, and session mechanics labels Apr 26, 2026
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 25ce1ad6-ee08-497e-a435-f107070be990

📥 Commits

Reviewing files that changed from the base of the PR and between b66689a and 1a49534.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (13)
  • packages/app/src/pages/session/composer/session-question-dock.tsx
  • packages/desktop-electron/src/main/windows.ts
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/server/instance/experimental.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/src/tool/question.txt
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/sdk/openapi.json
  • packages/ui/src/components/message-part.css
  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/zh.ts
📝 Walkthrough

Walkthrough

Gates custom-answer UI per question metadata, removes prompt resize/measurement plumbing, rebuilds cached custom state on cleanup, moves selection hint into the header and caps dock height, tightens Question Zod schemas and tool limits, updates tests/docs/i18n, and sets a minimum Electron window size.

Changes

Cohort / File(s) Summary
Question dock component
packages/app/src/pages/session/composer/session-question-dock.tsx
Adds customAllowed gating (hide/custom-slot only when question supports it); prevents opening custom editor when disallowed; adjusts keyboard focus/navigation to exclude unsupported custom slots; removes measurement/resize/RAF/ResizeObserver logic; rebuilds cached custom/customOn arrays to total() length on cleanup; moves multi/single hint into header.
Styling / UI constraints
packages/ui/src/components/message-part.css
Replaces prompt height fallback with min(70dvh, calc(100dvh - 180px)); refactors question header to baseline-aligned flex with ellipsis and dedicated header sub-slot styles; removes old question-hint rules.
Schema & tool validation
packages/opencode/src/question/index.ts, packages/opencode/src/tool/question.ts
Tightens Zod validations: Option.label/description max 50 chars; Info.question max 200; Info.header max 30; Info.options min 2 / max 4; Question.Request.questions and tool parameters.questions constrained to 1–4 items; updates Question.Prompt derivation/comment.
Tool doc / guidance
packages/opencode/src/tool/question.txt
Rewrites guidance into a prescriptive decision protocol: when/how to call the tool, explicit field constraints (lengths, option counts, multiple/custom rules), batching limits, and streaming guidance for long context.
Tests
packages/opencode/test/question/schema.test.ts, packages/opencode/test/tool/question.test.ts
Adds schema tests for label/description/question/header length bounds and options/questions cardinality and error-message contents; updates tool test fixture to include two options.
Internationalization
packages/ui/src/i18n/en.ts, packages/ui/src/i18n/zh.ts
Changes selection hint strings to shorter forms: English "Pick multiple" / "Pick 1" and corresponding Chinese phrasing.
Desktop window constraints
packages/desktop-electron/src/main/windows.ts
Sets minWidth: 480 and minHeight: 480 on the main Electron BrowserWindow.
Server query preprocessing
packages/opencode/src/server/instance/experimental.ts
Applies .optional() after z.preprocess(...) for the /session cursor validator so missing/empty cursor inputs are handled at the top level of the preprocess pipeline.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

P2, app

Poem

🐰 I trimmed the dock and tucked the stray,
hid extra slots that shouldn't play,
bumped hints up high and capped the view,
tightened rules for questions, too —
hop-hop, neat changes — small and true.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main changes: tightened schema bounds, dock height cap, and improved tool description, all core to the PR objectives.
Description check ✅ Passed The description comprehensively covers all required sections: Summary with four commits, Why (referencing issues #216 and #188), Related Issue links, How To Verify with commands and manual UI testing, and a completed Checklist.
Linked Issues check ✅ Passed The PR fully addresses both linked issues: #216 is resolved by C2's static CSS cap replacing dynamic measure(); #188 is addressed by C1's schema bounds, C3b's inline mode hint, and C3a's custom-flag support, with verification confirming all objectives met.
Out of Scope Changes check ✅ Passed All code changes directly support the PR objectives: schema bounds and validation (C1), dock sizing fix (C2), custom-flag support and header refactor (C3a/C3b), minimum window dimensions for desktop, cursor validator adjustment, and plan.ts copy update are all necessary for a cohesive question-tool improvement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-claude+question-tool-pr-a

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces stricter validation for the question tool by adding character limits and constraints on the number of options to the Zod schemas. It also updates the LLM documentation and refines the SessionQuestionDock UI to handle disabled custom answers and improve layout management. Feedback identifies a critical runtime error caused by calling .omit() on a refined Zod schema and notes a redundant validation check in the schema definition.

Comment thread packages/opencode/src/question/index.ts
Comment thread packages/opencode/src/question/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/test/question/schema.test.ts`:
- Around line 37-39: Add a boundary acceptance test to assert Info.header
accepts exactly 30 characters: create a test that calls Question.Info.parse with
{ ...validInfo, header: "x".repeat(30) } and expects it not to throw (or to
equal the parsed object). Specifically add a test alongside the existing header
rejection case that uses Question.Info.parse, the validInfo fixture, and the
Info.header constraint to confirm 30-char headers are valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a87422a5-5d9b-48a3-87de-18424bfcfed4

📥 Commits

Reviewing files that changed from the base of the PR and between 13ffaf3 and 5aac330.

📒 Files selected for processing (7)
  • packages/app/src/pages/session/composer/session-question-dock.tsx
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/tool/question.txt
  • packages/opencode/test/question/schema.test.ts
  • packages/ui/src/components/message-part.css
  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/zh.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: typecheck
  • GitHub Check: unit-desktop
  • GitHub Check: unit-opencode
  • GitHub Check: unit-app
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (3)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/question/schema.test.ts
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🧠 Learnings (15)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
📚 Learning: 2026-04-22T05:32:29.012Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 98
File: packages/desktop-electron/src/main/menu-labels.ts:1-2
Timestamp: 2026-04-22T05:32:29.012Z
Learning: In Astro-Han/pawwork, the app i18n layer (`packages/app/src/i18n/`) only contains `en.ts` and `zh.ts`, and `normalizeLocale` (in `packages/app/src/context/language.tsx`) only returns `"en"` or `"zh"`. The desktop `MenuLocale = "en" | "zh"` union in `packages/desktop-electron/src/main/menu-labels.ts` is intentionally limited to these two locales and is not a broader restriction — do not flag it as overly restrictive or suggest adding other locales.

Applied to files:

  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/zh.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/ui/src/i18n/zh.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use branded schemas (`Schema.brand`) for single-value types in Effect

Applied to files:

  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
  • packages/ui/src/components/message-part.css
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T17:02:35.873Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:35.873Z
Learning: In the pawwork repository, Stylelint is intentionally not configured (per AGENTS.md, only linting is enforced and the Biome formatter is disabled). When reviewing CSS files, do not raise Stylelint rule-based issues (e.g., `declaration-empty-line-before`) because they are false positives and not enforced by the project toolchain.

Applied to files:

  • packages/ui/src/components/message-part.css
🔇 Additional comments (14)
packages/ui/src/i18n/en.ts (1)

165-166: LGTM!

The updated hint strings ("Pick multiple" / "Pick 1") are concise and appropriate for the inline header chip design. This aligns well with the UI changes that move the mode hint into the header.

packages/ui/src/i18n/zh.ts (1)

151-152: LGTM!

The Chinese hint translations ("可选多个" / "选 1 个") appropriately mirror the English strings and fit the concise chip-style header design.

packages/ui/src/components/message-part.css (2)

835-837: LGTM!

The static CSS max-height constraint (min(70dvh, calc(100dvh - 180px))) effectively addresses the dock height cap requirement from #216. The comment documenting the 180px offset breakdown is helpful for future maintenance.


863-889: LGTM!

The new header title styles support the inline mode hint design well:

  • Flex row with baseline alignment keeps text vertically aligned.
  • Truncation on the sequence portion prevents overflow while keeping the mode hint visible (flex-shrink: 0).
packages/opencode/test/question/schema.test.ts (1)

1-107: LGTM overall!

The test suite provides comprehensive coverage for the new schema constraints. The tests are well-structured with clear boundary testing patterns for length limits and cardinality rules. The comment at lines 58-59 and 64-67 appropriately documents the nuanced behavior being tested.

packages/opencode/src/tool/question.txt (1)

1-54: LGTM!

The rewritten tool description is well-structured with clear sections for when to use, when not to use, examples, format constraints, and content routing guidance. The format section accurately reflects the schema constraints, and the content routing workflow helps the LLM understand how to handle longer context appropriately.

packages/app/src/pages/session/composer/session-question-dock.tsx (4)

87-88: LGTM!

The customAllowed memo correctly interprets custom !== false to treat undefined as allowed (matching the schema's "default: true" behavior). The conditional count() calculation properly excludes the custom slot from keyboard navigation when disabled.


151-158: LGTM!

The cache cleanup logic correctly handles per-tab custom availability by checking customByTab(i) for each index. Using Array.from({ length: total() }, ...) ensures consistent array sizing and avoids stale entries from unvisited tabs.


390-396: LGTM!

The inline header layout with sequence, separator, and mode hint aligns with the CSS changes in message-part.css. The aria-hidden="true" on the separator is appropriate since it's purely decorative.


455-529: LGTM!

The conditional rendering with <Show when={customAllowed()}> correctly hides the entire custom-answer UI when custom is explicitly false. The onMouseDown handler (lines 485-493) provides good UX by allowing clicks anywhere in the form area to focus the textarea.

packages/opencode/src/question/index.ts (4)

17-24: LGTM!

The Option schema now enforces concise labels and descriptions with actionable error messages that guide the LLM toward the correct fix pattern (keeping labels to 1-5 words, using streamed context for longer content).


29-55: LGTM!

The Info schema changes are well-designed:

  • Length constraints on question (200) and header (30) with clear error guidance.
  • Options cardinality (2-4) enforces meaningful choices.
  • The .refine() rule ensures users always have a way to answer when custom is disabled.

The refinement condition d.custom !== false || d.options.length > 0 correctly allows the default custom: undefined (treated as true) while requiring options when custom is explicitly disabled.


57-59: LGTM!

The comment accurately documents the Zod 4 behavior where .omit() strips refinements. This is important context for future maintainers and explains the intentional asymmetry between Info and Prompt schemas.


66-69: LGTM!

The Request.questions constraint (1-4) aligns with the tool documentation and provides clear guidance for splitting larger question sets.

Comment thread packages/opencode/test/question/schema.test.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one P1 enforcement gap plus a few smaller contract and UI edge cases. Details are inline.

Comment thread packages/opencode/src/question/index.ts
Comment thread packages/opencode/src/question/index.ts
Comment thread packages/opencode/test/question/schema.test.ts
Comment thread packages/ui/src/components/message-part.css
Astro-Han added a commit that referenced this pull request Apr 26, 2026
… refine

The Question.Request bound .min(1).max(4) was placed on a schema that the runtime never parses (Question.Service.ask takes Info[] directly), so the actual tool entry point z.array(Question.Prompt) could still accept 5+ questions. Move the bound onto the tool parameter array where parameters.parse() actually fires. Drop Info.refine because options.min(2) catches the same custom: false + empty options case earlier with a clearer message. Repair the existing tool/question.test.ts fixture which had only 1 option (now caught by options.min(2)). Add header 30-char accept boundary, hint-content assertions for question-length and options-count rejections so a future refactor cannot silently drop the error contract.

Addresses PR #245 review: P1 enforcement gap, P3 hint-content guard, Gemini medium (refine redundancy), CodeRabbit boundary parity.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
…t viable

Without a minHeight floor, a user could shrink the macOS / Windows window below the dock cap's safety threshold. The static cap formula reserves a 180px chrome buffer, so a 320px-tall window would leave 140px for the entire dock (header, options, footer) — the original symptom from #216 reappears in inverse. 480 matches the smallest height the C2 smoke checklist asserts the footer remains visible at, so any in-bounds window is guaranteed to honor the dock-budget contract.

Addresses PR #245 review P3.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
Run packages/sdk/js/script/build.ts so the published OpenAPI artifact and TS bindings reflect the new Question.Option/Info/Prompt bounds (maxLength on label/description/question/header, minItems/maxItems on options, minItems/maxItems on the tool questions array). The diff also picks up a few unrelated pre-existing drifts (session.export endpoint, session sort=created param, copy edits) that had accumulated since the last regen — keeping the artifact in sync is the same maintenance gate.

Addresses PR #245 review P2.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
…t viable

Without a minHeight floor, a user could shrink the macOS / Windows window below the dock cap's safety threshold. The static cap formula reserves a 180px chrome buffer, so a 320px-tall window would leave 140px for the entire dock (header, options, footer) — the original symptom from #216 reappears in inverse. 480 matches the smallest height the C2 smoke checklist asserts the footer remains visible at, so any in-bounds window is guaranteed to honor the dock-budget contract.

Addresses PR #245 review P3.
@Astro-Han Astro-Han force-pushed the worktree-claude+question-tool-pr-a branch from 80ac51d to 2586f55 Compare April 26, 2026 15:00

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass on the fixed head. I found three remaining P2 contract regressions worth addressing before merge.

Comment thread packages/opencode/src/tool/question.txt
Comment thread packages/opencode/src/question/index.ts
Comment thread packages/sdk/js/src/v2/gen/sdk.gen.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/tool/question.ts`:
- Around line 8-12: The questions schema currently uses Question.Prompt (which
omits the custom field) so incoming custom:false is dropped; update the schema
for the questions array to accept the full prompt type that includes the custom
boolean (e.g., replace or extend Question.Prompt with the type that includes
custom from packages/opencode/src/question/index.ts or add custom:
z.boolean().optional() to the schema) so the tool preserves and validates the
custom flag and aligns with the tool contract; ensure the updated schema is used
where questions is defined to allow model-driven disabling of custom answers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: af4b4e5d-dc77-4b4f-8f24-8ebad44d4953

📥 Commits

Reviewing files that changed from the base of the PR and between 80ac51d and 2586f55.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (11)
  • packages/app/src/pages/session/composer/session-question-dock.tsx
  • packages/desktop-electron/src/main/windows.ts
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/src/tool/question.txt
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/sdk/openapi.json
  • packages/ui/src/components/message-part.css
  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/zh.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-desktop
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (4)
packages/desktop-electron/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/desktop-electron/AGENTS.md)

Renderer process should only call window.api from src/preload

Files:

  • packages/desktop-electron/src/main/windows.ts
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/src/question/index.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🧠 Learnings (22)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
📚 Learning: 2026-04-25T09:51:16.740Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/menu-template.ts:178-189
Timestamp: 2026-04-25T09:51:16.740Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/menu-template.ts`), the `buildMacosMenuTemplate` and `buildWindowsMenuTemplate` functions are intentionally kept as separate, literal per-platform templates rather than sharing extracted submenu helpers. The split exists to allow future per-platform divergence; the templates already differ structurally (File menu: macOS omits `quit` because it lives in the App menu, Windows places `quit` next to `close`; Window submenu differs; macOS has an App menu with no Windows equivalent; View differs on terminal accelerator `Ctrl+\`` vs `CmdOrCtrl+\``). Do NOT suggest extracting shared submenu builder helpers (e.g., `buildEditSubmenu`, `buildViewSubmenu`) as YAGNI — the literal duplication is intentional and easy to scan.

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-25T09:19:30.734Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/index.ts:537-537
Timestamp: 2026-04-25T09:19:30.734Z
Learning: In Astro-Han/pawwork (packages/desktop-electron/src/main/), follow the IPC registration convention: the bootstrap entry (packages/desktop-electron/src/main/index.ts) should directly call each module’s exported register*Ipc() function. Do not route/centralize these sub-module IPC registrations through src/main/ipc.ts. Keep sub-module IPC features cohesive (e.g., src/main/ipc/about.ts should own its types/helpers and expose register*Ipc()), and allow index.ts to aggregate by calling each register*Ipc() directly.

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-22T05:32:29.012Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 98
File: packages/desktop-electron/src/main/menu-labels.ts:1-2
Timestamp: 2026-04-22T05:32:29.012Z
Learning: In Astro-Han/pawwork, the app i18n layer (`packages/app/src/i18n/`) only contains `en.ts` and `zh.ts`, and `normalizeLocale` (in `packages/app/src/context/language.tsx`) only returns `"en"` or `"zh"`. The desktop `MenuLocale = "en" | "zh"` union in `packages/desktop-electron/src/main/menu-labels.ts` is intentionally limited to these two locales and is not a broader restriction — do not flag it as overly restrictive or suggest adding other locales.

Applied to files:

  • packages/ui/src/i18n/zh.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/ui/src/i18n/zh.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `init` option in `tmpdir` to define custom setup functions that can return extra data accessible via `tmp.extra`, and use the `dispose` option for custom cleanup logic.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T17:02:40.117Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:40.117Z
Learning: In Astro-Han/pawwork, Stylelint is NOT configured in the project. Per AGENTS.md, only linting (not formatting) is used, and the Biome formatter is explicitly disabled. Do NOT raise Stylelint-based review comments (e.g., `declaration-empty-line-before`) in this repo, as they are false positives from the static analysis environment and not enforced by the project toolchain.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/ui/src/components/message-part.css
  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T17:02:35.873Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:35.873Z
Learning: In the pawwork repository, Stylelint is intentionally not configured (per AGENTS.md, only linting is enforced and the Biome formatter is disabled). When reviewing CSS files, do not raise Stylelint rule-based issues (e.g., `declaration-empty-line-before`) because they are false positives and not enforced by the project toolchain.

Applied to files:

  • packages/ui/src/components/message-part.css
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use branded schemas (`Schema.brand`) for single-value types in Effect

Applied to files:

  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🔇 Additional comments (14)
packages/desktop-electron/src/main/windows.ts (1)

74-75: LGTM! Minimum window dimensions align with PR objectives.

Setting minWidth and minHeight to 480px ensures adequate viewport budget for the question dock and conversation history. At the minimum window height, the 180px chrome buffer leaves 300px for content, which coordinates well with the CSS cap (calc(100dvh - 180px)) introduced in commit C2.

packages/opencode/src/tool/question.txt (1)

36-53: Clear and actionable tool-routing contract.

The updated bounds + “stream context first, then ask short” guidance is crisp and consistent with the dock UX goals.

packages/ui/src/i18n/en.ts (1)

165-166: Good i18n copy tightening for header chips.

“Pick multiple” / “Pick 1” is clearer in the compact header layout.

packages/ui/src/components/message-part.css (1)

835-889: Dock cap + header slot styling look solid.

The static cap and non-shrinking mode chip are implemented cleanly for the intended compact dock behavior.

packages/ui/src/i18n/zh.ts (1)

151-152: Concise zh hint updates are aligned with the new header presentation.

packages/opencode/test/tool/question.test.ts (1)

76-79: Good fixture update for new option-count constraints.

Adding the second option keeps this live test aligned with Question.Info.options min bound.

packages/opencode/test/question/schema.test.ts (1)

77-98: Nice contract guard for actionable validation hints.

These assertions prevent silent regressions where bounds remain but repair guidance is lost.

packages/opencode/src/question/index.ts (1)

17-24: Schema tightening is well executed.

The new bounds and hint messages are consistent and provide clear repair direction for invalid payloads.

Also applies to: 31-43, 57-60

packages/app/src/pages/session/composer/session-question-dock.tsx (6)

87-88: LGTM! Clean implementation of customAllowed gating.

The memo correctly interprets custom !== false to allow custom answers by default (when undefined or true), and count properly adjusts the focusable option count. This ensures focus management respects the schema constraint.


122-130: LGTM! Per-tab custom check is correctly implemented.

The function properly checks questions()[tab]?.custom !== false rather than using the current tab's customAllowed() memo, ensuring correct focus restoration when jumping between tabs with different custom configurations.


148-159: LGTM! Robust cache persistence logic.

The customByTab helper and sized arrays ensure that:

  1. Tabs with custom: false get cleared values (empty string/false) instead of stale data
  2. Unvisited tabs are handled via nullish coalescing defaults
  3. Array length matches total() to avoid index mismatches on restore

309-316: LGTM! Good defensive guard.

While count() should prevent focus from reaching the custom index when customAllowed() is false, this explicit check provides a safety net against potential race conditions during rapid tab switches.


390-396: LGTM! Clean header restructure with proper accessibility.

The inline mode chip with aria-hidden="true" on the decorative separator is correct. The semantic data-slot structure aligns with the CSS changes mentioned in related context.


455-529: LGTM! Well-structured custom option gating.

The implementation correctly:

  • Gates the entire custom UI with <Show when={customAllowed()}>
  • Handles customRef lifecycle safely (focus logic can't reach options().length when custom is disabled since count() excludes it)
  • Implements proper form focus behavior via onMouseDown that delegates to textarea
  • Allows Shift+Enter for multiline input while Enter submits

Comment thread packages/opencode/src/tool/question.ts
Astro-Han added a commit that referenced this pull request Apr 26, 2026
The tool description tells the model "Set custom: false only when the options are exhaustive", but Prompt = Info.omit({ custom: true }) hid the flag from the JSON schema sent to providers, making the instruction unreachable from a real tool call. Drop the omit (Prompt = Info.omit({}) re-meta'd) so custom is advertised. The dock already honors custom: false per C3a; this connects the contract end-to-end.

Addresses PR #245 review P2.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
The PlanExitTool option description "Switch to build agent and start implementing the plan" was 53 chars, over Question.Option.description.max(50) introduced in this PR. Question.Service.ask doesn't parse Info[] before publishing, so the server could emit an event whose payload violates the published OpenAPI contract. Trim to "Switch to build agent and implement the plan" (44 chars).

Addresses PR #245 review P2.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
The previous SDK regen turned the experimental cursor query param into a required field, breaking client.experimental.session.list({}) and list() first-page calls. Root cause: z.preprocess(transform, inner.optional()) does not propagate inner-schema optionality to the outer JSON schema in Zod 4 / hono-openapi, so toJSONSchema marked the field required. Add an outer .optional() on the preprocess wrapper so the JSON schema honors the optional contract; keep the inner .optional() so a "" → undefined preprocess still parses (regression test in global-session-list covers this).

Regenerate sdk.gen.ts and types.gen.ts to pick up the optional cursor type.

Addresses PR #245 review P2.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
…t viable

Without a minHeight floor, a user could shrink the macOS / Windows window below the dock cap's safety threshold. The static cap formula reserves a 180px chrome buffer, so a 320px-tall window would leave 140px for the entire dock (header, options, footer) — the original symptom from #216 reappears in inverse. 480 matches the smallest height the C2 smoke checklist asserts the footer remains visible at, so any in-bounds window is guaranteed to honor the dock-budget contract.

Addresses PR #245 review P3.
Astro-Han added a commit that referenced this pull request Apr 26, 2026
The previous SDK regen turned the experimental cursor query param into a required field, breaking client.experimental.session.list({}) and list() first-page calls. Root cause: z.preprocess(transform, inner.optional()) does not propagate inner-schema optionality to the outer JSON schema in Zod 4 / hono-openapi, so toJSONSchema marked the field required. Add an outer .optional() on the preprocess wrapper so the JSON schema honors the optional contract; keep the inner .optional() so a "" → undefined preprocess still parses (regression test in global-session-list covers this).

Regenerate sdk.gen.ts and types.gen.ts to pick up the optional cursor type.

Addresses PR #245 review P2.
@Astro-Han Astro-Han force-pushed the worktree-claude+question-tool-pr-a branch from a714b7c to b66689a Compare April 26, 2026 15:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/question/index.ts`:
- Around line 21-24: The Option schema currently requires description
(z.string()) which blocks label-only choices; update the Option validation in
packages/opencode/src/question/index.ts so Option.description is optional by
changing the schema for description to accept undefined (e.g., use .optional()
on the z.string() validator) and ensure any downstream code that reads
Option.description handles absence (e.g., fallbacks or conditional rendering) in
functions/classes that consume the Option objects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 270c2504-7662-499e-86f9-27a8b72c4a4c

📥 Commits

Reviewing files that changed from the base of the PR and between a714b7c and b66689a.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (13)
  • packages/app/src/pages/session/composer/session-question-dock.tsx
  • packages/desktop-electron/src/main/windows.ts
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/server/instance/experimental.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/src/tool/question.txt
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/sdk/openapi.json
  • packages/ui/src/components/message-part.css
  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/zh.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: typecheck
  • GitHub Check: unit-opencode
  • GitHub Check: unit-desktop
  • GitHub Check: unit-app
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (4)
packages/desktop-electron/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/desktop-electron/AGENTS.md)

Renderer process should only call window.api from src/preload

Files:

  • packages/desktop-electron/src/main/windows.ts
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/src/tool/plan.ts
  • packages/opencode/src/server/instance/experimental.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🧠 Learnings (22)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
📚 Learning: 2026-04-20T14:36:08.774Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/desktop-electron/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:08.774Z
Learning: Applies to packages/desktop-electron/src/**/*.{ts,tsx,js,jsx} : Renderer process should only call `window.api` from `src/preload`

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-25T09:51:16.740Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/menu-template.ts:178-189
Timestamp: 2026-04-25T09:51:16.740Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/menu-template.ts`), the `buildMacosMenuTemplate` and `buildWindowsMenuTemplate` functions are intentionally kept as separate, literal per-platform templates rather than sharing extracted submenu helpers. The split exists to allow future per-platform divergence; the templates already differ structurally (File menu: macOS omits `quit` because it lives in the App menu, Windows places `quit` next to `close`; Window submenu differs; macOS has an App menu with no Windows equivalent; View differs on terminal accelerator `Ctrl+\`` vs `CmdOrCtrl+\``). Do NOT suggest extracting shared submenu builder helpers (e.g., `buildEditSubmenu`, `buildViewSubmenu`) as YAGNI — the literal duplication is intentional and easy to scan.

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-25T09:19:30.734Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/index.ts:537-537
Timestamp: 2026-04-25T09:19:30.734Z
Learning: In Astro-Han/pawwork (packages/desktop-electron/src/main/), follow the IPC registration convention: the bootstrap entry (packages/desktop-electron/src/main/index.ts) should directly call each module’s exported register*Ipc() function. Do not route/centralize these sub-module IPC registrations through src/main/ipc.ts. Keep sub-module IPC features cohesive (e.g., src/main/ipc/about.ts should own its types/helpers and expose register*Ipc()), and allow index.ts to aggregate by calling each register*Ipc() directly.

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-22T05:32:29.012Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 98
File: packages/desktop-electron/src/main/menu-labels.ts:1-2
Timestamp: 2026-04-22T05:32:29.012Z
Learning: In Astro-Han/pawwork, the app i18n layer (`packages/app/src/i18n/`) only contains `en.ts` and `zh.ts`, and `normalizeLocale` (in `packages/app/src/context/language.tsx`) only returns `"en"` or `"zh"`. The desktop `MenuLocale = "en" | "zh"` union in `packages/desktop-electron/src/main/menu-labels.ts` is intentionally limited to these two locales and is not a broader restriction — do not flag it as overly restrictive or suggest adding other locales.

Applied to files:

  • packages/ui/src/i18n/zh.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/ui/src/i18n/zh.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T05:48:36.205Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/e2e/app/composer-parity.spec.ts:0-0
Timestamp: 2026-04-24T05:48:36.205Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, the Model chip trigger button carries `data-action="prompt-model"` (around line 1187) and the Variant chip trigger button carries `data-action="prompt-model-variant"` (around line 1231), both set via `triggerProps`. These are therefore already captured by any `[data-action]` selector sweep in E2E tests and do not need a separate `[data-component]` query to be included in parity assertions — though unioning both is kept as belt-and-suspenders in `collectBarSet`.

Applied to files:

  • packages/opencode/src/tool/question.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/ui/src/components/message-part.css
  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T17:02:35.873Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:35.873Z
Learning: In the pawwork repository, Stylelint is intentionally not configured (per AGENTS.md, only linting is enforced and the Biome formatter is disabled). When reviewing CSS files, do not raise Stylelint rule-based issues (e.g., `declaration-empty-line-before`) because they are false positives and not enforced by the project toolchain.

Applied to files:

  • packages/ui/src/components/message-part.css
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use branded schemas (`Schema.brand`) for single-value types in Effect

Applied to files:

  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T17:02:40.117Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:40.117Z
Learning: In Astro-Han/pawwork, Stylelint is NOT configured in the project. Per AGENTS.md, only linting (not formatting) is used, and the Biome formatter is explicitly disabled. Do NOT raise Stylelint-based review comments (e.g., `declaration-empty-line-before`) in this repo, as they are false positives from the static analysis environment and not enforced by the project toolchain.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🔇 Additional comments (17)
packages/desktop-electron/src/main/windows.ts (1)

74-75: Good minimum-size safeguard.

Line 74 and Line 75 add sensible lower bounds that prevent unusably small main windows while keeping current startup/state behavior intact.

packages/opencode/src/tool/plan.ts (1)

42-42: Wording update is clear and behavior-preserving.

This keeps intent intact while making the CTA more concise.

packages/opencode/src/server/instance/experimental.ts (1)

374-380: Cursor optionality adjustment looks correct.

Applying .optional() at the outer preprocess level is consistent with accepting omitted/empty cursor inputs without changing downstream pagination decoding.

packages/opencode/test/tool/question.test.ts (1)

76-79: Test fixture now matches schema cardinality rules.

Good update to keep this test valid under the tightened options bounds.

packages/ui/src/i18n/en.ts (1)

165-166: English hint copy is appropriately tightened for inline mode display.

The shorter labels are a good fit for constrained header space.

packages/ui/src/i18n/zh.ts (1)

151-152: Chinese hint copy update is clear and compact.

“多选 / 单选” maps cleanly to the inline mode chip presentation.

packages/opencode/src/tool/question.ts (1)

8-12: Question array bounds and repair hints are well-scoped.

The 1–4 limits plus actionable messages improve contract clarity for tool callers.

packages/ui/src/components/message-part.css (2)

835-837: Dock height cap change is solid and objective-aligned.

This cap enforces a predictable viewport budget and avoids over-expanding the question dock.


863-889: Header mode-chip layout is resilient under truncation.

Keeping separator/mode non-shrinking while truncating sequence text is the right tradeoff for readability.

packages/opencode/test/question/schema.test.ts (5)

4-18: Option length boundary tests are complete and precise.

Good pairwise reject/accept coverage at 50/51 chars for both label and description.


29-59: Info schema boundary coverage is strong.

The tests clearly lock in question/header length limits and 2–4 option cardinality.


61-86: Custom-flag and Prompt parsing behavior is validated well.

These cases correctly guard both explicit custom: false and omitted-custom paths.


88-109: Actionable error-hint contract is correctly protected.

Great choice to assert representative hint text so refactors can’t silently degrade repair guidance.


111-141: Request question-count bounds are covered comprehensively.

Rejecting 0/5 and accepting 1–4 directly enforces the intended tool-level contract.

packages/opencode/src/tool/question.txt (1)

34-53: Good content-routing and payload-format guidance.

This is clear and actionable for model behavior, and it aligns well with the bounded payload shape and “stream context first, then ask short” flow.

packages/app/src/pages/session/composer/session-question-dock.tsx (2)

87-126: Custom-slot gating is correctly wired into navigation and selection.

customAllowed is consistently applied to focus count, tab focus restoration, and custom-slot activation, preventing invalid focus/selection when custom is disabled.

Also applies to: 309-316


151-158: Cache normalization + conditional custom UI are solid.

The cleanup rewrite and Show when={customAllowed()} block prevent stale custom-state resurrection on remount for tabs that disallow custom input.

Also applies to: 455-529

Comment thread packages/opencode/src/question/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/question/index.ts`:
- Around line 17-24: The label and description Zod schemas currently accept
empty or whitespace-only strings; update each z.string() for those fields (e.g.,
the label and description schemas shown and the other string fields referenced
31-38) to reject blank input by trimming and enforcing a non-empty length—use
z.string().trim().min(1, "Field cannot be empty or whitespace-only").max(50,
"...").describe("...") (or equivalently add .refine(s => s.trim().length > 0,
"cannot be empty or whitespace-only")) so whitespace-only values are rejected;
apply the same change to all similar string fields in this file.

In `@packages/opencode/src/tool/question.txt`:
- Around line 38-42: The wording is conflicting: "options: 2–4 distinct,
mutually exclusive choices" contradicts allowing non-exclusive choices when
multiple: true; update the spec text for `options` to remove "mutually
exclusive" and instead require choices be distinct and clearly described, and
add a short clarifying clause referencing `multiple` that explains `multiple:
true` permits non-exclusive selections (e.g., feature toggles); keep existing
constraints for `options[].label`, `options[].description`, and `custom`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0ea127cd-26e4-477f-a30e-11bd4e07ec9b

📥 Commits

Reviewing files that changed from the base of the PR and between a714b7c and b66689a.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (13)
  • packages/app/src/pages/session/composer/session-question-dock.tsx
  • packages/desktop-electron/src/main/windows.ts
  • packages/opencode/src/question/index.ts
  • packages/opencode/src/server/instance/experimental.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/src/tool/question.ts
  • packages/opencode/src/tool/question.txt
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/sdk/openapi.json
  • packages/ui/src/components/message-part.css
  • packages/ui/src/i18n/en.ts
  • packages/ui/src/i18n/zh.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: unit-windows-desktop
🧰 Additional context used
📓 Path-based instructions (4)
packages/desktop-electron/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/desktop-electron/AGENTS.md)

Renderer process should only call window.api from src/preload

Files:

  • packages/desktop-electron/src/main/windows.ts
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/server/instance/experimental.ts
  • packages/opencode/src/question/index.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🧠 Learnings (22)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.
📚 Learning: 2026-04-20T14:36:08.774Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/desktop-electron/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:08.774Z
Learning: Applies to packages/desktop-electron/src/**/*.{ts,tsx,js,jsx} : Renderer process should only call `window.api` from `src/preload`

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-25T09:19:30.734Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/index.ts:537-537
Timestamp: 2026-04-25T09:19:30.734Z
Learning: In Astro-Han/pawwork (packages/desktop-electron/src/main/), follow the IPC registration convention: the bootstrap entry (packages/desktop-electron/src/main/index.ts) should directly call each module’s exported register*Ipc() function. Do not route/centralize these sub-module IPC registrations through src/main/ipc.ts. Keep sub-module IPC features cohesive (e.g., src/main/ipc/about.ts should own its types/helpers and expose register*Ipc()), and allow index.ts to aggregate by calling each register*Ipc() directly.

Applied to files:

  • packages/desktop-electron/src/main/windows.ts
📚 Learning: 2026-04-26T15:35:31.757Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 245
File: packages/opencode/src/question/index.ts:21-24
Timestamp: 2026-04-26T15:35:31.757Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/question/index.ts`), `Question.Option.description` is intentionally **required** (`z.string()`, not `.optional()`). This matches the upstream opencode contract and all current callers (e.g. `PlanExitTool`) always supply a description. The defensive `<Show when={props.description}>` rendering in `session-question-dock.tsx` is a standard guard, not a signal that the field is intended to be optional. Do NOT suggest making `Option.description` optional without a dedicated follow-up that covers schema + tool description + dock copy + tests.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/src/tool/plan.ts
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/server/instance/experimental.ts
  • packages/ui/src/components/message-part.css
  • packages/opencode/src/tool/question.txt
  • packages/opencode/src/question/index.ts
  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-24T05:48:36.205Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/e2e/app/composer-parity.spec.ts:0-0
Timestamp: 2026-04-24T05:48:36.205Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, the Model chip trigger button carries `data-action="prompt-model"` (around line 1187) and the Variant chip trigger button carries `data-action="prompt-model-variant"` (around line 1231), both set via `triggerProps`. These are therefore already captured by any `[data-action]` selector sweep in E2E tests and do not need a separate `[data-component]` query to be included in parity assertions — though unioning both is kept as belt-and-suspenders in `collectBarSet`.

Applied to files:

  • packages/opencode/src/tool/question.ts
📚 Learning: 2026-04-24T06:50:02.712Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 211
File: packages/opencode/src/provider/models.ts:113-179
Timestamp: 2026-04-24T06:50:02.712Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/provider/models.ts`), `PublishModel` and `PublishProvider` intentionally duplicate fields from `Model` and `Provider` with looser optionality. The split is a deliberate behavior boundary: `PublishModel`/`PublishProvider` (with `.passthrough()`) are used for lenient publish-time validation of incoming catalogs, while `Model`/`Provider` are used for strict runtime normalization. Do not flag this duplication as a maintenance burden or suggest extracting a shared base schema — the explicit separation is intentional and tied to the models-refresh reliability path introduced in PR `#211`.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-21T16:57:25.580Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/agent.ts:108-119
Timestamp: 2026-04-21T16:57:25.580Z
Learning: In `packages/opencode/src/config/agent.ts` (Astro-Han/pawwork), `ConfigPermission.Info` only accepts permission objects or the three action strings `"ask"`, `"allow"`, `"deny"`, and transforms those action strings into `{ "*": action }` before `normalize()` runs. By the time `normalize()` is reached, `configuredPermission` is always either `undefined` or a `Record<string, Rule>` — never a raw arbitrary string. The `Object.assign(permission, configuredPermission)` pattern is therefore safe. Do not flag it as corrupting string permission references.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T05:39:56.086Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 208
File: packages/app/src/components/prompt-input.tsx:1569-1611
Timestamp: 2026-04-24T05:39:56.086Z
Learning: In Astro-Han/pawwork `packages/app/src/components/prompt-input.tsx`, after the composer unification in PR `#208` (fixed in commit 5d810aa):
- `SendButton.disabled` does NOT gate on `store.mode !== "normal"`. Shell mode has a fully visible, clickable orange submit button that calls `handleSubmit` directly (same path as the Enter key in `handleKeyDown`). Do NOT suggest re-adding the mode gate.
- `SendButton` does NOT use the `buttons()` spring opacity animation (`style={buttons()}`). It is always fully visible regardless of mode.
- `WorkspaceChip` is gated on `props.homeMode && store.mode === "normal"` so it hides in shell mode (preventing it from appearing isolated/bright while neighboring controls fade).
- The left-side chip group (`aria-hidden={store.mode !== "normal"}`) covers attach/model/variant/workspace controls only; `SendButton` remains in a separate right-side sibling div.

Applied to files:

  • packages/opencode/src/tool/question.ts
  • packages/ui/src/components/message-part.css
  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/tool/question.test.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-22T05:32:29.012Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 98
File: packages/desktop-electron/src/main/menu-labels.ts:1-2
Timestamp: 2026-04-22T05:32:29.012Z
Learning: In Astro-Han/pawwork, the app i18n layer (`packages/app/src/i18n/`) only contains `en.ts` and `zh.ts`, and `normalizeLocale` (in `packages/app/src/context/language.tsx`) only returns `"en"` or `"zh"`. The desktop `MenuLocale = "en" | "zh"` union in `packages/desktop-electron/src/main/menu-labels.ts` is intentionally limited to these two locales and is not a broader restriction — do not flag it as overly restrictive or suggest adding other locales.

Applied to files:

  • packages/ui/src/i18n/zh.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/ui/src/i18n/zh.ts
  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use branded schemas (`Schema.brand`) for single-value types in Effect

Applied to files:

  • packages/opencode/test/question/schema.test.ts
  • packages/opencode/src/question/index.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-20T14:21:56.373Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 71
File: packages/app/src/components/session/session-status-connections.tsx:146-147
Timestamp: 2026-04-20T14:21:56.373Z
Learning: In the Astro-Han/pawwork repository (SolidJS app), `sync.data.config` is always initialized to `{}` at `packages/app/src/context/global-sync.tsx` line 71 and is never `undefined` at runtime. Non-optional property access like `sync.data.config.plugin` is intentional and consistent with the pattern used in `packages/app/src/components/status-popover-body.tsx` line 243. Do not flag `sync.data.config.plugin` as needing optional chaining.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T17:02:40.117Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:40.117Z
Learning: In Astro-Han/pawwork, Stylelint is NOT configured in the project. Per AGENTS.md, only linting (not formatting) is used, and the Biome formatter is explicitly disabled. Do NOT raise Stylelint-based review comments (e.g., `declaration-empty-line-before`) in this repo, as they are false positives from the static analysis environment and not enforced by the project toolchain.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/question/schema.test.ts
📚 Learning: 2026-04-23T17:02:35.873Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 198
File: packages/app/src/index.css:95-97
Timestamp: 2026-04-23T17:02:35.873Z
Learning: In the pawwork repository, Stylelint is intentionally not configured (per AGENTS.md, only linting is enforced and the Biome formatter is disabled). When reviewing CSS files, do not raise Stylelint rule-based issues (e.g., `declaration-empty-line-before`) because they are false positives and not enforced by the project toolchain.

Applied to files:

  • packages/ui/src/components/message-part.css
📚 Learning: 2026-04-23T15:26:07.250Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/src/pages/layout/sidebar-items.tsx:102-107
Timestamp: 2026-04-23T15:26:07.250Z
Learning: In Astro-Han/pawwork (`packages/app/src/pages/layout/sidebar-items.tsx`), the `indicator()` function in `SessionRow` intentionally renders `props.leadingSlot` (the pin button) only as a fallback when no status indicator (running/permission/error/unseen) is active. When a higher-priority status wins the slot, the pin button is removed from the DOM — this is a deliberate design choice for the merged leading slot (`#150`). The keyboard unpin path is preserved via: (1) focusing the row anchor triggers `group-focus-within` which reveals the dots menu trigger, then Tab → Enter → "Unpin Session"; (2) the context menu (right-click / Shift+F10) exposes "Unpin Session". The "always render + CSS overlay" approach was considered but rejected due to z-index/pointer-events complexity; residual `...` slot behavior is tracked in `#192`. Do NOT flag the absence of the pin button from the DOM when a status is active as an accessibility regression.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/composer/session-question-dock.tsx
🔇 Additional comments (20)
packages/desktop-electron/src/main/windows.ts (1)

74-75: Good guardrail for usability.

Setting minWidth and minHeight here is a clean, low-risk way to enforce a minimum usable window size and matches the stated objective.

packages/opencode/src/server/instance/experimental.ts (1)

374-380: Good cursor optionality fix in query schema.

Line 379 correctly makes the cursor field optional at the schema boundary while preserving empty-string normalization via preprocess. This matches the downstream decoding behavior that treats absent/empty cursor values as no cursor.

packages/opencode/src/tool/plan.ts (1)

42-42: LGTM!

The description text is now more concise ("implement the plan" vs "start implementing the plan"), aligning with the PR's objective of tightening tool descriptions.

packages/ui/src/i18n/zh.ts (1)

151-152: LGTM!

The shortened Chinese hint strings ("多选" / "单选") are idiomatic and align well with the English counterparts ("Pick multiple" / "Pick 1"). This supports the UI refactor that moved hints into the header where space is more constrained.

packages/ui/src/components/message-part.css (2)

835-837: LGTM!

The dock height cap using min(70dvh, calc(100dvh - 180px)) correctly addresses issue #216. The inline comment documenting the 180px breakdown (44 + 48 + 48 + 40) is helpful for future maintenance. Combined with the Electron minHeight: 480 constraint added in the companion commit, this ensures the footer remains visible across supported window sizes.


863-889: LGTM!

The header layout correctly implements:

  • Flex baseline alignment for visual consistency
  • Ellipsis truncation on the sequence portion (min-width: 0; overflow: hidden)
  • flex-shrink: 0 on separator and mode to prevent them from collapsing
  • font-weight: 600 on mode chip as specified in PR description
packages/ui/src/i18n/en.ts (1)

165-166: LGTM!

The shortened hint strings ("Pick multiple" / "Pick 1") are clear, action-oriented, and appropriately concise for display in the header alongside the question progress indicator.

packages/opencode/src/tool/question.ts (1)

8-12: LGTM!

The schema constraints (1–4 questions) with actionable error messages align with the PR objectives. The max error message helpfully guides the model to "split into multiple tool calls or stream context first" for self-correction.

packages/opencode/test/tool/question.test.ts (1)

76-79: LGTM!

The test fixture correctly updated to provide 2 options, complying with the new options.min(2) schema constraint. Both options include required descriptions per the established contract.

packages/app/src/pages/session/composer/session-question-dock.tsx (5)

87-88: LGTM!

The customAllowed derivation correctly interprets custom !== false (treating undefined as allowed, per the SDK type custom?: boolean). The count() adjustment ensures keyboard navigation boundaries are accurate when custom input is disabled.


122-130: LGTM!

The pickFocus function correctly checks whether custom is allowed for the target tab before returning the custom slot index. This prevents focus from landing on a non-existent element when navigating between tabs with different custom settings.


151-158: LGTM!

Cache serialization correctly rebuilds custom and customOn arrays to total() length, persisting empty/false for tabs where custom is disallowed. This prevents stale focus state when restoring a cached request with mixed custom-allowed tabs.


390-396: LGTM!

The header now displays the question sequence, separator, and selection mode inline. This layout aligns with the CSS changes in message-part.css and the shortened i18n strings.


455-529: LGTM!

The custom answer slot is correctly gated by <Show when={customAllowed()}>, ensuring the UI respects the Question.Info.custom flag. Both the collapsed button state and the expanded editing form are properly wrapped.

packages/opencode/test/question/schema.test.ts (1)

1-142: LGTM!

Comprehensive test coverage for the new schema constraints:

  • Boundary tests for all length limits (Option.label/description 50, Info.question 200, Info.header 30)
  • Cardinality tests for options (2–4) and questions (1–4)
  • Actionable hint assertions ensuring error messages guide LLM self-correction
  • Edge case coverage for custom: false with empty options

The test structure follows established patterns with clear naming and appropriate use of fixtures.

packages/opencode/src/question/index.ts (3)

39-43: Good cardinality constraints on options.

The options 2–4 bound is clear and directly supports the intended dock UX/scannability.


50-54: Prompt schema exposure for custom looks correct.

Keeping custom on the LLM-facing prompt and preserving a separate schema ref is a solid way to align tool docs with reachable tool inputs.


61-64: Request-level question count bounds are well-defined.

The questions 1–4 constraint and actionable hint text are precise and consistent with the tool contract.

packages/opencode/src/tool/question.txt (2)

1-23: Use/avoidance criteria are clear and actionable.

This is a strong upgrade from generic guidance and should improve proactive-but-disciplined tool use.


45-53: Content-routing guidance is solid.

Separating long framing from the short tool prompt should materially improve dock readability and option visibility.

Comment thread packages/opencode/src/question/index.ts
Comment thread packages/opencode/src/tool/question.txt Outdated
…ol description

- Add character caps: question.max(200), header.max(30), option.label.max(50), option.description.max(50)
- Add count caps: options.min(2).max(4) on Info, questions.min(1).max(4) on the tool parameters array (the actual entry-point — Question.Service.ask takes Info[] directly and never parses Question.Request, so the array bound has to live on the tool param schema to fire on a real call)
- Each bound carries an actionable hint message so LLMs see structured repair guidance on rejection; tests assert the hint content for one length-error and one count-error so a future refactor cannot silently drop the contract
- Expose `custom` flag in the LLM-facing schema (Prompt = Info.omit({})) so the tool description's "Set false only when options are exhaustive" instruction is reachable from a real tool call; the dock already honors custom: false per a later commit
- Trim PlanExitTool option description from 53 to 44 chars to fit the new option.description.max(50) cap (Question.Service.ask doesn't parse Info[] before publishing, so internal callers must self-fit)
- Rewrite question.txt with When/NOT/Examples/Format/Content-routing sections inspired by Claude Code's EnterPlanMode prompt; Content-routing teaches the model to stream long framing before invoking the tool
- Regenerate packages/sdk/openapi.json and v2 type bindings so external SDK and API docs reflect the new bounds
- Repair the existing tool/question.test.ts fixture (originally 1 option, now caught by options.min(2))

Part of #188.
Replace the dynamic measure() function (which fell back to 100dvh when sticky-class detection failed, causing the v0.2.12 footer-off-screen repro) with a static max-height: min(70dvh, calc(100dvh - 180px)). The 180px buffer is the verified PawWork session top chrome stack: 44 titlebar + 48 tabs + 48 sticky session title + 40 safety margin. No JS measurement, no ResizeObserver, no CSS variables — the cap math is structurally stable.

Closes #216.
The dock previously rendered the type-your-own-answer slot regardless of the schema's custom flag, and counted it in keyboard navigation. PR A wraps the custom UI in <Show when={q.custom !== false}> and updates count(), pickFocus(), and selectOption() so the slot is fully omitted when disallowed. The cache also clears custom state for tabs whose schema disallows it, preventing stale focus on remount.

Part of #188.
Replace the dedicated [data-slot="question-hint"] row with an inline mode chip in the header title: "{n} of {total} questions · {mode}". Mode renders in --text-strong weight 600 with flex-shrink:0 so it survives the ellipsis race; separator in --icon-weak. Tighten en+zh hint copy: "Pick 1" / "Pick multiple" / "单选" / "多选" — Chinese uses the established UI convention rather than a literal translation. Other 16 locales follow upstream sync.

Closes #188.
…t viable

Without a minHeight floor, a user could shrink the macOS / Windows window below the dock cap's safety threshold. The static cap formula reserves a 180px chrome buffer, so a 320px-tall window would leave 140px for the entire dock (header, options, footer) — the original symptom from #216 reappears in inverse. 480 matches the smallest height the C2 smoke checklist asserts the footer remains visible at, so any in-bounds window is guaranteed to honor the dock-budget contract.

Addresses PR #245 review P3.
The previous SDK regen turned the experimental cursor query param into a required field, breaking client.experimental.session.list({}) and list() first-page calls. Root cause: z.preprocess(transform, inner.optional()) does not propagate inner-schema optionality to the outer JSON schema in Zod 4 / hono-openapi, so toJSONSchema marked the field required. Add an outer .optional() on the preprocess wrapper so the JSON schema honors the optional contract; keep the inner .optional() so a "" → undefined preprocess still parses (regression test in global-session-list covers this).

Regenerate sdk.gen.ts and types.gen.ts to pick up the optional cursor type.

Addresses PR #245 review P2.
@Astro-Han Astro-Han force-pushed the worktree-claude+question-tool-pr-a branch from b66689a to 1a49534 Compare April 26, 2026 15:43
@Astro-Han Astro-Han merged commit f283f6f into dev Apr 26, 2026
23 checks passed
@Astro-Han Astro-Han deleted the worktree-claude+question-tool-pr-a branch April 26, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Cap question dock height so footer and history stay in viewport [Feature] Improve question tool clarity and proactive use

1 participant