Skip to content

Fix change_title reliability across providers#64

Open
happier-bot wants to merge 2 commits intohappier-dev:devfrom
happier-bot:happier-bot/issue-62-change-title-reliability
Open

Fix change_title reliability across providers#64
happier-bot wants to merge 2 commits intohappier-dev:devfrom
happier-bot:happier-bot/issue-62-change-title-reliability

Conversation

@happier-bot
Copy link
Contributor

@happier-bot happier-bot commented Feb 21, 2026

Fixes #62.

Changes

  • Make CHANGE_TITLE_INSTRUCTION provider-robust (don’t reference non-existent functions.happier__change_title; prefer mcp__happier__change_title but allow canonical change_title).
  • Normalize additional change_title aliases in CLI + UI (change_title, happier__change_title, happy__change_title, legacy MCP names).
  • OpenCode ACP: treat toolName=change_title as Task when the input shape looks like a task/subagent tool call (prevents tasks/subtasks being labeled "change title").

Tests

  • yarn workspace @happier-dev/app test sources/sync/reducer/messageToEvent.test.ts
  • yarn workspace @happier-dev/cli test:unit

Note: in this container environment, two existing tests fail consistently due to process-tree termination behavior:

  • src/agent/acp/__tests__/killProcessTree.test.ts
  • src/agent/acp/__tests__/AcpBackend.dispose.killsProcessTree.test.ts

They appear unrelated to the changes in this PR.

Summary by CodeRabbit

  • New Features

    • Dynamic change-title instruction text that can vary by provider and preferred tool name.
  • Refactor

    • Centralized canonical aliases for change-title tool names and replaced scattered checks with a shared alias lookup, improving consistent recognition across integrations.
  • Tests

    • Added cases covering multiple change-title name variants and context-aware tool-name resolution.

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Walkthrough

Centralizes change-title tool name aliases into a shared protocol module, replaces ad-hoc string checks with the alias predicate across CLI and UI, adds a dynamic change-title instruction builder, and introduces a heuristic mapping change_title->Task in OpenCode transport with accompanying tests.

Changes

Cohort / File(s) Summary
Protocol aliases
packages/protocol/src/tools/v2/aliases.ts, packages/protocol/src/tools/v2/index.ts
Add CHANGE_TITLE_TOOL_NAME_ALIASES, ChangeTitleToolNameAlias type, Zod schema, and isChangeTitleToolNameAlias helper; re-export from v2 index.
Instruction builder
apps/cli/src/agent/runtime/changeTitleInstruction.ts
Introduce ChangeTitleInstructionOptions and buildChangeTitleInstruction(opts); make CHANGE_TITLE_INSTRUCTION dynamic (uses preferredToolName / aliases).
Alias usage: CLI backends
apps/cli/src/backends/opencode/acp/transport.ts, apps/cli/src/backends/opencode/acp/transport.test.ts, apps/cli/src/backends/gemini/runtime/createGeminiBackendMessageHandler.ts, apps/cli/src/backends/gemini/runtime/geminiTurnMessageState.ts, apps/cli/src/backends/gemini/utils/permissionHandler.ts, apps/cli/src/backends/gemini/utils/promptUtils.ts
Replace hard-coded change_title name checks with isChangeTitleToolNameAlias; add heuristic in OpenCode transport to map change_title to Task when input looks task-like; add tests for task detection and explicit-title handling; make callId and auto-approve checks alias-driven.
Alias usage: CLI tools normalization
apps/cli/src/agent/tools/normalization/index.ts
Use isChangeTitleToolNameAlias to canonicalize change_title tool names instead of inline comparisons.
Alias usage: UI normalization & parsing
apps/ui/sources/components/tools/normalization/normalize/nameInference.ts, apps/ui/sources/sync/reducer/messageToEvent.ts, apps/ui/sources/sync/reducer/messageToEvent.test.ts
Replace local CHANGE_TITLE_TOOL_NAMES set / explicit checks with isChangeTitleToolNameAlias; update tests to assert behavior across multiple alias variants (looped assertions).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix change_title reliability across providers' directly and clearly summarizes the main objective of the PR, which is addressing the change_title tool reliability and aliasing issues across different providers.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #62 by centralizing change_title aliases, making the instruction provider-robust, normalizing aliases across CLI/UI, and handling OpenCode ACP behavior to prevent tasks from being labeled 'change title'.
Out of Scope Changes check ✅ Passed All changes directly support the objectives of fixing change_title reliability: new alias module, normalized imports across services, ACP transport improvements, and updated tests—no extraneous modifications detected.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 21, 2026

Greptile Summary

This PR improves change_title tool reliability across providers by normalizing additional tool name aliases and adding a heuristic to distinguish task-like calls from actual title changes in the OpenCode ACP transport.

  • Instruction text (changeTitleInstruction.ts): Rewritten to be provider-agnostic — no longer references the non-existent functions.happier__change_title API; instead uses generic language with a preference for mcp__happier__change_title.
  • CLI normalization (normalization/index.ts): Three new aliases added (happier__change_title, happy__change_title, change_title) to canonicalizeToolNameV2, ensuring all known variants map to the canonical change_title.
  • UI normalization (nameInference.ts, messageToEvent.ts): Same three aliases added to the UI-side normalization and the event parser's CHANGE_TITLE_TOOL_NAMES Set, keeping CLI and UI in sync.
  • OpenCode ACP transport (transport.ts): Added a heuristic fallback that detects when a change_title call is actually a task/subagent call by checking for prompt/subagent_type fields in the absence of a title field. This prevents tasks from being mis-rendered as title changes.
  • Tests: New transport test cases cover the heuristic (both positive and negative paths). The UI messageToEvent test is expanded from 2 to 5 tool name variants.

Confidence Score: 5/5

  • This PR is safe to merge — changes are additive normalizations with good test coverage and no behavioral regressions.
  • All changes are additive (new name aliases and a heuristic guard) with no breaking changes to existing behavior. The normalization additions are consistent across CLI and UI. The heuristic in the OpenCode transport is well-documented, has test coverage for both the positive and negative paths, and the fallback logic is conservative (only triggers when task-like fields are present AND title is absent). The instruction text change is purely cosmetic/LLM-facing and removes a reference to a non-existent API.
  • No files require special attention. The OpenCode transport heuristic (apps/cli/src/backends/opencode/acp/transport.ts) is the most complex change but is well-tested.

Important Files Changed

Filename Overview
apps/cli/src/agent/runtime/changeTitleInstruction.ts Instruction text updated to be provider-agnostic: no longer references non-existent functions.happier__change_title, instead uses generic phrasing with a preference for mcp__happier__change_title. Clear and well-documented.
apps/cli/src/agent/tools/normalization/index.ts Extended canonicalizeToolNameV2 to normalize three additional change_title aliases (happier__change_title, happy__change_title, change_title) to the canonical change_title name. Uses existing lower variable for case-insensitive matching, consistent with surrounding code.
apps/cli/src/backends/opencode/acp/transport.ts Added heuristic fallback in determineToolName to detect task-like change_title calls by checking for prompt/subagent_type fields in the absence of a title field. The heuristic is documented and tested. Minor concern: hasPrompt alone (without hasSubagentType) is sufficient to trigger Task classification, which is intentionally broad but could theoretically mis-classify edge cases.
apps/cli/src/backends/opencode/acp/transport.test.ts Two new test cases added: one for the heuristic fallback (input with prompt + subagent_type but no title maps to Task) and one ensuring explicit title input stays as change_title. Good coverage of both positive and negative heuristic paths.
apps/ui/sources/components/tools/normalization/normalize/nameInference.ts Extended canonicalizeToolNameNonV2 with three additional change_title aliases, matching the CLI normalization. Uses exact case comparison (pre-existing pattern in this function).
apps/ui/sources/sync/reducer/messageToEvent.ts Added three new entries to CHANGE_TITLE_TOOL_NAMES Set: happier__change_title, happy__change_title, and change_title. Well-commented with provider context.
apps/ui/sources/sync/reducer/messageToEvent.test.ts Test expanded from 2 tool name variants to 5, covering all CHANGE_TITLE_TOOL_NAMES entries. Refactored to loop-based assertion for cleaner coverage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Tool call arrives with toolName"] --> B{"Is toolName a change_title variant?"}
    B -- "No" --> C["Standard canonicalization pipeline"]
    B -- "Yes (any of 5 aliases)" --> D["CLI/UI: Canonicalize to 'change_title'"]
    
    D --> E{"OpenCode ACP transport?"}
    E -- "No" --> F["Render as title change event"]
    E -- "Yes" --> G{"Has _acp.title == 'task'?"}
    G -- "Yes" --> H["Map to 'Task'"]
    G -- "No" --> I{"Has prompt/subagent_type WITHOUT title?"}
    I -- "Yes (heuristic)" --> H
    I -- "No" --> F

    style H fill:#4CAF50,color:#fff
    style F fill:#2196F3,color:#fff
    style D fill:#FF9800,color:#fff
Loading

Last reviewed commit: 7254216

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
apps/cli/src/agent/tools/normalization/index.ts (1)

279-286: Indentation mismatch with surrounding code.

The new block uses 2-space indentation; the rest of canonicalizeToolNameV2 (and the whole file) uses 4-space.

♻️ Proposed fix
-    if (
-      lower === 'mcp__happier__change_title' ||
-      lower === 'mcp__happy__change_title' ||
-      lower === 'happier__change_title' ||
-      lower === 'happy__change_title' ||
-      lower === 'change_title'
-    )
-      return 'change_title';
+    if (
+        lower === 'mcp__happier__change_title' ||
+        lower === 'mcp__happy__change_title' ||
+        lower === 'happier__change_title' ||
+        lower === 'happy__change_title' ||
+        lower === 'change_title'
+    ) return 'change_title';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/agent/tools/normalization/index.ts` around lines 279 - 286, The
new conditional block in canonicalizeToolNameV2 is indented with 2 spaces which
breaks consistency—update the block so all lines use 4-space indentation to
match the surrounding code style (including the if (...) lines and the return
'change_title'; line), ensuring the conditional parentheses and the return
statement align with other blocks in the function.
apps/ui/sources/sync/reducer/messageToEvent.test.ts (1)

20-34: Consider it.each for per-variant failure attribution.

With a for loop, the first failing expect halts the test and leaves the remaining four variants untested. Switching to it.each gives an independent result for each name, making regressions easier to pinpoint.

♻️ Proposed refactor
-    it('supports legacy + new + canonical change_title tool names', () => {
-        const events = [
-            parseMessageAsEvent(makeToolCallMessage('mcp__happy__change_title')),
-            parseMessageAsEvent(makeToolCallMessage('mcp__happier__change_title')),
-            parseMessageAsEvent(makeToolCallMessage('happier__change_title')),
-            parseMessageAsEvent(makeToolCallMessage('happy__change_title')),
-            parseMessageAsEvent(makeToolCallMessage('change_title')),
-        ];
-
-        for (const event of events) {
-            expect(event).toEqual({
-                type: 'message',
-                message: 'Title changed to "My title"',
-            });
-        }
-    });
+    it.each([
+        'mcp__happy__change_title',
+        'mcp__happier__change_title',
+        'happier__change_title',
+        'happy__change_title',
+        'change_title',
+    ])('recognises "%s" as a change_title tool', (toolName) => {
+        expect(parseMessageAsEvent(makeToolCallMessage(toolName))).toEqual({
+            type: 'message',
+            message: 'Title changed to "My title"',
+        });
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/sources/sync/reducer/messageToEvent.test.ts` around lines 20 - 34,
The test groups multiple tool-name variants into a single `for` loop which stops
at the first failure; replace that loop with Jest's table-driven tests using
`it.each` so each variant is its own test case. Iterate over the array of names
(the values passed into `makeToolCallMessage`) with `it.each(names)(...)`, call
`parseMessageAsEvent(makeToolCallMessage(name))` inside the test body, and
assert the expected object (`type: 'message', message: 'Title changed to "My
title"'`) for each; this preserves the same logic but gives per-variant failure
attribution for easier debugging.
apps/cli/src/backends/opencode/acp/transport.ts (1)

197-200: Unnecessary as any casts — input is already Record<string, unknown>.

input is typed as Record<string, unknown>, so input.prompt, input.subagent_type, and input.title are valid property accesses returning unknown. The (input as any)?. casts are redundant and violate the no-untyped-escapes guideline.

♻️ Proposed fix
-      const hasPrompt = typeof (input as any)?.prompt === 'string' && String((input as any).prompt).trim().length > 0;
-      const hasSubagentType =
-        typeof (input as any)?.subagent_type === 'string' && String((input as any).subagent_type).trim().length > 0;
-      const hasTitle = typeof (input as any)?.title === 'string' && String((input as any).title).trim().length > 0;
+      const hasPrompt = typeof input.prompt === 'string' && (input.prompt as string).trim().length > 0;
+      const hasSubagentType =
+        typeof input.subagent_type === 'string' && (input.subagent_type as string).trim().length > 0;
+      const hasTitle = typeof input.title === 'string' && (input.title as string).trim().length > 0;

As per coding guidelines: "Broad as any casts are forbidden except in boundary fixtures with a one-line justification."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/backends/opencode/acp/transport.ts` around lines 197 - 200, The
three boolean guards (hasPrompt, hasSubagentType, hasTitle) currently use
redundant (input as any)?. casts; remove those casts and directly access the
properties on input, relying on TypeScript's typeof narrowing (e.g. typeof
input['prompt'] === 'string' && input['prompt'].trim().length > 0) for each
check; update hasPrompt, hasSubagentType, and hasTitle to use input['prompt'],
input['subagent_type'], and input['title'] (or input.prompt etc.) with typeof
checks so no broad as any cast is needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/cli/src/agent/tools/normalization/index.ts`:
- Around line 279-286: The new conditional block in canonicalizeToolNameV2 is
indented with 2 spaces which breaks consistency—update the block so all lines
use 4-space indentation to match the surrounding code style (including the if
(...) lines and the return 'change_title'; line), ensuring the conditional
parentheses and the return statement align with other blocks in the function.

In `@apps/cli/src/backends/opencode/acp/transport.ts`:
- Around line 197-200: The three boolean guards (hasPrompt, hasSubagentType,
hasTitle) currently use redundant (input as any)?. casts; remove those casts and
directly access the properties on input, relying on TypeScript's typeof
narrowing (e.g. typeof input['prompt'] === 'string' &&
input['prompt'].trim().length > 0) for each check; update hasPrompt,
hasSubagentType, and hasTitle to use input['prompt'], input['subagent_type'],
and input['title'] (or input.prompt etc.) with typeof checks so no broad as any
cast is needed.

In `@apps/ui/sources/sync/reducer/messageToEvent.test.ts`:
- Around line 20-34: The test groups multiple tool-name variants into a single
`for` loop which stops at the first failure; replace that loop with Jest's
table-driven tests using `it.each` so each variant is its own test case. Iterate
over the array of names (the values passed into `makeToolCallMessage`) with
`it.each(names)(...)`, call `parseMessageAsEvent(makeToolCallMessage(name))`
inside the test body, and assert the expected object (`type: 'message', message:
'Title changed to "My title"'`) for each; this preserves the same logic but
gives per-variant failure attribution for easier debugging.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 21, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/cli/src/backends/gemini/utils/permissionHandler.ts (1)

24-33: Spreading all 5 aliases into substring-match arrays creates redundant entries.

Both alwaysAutoApproveToolNameIncludes and alwaysAutoApproveToolCallIdIncludes are checked with .some((t) => lower.includes(t)). Because all longer aliases ('mcp__happier__change_title', etc.) contain 'change_title' as a substring, the shortest alias already covers the entire alias list. The spread doesn't add correctness — only noise in the array.

That said, the current behavior is not incorrect, and the comment about "permissive" intent is clear. Consider keeping just 'change_title' (plus its isChangeTitleToolNameAlias helper if you want exact-match coverage elsewhere):

♻️ Optional simplification
-    this.alwaysAutoApproveToolNameIncludes = [
-      ...CHANGE_TITLE_TOOL_NAME_ALIASES,
-      'geminireasoning',
-      'codexreasoning',
-    ];
-    this.alwaysAutoApproveToolCallIdIncludes = [
-      // Some transports only expose the tool in the toolCallId; keep it permissive.
-      ...CHANGE_TITLE_TOOL_NAME_ALIASES,
-      'save_memory',
-    ];
+    this.alwaysAutoApproveToolNameIncludes = [
+      'change_title', // substring covers all MCP/legacy aliases
+      'geminireasoning',
+      'codexreasoning',
+    ];
+    this.alwaysAutoApproveToolCallIdIncludes = [
+      // Some transports only expose the tool in the toolCallId; keep it permissive.
+      'change_title', // substring covers all MCP/legacy aliases
+      'save_memory',
+    ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/backends/gemini/utils/permissionHandler.ts` around lines 24 -
33, The arrays alwaysAutoApproveToolNameIncludes and
alwaysAutoApproveToolCallIdIncludes redundantly spread
CHANGE_TITLE_TOOL_NAME_ALIASES; reduce noise by replacing the spread with the
minimal substring "change_title" (retain other explicit entries like
'geminireasoning', 'codexreasoning', 'save_memory'), and keep the
isChangeTitleToolNameAlias helper (or the full CHANGE_TITLE_TOOL_NAME_ALIASES
constant) for any places that need exact-match checks instead of substring
matching.
packages/protocol/src/tools/v2/aliases.ts (1)

27-29: isChangeTitleToolNameAlias silently returns false for mixed-case input — self-normalize or document the contract.

All entries in CHANGE_TITLE_TOOL_NAME_ALIASES are lowercase. Callers that forget to pre-normalize will get a silent false negative (e.g., isChangeTitleToolNameAlias('Change_Title')false). Current callers in this PR do normalize, but this is an invisible contract with no enforcement.

The simplest fix is to fold the normalization into the function itself:

🛡️ Proposed fix
 export function isChangeTitleToolNameAlias(name: string): boolean {
-  return (CHANGE_TITLE_TOOL_NAME_ALIASES as readonly string[]).includes(name);
+  return (CHANGE_TITLE_TOOL_NAME_ALIASES as readonly string[]).includes(name.toLowerCase());
 }

If keeping the current contract, add a JSDoc @param note such as @param name - must be pre-normalized to lowercase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/protocol/src/tools/v2/aliases.ts` around lines 27 - 29, The function
isChangeTitleToolNameAlias currently does a case-sensitive lookup against
CHANGE_TITLE_TOOL_NAME_ALIASES (all lowercase) which causes silent false
negatives for mixed-case input; update isChangeTitleToolNameAlias to normalize
the input (e.g., use name.toLowerCase()) before checking includes (i.e., call
(CHANGE_TITLE_TOOL_NAME_ALIASES as readonly
string[]).includes(name.toLowerCase())), or alternatively add a JSDoc `@param`
noting the caller must pass a lowercase name—prefer implementing the
normalization inside isChangeTitleToolNameAlias for safety.
apps/cli/src/agent/runtime/changeTitleInstruction.ts (1)

13-13: Hardcoded default 'mcp__happier__change_title' could drift from CHANGE_TITLE_TOOL_NAME_ALIASES.

The fallback default on line 13 duplicates the string already present at CHANGE_TITLE_TOOL_NAME_ALIASES[1]. If the alias list is ever reordered or the preferred MCP name changes, this default silently diverges.

♻️ Suggested improvement
+// CHANGE_TITLE_TOOL_NAME_ALIASES[1] is the preferred MCP-prefixed name
+const DEFAULT_PREFERRED_TOOL_NAME = CHANGE_TITLE_TOOL_NAME_ALIASES[1]; // 'mcp__happier__change_title'

 export const buildChangeTitleInstruction = (opts: ChangeTitleInstructionOptions = {}): string => {
-  const preferred = (opts.preferredToolName ?? 'mcp__happier__change_title').trim();
+  const preferred = (opts.preferredToolName ?? DEFAULT_PREFERRED_TOOL_NAME).trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/agent/runtime/changeTitleInstruction.ts` at line 13, The default
fallback for preferred tool name is hardcoded to 'mcp__happier__change_title'
which duplicates an entry in CHANGE_TITLE_TOOL_NAME_ALIASES and may drift;
change the fallback to use the alias array instead (use
CHANGE_TITLE_TOOL_NAME_ALIASES[1] or a safe alias lookup) when computing
preferred from opts.preferredToolName, and ensure you still call .trim() and
handle the case where the alias array may be missing or empty (fall back to a
sensible alias or throw a clear error).
packages/protocol/src/tools/v2/index.ts (1)

20-20: ChangeTitleToolNameAliasSchema is not re-exported, leaving the runtime validator inaccessible through the public index.

aliases.ts exports both the schema (ChangeTitleToolNameAliasSchema) and the type (ChangeTitleToolNameAlias), but only the type is re-exported here. Any downstream consumer that needs to validate a value at runtime (.parse() / .safeParse()) is blocked from doing so through the standard package entry point.

♻️ Proposed addition
-export { CHANGE_TITLE_TOOL_NAME_ALIASES, isChangeTitleToolNameAlias, type ChangeTitleToolNameAlias } from './aliases.js';
+export {
+  CHANGE_TITLE_TOOL_NAME_ALIASES,
+  ChangeTitleToolNameAliasSchema,
+  isChangeTitleToolNameAlias,
+  type ChangeTitleToolNameAlias,
+} from './aliases.js';

This also brings the export block in line with the multi-line style used by every other export group in the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/protocol/src/tools/v2/index.ts` at line 20, The public index
currently re-exports the type ChangeTitleToolNameAlias but not the runtime
validator ChangeTitleToolNameAliasSchema; update the export group to also export
ChangeTitleToolNameAliasSchema from './aliases.js' so downstream consumers can
call .parse()/ .safeParse(); ensure the export statement includes
CHANGE_TITLE_TOOL_NAME_ALIASES, isChangeTitleToolNameAlias,
ChangeTitleToolNameAliasSchema, and type ChangeTitleToolNameAlias (matching the
multi-line style used elsewhere).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/cli/src/agent/runtime/changeTitleInstruction.ts`:
- Around line 4-10: Replace the exported type alias
ChangeTitleInstructionOptions with an exported interface of the same name to
follow the coding guideline; keep the identical property signature
preferredToolName?: string and the JSDoc comment (including the default note),
and update any local references/usages to continue to refer to
ChangeTitleInstructionOptions (no other API changes required).
- Around line 12-24: Add a top-level JSDoc block describing this module's
responsibility (e.g., that it builds a localized instruction string for the
change-title tool and lists exported helpers) and place it before any
imports/exports; then convert the exported function declaration
buildChangeTitleInstruction to an exported arrow function expression (preserving
the same parameter name ChangeTitleInstructionOptions, defaulting behavior,
usages of preferred, fallbacks, fallbackPreview, and the trimIdent return) to
satisfy the arrow-function guideline.

In `@apps/cli/src/backends/gemini/runtime/geminiTurnMessageState.ts`:
- Line 23: The import statement for hasChangeTitleInstruction was inserted
mid-file between function definitions; move the import for
hasChangeTitleInstruction to the top of the module (with the other imports) so
all imports are declared before any code or function/class declarations (look
for hasChangeTitleInstruction and the surrounding functions/types in
geminiTurnMessageState.ts and relocate that import to the file header).

---

Nitpick comments:
In `@apps/cli/src/agent/runtime/changeTitleInstruction.ts`:
- Line 13: The default fallback for preferred tool name is hardcoded to
'mcp__happier__change_title' which duplicates an entry in
CHANGE_TITLE_TOOL_NAME_ALIASES and may drift; change the fallback to use the
alias array instead (use CHANGE_TITLE_TOOL_NAME_ALIASES[1] or a safe alias
lookup) when computing preferred from opts.preferredToolName, and ensure you
still call .trim() and handle the case where the alias array may be missing or
empty (fall back to a sensible alias or throw a clear error).

In `@apps/cli/src/backends/gemini/utils/permissionHandler.ts`:
- Around line 24-33: The arrays alwaysAutoApproveToolNameIncludes and
alwaysAutoApproveToolCallIdIncludes redundantly spread
CHANGE_TITLE_TOOL_NAME_ALIASES; reduce noise by replacing the spread with the
minimal substring "change_title" (retain other explicit entries like
'geminireasoning', 'codexreasoning', 'save_memory'), and keep the
isChangeTitleToolNameAlias helper (or the full CHANGE_TITLE_TOOL_NAME_ALIASES
constant) for any places that need exact-match checks instead of substring
matching.

In `@packages/protocol/src/tools/v2/aliases.ts`:
- Around line 27-29: The function isChangeTitleToolNameAlias currently does a
case-sensitive lookup against CHANGE_TITLE_TOOL_NAME_ALIASES (all lowercase)
which causes silent false negatives for mixed-case input; update
isChangeTitleToolNameAlias to normalize the input (e.g., use name.toLowerCase())
before checking includes (i.e., call (CHANGE_TITLE_TOOL_NAME_ALIASES as readonly
string[]).includes(name.toLowerCase())), or alternatively add a JSDoc `@param`
noting the caller must pass a lowercase name—prefer implementing the
normalization inside isChangeTitleToolNameAlias for safety.

In `@packages/protocol/src/tools/v2/index.ts`:
- Line 20: The public index currently re-exports the type
ChangeTitleToolNameAlias but not the runtime validator
ChangeTitleToolNameAliasSchema; update the export group to also export
ChangeTitleToolNameAliasSchema from './aliases.js' so downstream consumers can
call .parse()/ .safeParse(); ensure the export statement includes
CHANGE_TITLE_TOOL_NAME_ALIASES, isChangeTitleToolNameAlias,
ChangeTitleToolNameAliasSchema, and type ChangeTitleToolNameAlias (matching the
multi-line style used elsewhere).

Comment on lines +4 to +10
export type ChangeTitleInstructionOptions = {
/**
* Preferred tool name to mention first.
* Defaults to `mcp__happier__change_title` (MCP convention).
*/
preferredToolName?: string;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use interface instead of type for ChangeTitleInstructionOptions.

As per coding guidelines, prefer interface over type for defining object shapes in TypeScript.

♻️ Proposed fix
-export type ChangeTitleInstructionOptions = {
-  /**
-   * Preferred tool name to mention first.
-   * Defaults to `mcp__happier__change_title` (MCP convention).
-   */
-  preferredToolName?: string;
-};
+export interface ChangeTitleInstructionOptions {
+  /**
+   * Preferred tool name to mention first.
+   * Defaults to `mcp__happier__change_title` (MCP convention).
+   */
+  preferredToolName?: string;
+}

As per coding guidelines: "Prefer interface over type for defining object shapes in TypeScript."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/agent/runtime/changeTitleInstruction.ts` around lines 4 - 10,
Replace the exported type alias ChangeTitleInstructionOptions with an exported
interface of the same name to follow the coding guideline; keep the identical
property signature preferredToolName?: string and the JSDoc comment (including
the default note), and update any local references/usages to continue to refer
to ChangeTitleInstructionOptions (no other API changes required).

Comment on lines +12 to +24
export function buildChangeTitleInstruction(opts: ChangeTitleInstructionOptions = {}): string {
const preferred = (opts.preferredToolName ?? 'mcp__happier__change_title').trim();
const fallbacks = CHANGE_TITLE_TOOL_NAME_ALIASES.filter((n) => n !== preferred);
const fallbackPreview = fallbacks.slice(0, 3).join(', ');

return trimIdent(
`Based on the user's message, use the chat title tool to set (or update) a short, descriptive session title.

The tool may be exposed under different names depending on the provider. Prefer "${preferred}" when available; otherwise use an equivalent alias (for example: ${fallbackPreview}).

Call this tool again if the task changes significantly.`
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing file-level JSDoc header and export function should be an arrow function.

Two guideline violations in the new function:

  1. File header JSDoc: The file has no top-level JSDoc block describing its responsibilities. The guidelines require: "Include comprehensive JSDoc comments as file header comments explaining file responsibilities."

  2. Arrow function preferred: export function buildChangeTitleInstruction is a function declaration. Guidelines say "Prefer arrow functions over function declarations."

♻️ Proposed fixes

Add a file-level JSDoc before the import:

+/**
+ * changeTitleInstruction
+ *
+ * Builds provider-agnostic prompts instructing the agent to call the
+ * change-title tool, with dynamic alias fallbacks. Exports both the
+ * builder and a pre-built default constant for backend injection.
+ */
 import { CHANGE_TITLE_TOOL_NAME_ALIASES } from '@happier-dev/protocol/tools/v2';

Convert the function declaration to an arrow function:

-export function buildChangeTitleInstruction(opts: ChangeTitleInstructionOptions = {}): string {
+export const buildChangeTitleInstruction = (opts: ChangeTitleInstructionOptions = {}): string => {
   const preferred = (opts.preferredToolName ?? 'mcp__happier__change_title').trim();
   const fallbacks = CHANGE_TITLE_TOOL_NAME_ALIASES.filter((n) => n !== preferred);
   const fallbackPreview = fallbacks.slice(0, 3).join(', ');

   return trimIdent(
     `Based on the user's message, use the chat title tool to set (or update) a short, descriptive session title.

The tool may be exposed under different names depending on the provider. Prefer "${preferred}" when available; otherwise use an equivalent alias (for example: ${fallbackPreview}).

Call this tool again if the task changes significantly.`
   );
-}
+};

As per coding guidelines: "Include comprehensive JSDoc comments as file header comments explaining file responsibilities" and "Prefer arrow functions over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/agent/runtime/changeTitleInstruction.ts` around lines 12 - 24,
Add a top-level JSDoc block describing this module's responsibility (e.g., that
it builds a localized instruction string for the change-title tool and lists
exported helpers) and place it before any imports/exports; then convert the
exported function declaration buildChangeTitleInstruction to an exported arrow
function expression (preserving the same parameter name
ChangeTitleInstructionOptions, defaulting behavior, usages of preferred,
fallbacks, fallbackPreview, and the trimIdent return) to satisfy the
arrow-function guideline.

};
}

import { hasChangeTitleInstruction } from '../utils/promptUtils';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Import placed mid-file — must be moved to the top.

The hasChangeTitleInstruction import sits between two function definitions (after line 21, before line 25), violating the explicit rule: "ALL imports must be at the top of the file - NEVER import modules mid-code."

🔧 Proposed fix

Move the import to the top of the file, before all declarations:

+import { hasChangeTitleInstruction } from '../utils/promptUtils';
+
 export type GeminiTurnMessageState = {
   thinking: boolean;
   ...
 };
 
 export function createGeminiTurnMessageState(): GeminiTurnMessageState { ... }
 
-import { hasChangeTitleInstruction } from '../utils/promptUtils';
-
 export function resetGeminiTurnMessageStateForPrompt(

As per coding guidelines: "ALL imports must be at the top of the file - NEVER import modules mid-code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/backends/gemini/runtime/geminiTurnMessageState.ts` at line 23,
The import statement for hasChangeTitleInstruction was inserted mid-file between
function definitions; move the import for hasChangeTitleInstruction to the top
of the module (with the other imports) so all imports are declared before any
code or function/class declarations (look for hasChangeTitleInstruction and the
surrounding functions/types in geminiTurnMessageState.ts and relocate that
import to the file header).

leeroybrun added a commit that referenced this pull request Mar 2, 2026
Ports and hardens the bot-generated PR changes against current dev worktree.

- #60: clarify default model label + prune Claude model suggestions
- #64: centralize change_title aliases + normalize across UI/CLI providers
- #71: document skipping capability probe on Codex ACP resume
- #78: stabilize iOS PathPicker header options while typing
- #83: remove synthetic stop+explain follow-up; unify stop button copy/i18n
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

change title

1 participant