Skip to content

tab: enforce deterministic model arg and improve tab-name extraction (opencode)#487

Draft
chr1syy wants to merge 8 commits intoRunMaestro:mainfrom
chr1syy:tab-naming-fix
Draft

tab: enforce deterministic model arg and improve tab-name extraction (opencode)#487
chr1syy wants to merge 8 commits intoRunMaestro:mainfrom
chr1syy:tab-naming-fix

Conversation

@chr1syy
Copy link
Contributor

@chr1syy chr1syy commented Mar 1, 2026

This pull request makes significant improvements to the tab naming logic for agent-based sessions, focusing on more robust and predictable model resolution, enhanced argument sanitization, and improved debugging and logging. The changes ensure that the correct model is used for tab naming, prevent invalid or duplicate CLI arguments, and provide better diagnostics for troubleshooting tab naming quality.

Model Resolution and Argument Handling:

  • Improved the logic for resolving the model ID used in tab naming by strictly preferring (in order): session override, agent config (only if it looks like a valid provider/model), then the agent's default model. The resolved model ID is sanitized to remove trailing slashes and empty values.
  • Enhanced argument sanitization by filtering out non-string arguments, deduplicating --model flags, converting them to a single --model=<value> form, and ensuring only valid model values are passed to the agent CLI. Also, the canonical model flag is injected just before process spawn.

Debugging and Logging Enhancements:

  • Added detailed debug logging throughout the tab naming process, including resolved model information, argument lists before spawning, and final model values. This aids in diagnosing issues with model selection and argument construction. [1] [2] [3]
  • Logged the raw output from the agent before tab name extraction, and added warnings when the agent returns generic or low-quality tab names, helping to identify and address prompt/model issues.

Tab Name Extraction Improvements:

  • Updated the tab name extraction logic to allow quoted single-line outputs and to apply filtering rules to the unquoted value, making extraction more robust to agent output formatting.

Summary by CodeRabbit

  • New Features

    • Session-level model override and per-agent default model support for tab naming; model flags are canonicalized during execution.
  • Bug Fixes

    • Deterministic model resolution and sanitization to prevent malformed executions.
    • Safer SSH stdin routing so prompts are reliably sent for remote runs.
    • Final-argument sanitization to avoid non-string values.
  • Improvements

    • Enhanced diagnostics/logging for model resolution, argument construction and spawn details.
    • Better detection/filtering of quoted or spurious outputs when extracting tab names.
  • Tests

    • Updated tests to cover stdin-driven SSH flow and revised unquoting behavior.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional sessionCustomModel and agent.defaultModel, implements deterministic model resolution and canonical model flag injection in tab-naming flow, routes prompts via stdin for SSH using a new SSH helper, enhances diagnostics/sanitization, and refines tab-name extraction/filtering.

Changes

Cohort / File(s) Summary
Tab naming handler (main process)
src/main/ipc/handlers/tabNaming.ts
Add sessionCustomModel?; deterministic model resolution (sessionCustomModel > agent-config model (with '/') > agent.defaultModel); sanitize model string; remove existing --model/-m flags and inject canonical model args; pass sessionCustomModel into overrides resolution; add safeDebug, diagnostics, finalArgs sanitization, and improved extractTabName filtering and unquoting.
Agent config type update
src/main/agents/definitions.ts
Add optional defaultModel?: string to AgentConfig.
Preload tab naming config
src/main/preload/tabNaming.ts
Expose sessionCustomModel?: string in TabNamingConfig.
Renderer API typing
src/renderer/global.d.ts
Add sessionCustomModel?: string to Maestro API tabNaming.generateTabName config typing and related API surface.
Renderer call site
src/renderer/hooks/input/useInputProcessing.ts
Pass sessionCustomModel: activeSession.customModel when calling window.maestro.tabNaming.generateTabName.
SSH command builder & tests
main/utils/...ssh-command-builder*, src/__tests__/main/ipc/handlers/tabNaming.test.ts
Introduce/replace usage with buildSshCommandWithStdin (returns sshStdinScript); tests updated to assert stdin-driven SSH flow and use stdinInput/sshStdinScript shape.
Child process spawn detection
src/main/process-manager/spawners/ChildProcessSpawner.ts
Consider sshStdinScript when deciding to send prompts via stdin (expands conditions for stdin usage).
Public IPC declaration
src/main/ipc/... (declarations)
Update IPC handler input type for tabNaming:generateTabName to include sessionCustomModel?: string.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Renderer
  participant IPC as Main IPC
  participant Handler as TabNamingHandler
  participant Spawner as ChildProcessSpawner
  participant Agent as Agent/CLI
  participant Extractor as extractTabName

  Renderer->>IPC: generateTabName(userMessage, agentType, cwd, sessionCustomModel)
  IPC->>Handler: forward request
  Handler->>Handler: load agent config (incl. defaultModel)
  Handler->>Handler: resolve model (sessionCustomModel > agent-config model with '/' > defaultModel) 
  Handler->>Handler: strip existing --model/-m, inject canonical model args
  Handler->>Spawner: prepare finalArgs (include sshStdinScript indicator if SSH)
  alt SSH with stdin script
    Spawner->>Agent: build SSH command with stdin (buildSshCommandWithStdin), send prompt via stdin
  else local or SSH without stdin
    Spawner->>Agent: spawn process with finalArgs
  end
  Agent-->>Handler: stdout/stderr
  Handler->>Extractor: extractTabName(raw output)
  Extractor-->>Handler: return candidate name or warning
  Handler-->>IPC: return tab name + diagnostics
  IPC-->>Renderer: deliver tab name
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: deterministic model argument handling and improved tab-name extraction, matching the core objectives and implementation details across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 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.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/ipc/handlers/tabNaming.ts (1)

261-262: ⚠️ Potential issue | 🟡 Minor

Non-adjacent includes check may produce false positives.

The condition finalArgs.includes('--input-format') && finalArgs.includes('stream-json') could incorrectly return true if 'stream-json' appears elsewhere in the args (e.g., as a value for a different flag). Consider checking adjacency:

-const hasStreamJsonInput =
-  finalArgs.includes('--input-format') && finalArgs.includes('stream-json');
+const inputFormatIdx = finalArgs.indexOf('--input-format');
+const hasStreamJsonInput =
+  inputFormatIdx !== -1 && finalArgs[inputFormatIdx + 1] === 'stream-json';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 261 - 262, The current
hasStreamJsonInput check can false-positive because it only checks
finalArgs.includes('--input-format') && finalArgs.includes('stream-json');
instead, find the index of '--input-format' in finalArgs (e.g., const idx =
finalArgs.indexOf('--input-format')) and set hasStreamJsonInput = idx !== -1 &&
finalArgs[idx + 1] === 'stream-json' so you verify adjacency; update the code
around the hasStreamJsonInput variable in tabNaming.ts accordingly.
🧹 Nitpick comments (4)
src/main/ipc/handlers/tabNaming.ts (4)

135-152: Type casting suggests missing type definition for sessionCustomModel.

The config parameter is cast to any to access sessionCustomModel, but this property isn't defined in the config type (lines 83-91). If this property is expected, add it to the type definition for type safety.

 async (config: {
   userMessage: string;
   agentType: string;
   cwd: string;
+  sessionCustomModel?: string;
   sessionSshRemoteConfig?: {
     enabled: boolean;
     remoteId: string | null;
     workingDirOverride?: string;
   };
 }): Promise<string | null> => {

Then replace the (config as any).sessionCustomModel casts with direct property access.

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

In `@src/main/ipc/handlers/tabNaming.ts` around lines 135 - 152, The code uses
(config as any).sessionCustomModel to read sessionCustomModel and bypasses
typing; add sessionCustomModel?: string to the config type/interface used by the
function that declares the config parameter so the property is properly typed,
then remove the casts and access config.sessionCustomModel directly when setting
resolvedModelId (the same pattern should be preserved for resolvedModelId,
agentConfigValues.model and (agent as any).defaultModel — replace the (agent as
any).defaultModel cast by adding defaultModel?: string to the Agent
type/interface or using the existing Agent type so you can use
agent.defaultModel directly).

391-402: Mutating resolvedModelId affects diagnostic logging.

The mutation at line 401 changes resolvedModelId after it was originally resolved. This variable is later used in logging (line 468), which will show the modified value rather than the original resolution. Consider using a separate variable for the final injected model.

+          let finalModelForInjection = resolvedModelId;
           if (
-            !resolvedModelId.includes('/') &&
+            !finalModelForInjection.includes('/') &&
             (agent as any).defaultModel &&
             typeof (agent as any).defaultModel === 'string' &&
             (agent as any).defaultModel.includes('/')
           ) {
-            resolvedModelId = (agent as any).defaultModel as string;
+            finalModelForInjection = (agent as any).defaultModel as string;
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 391 - 402, The code
currently mutates resolvedModelId (in the block that prefers agent.defaultModel
when resolvedModelId lacks a provider prefix), which alters the value later used
for diagnostic logging; instead, introduce a new variable (e.g., injectedModelId
or cliModelId) to hold the model string that will be injected as a CLI flag and
leave resolvedModelId unchanged for logs. Update the injection logic to assign
to injectedModelId when you would have reassigned resolvedModelId and use
injectedModelId for any CLI/config insertion while keeping resolvedModelId for
later logging and diagnostics; reference resolvedModelId, (agent as
any).defaultModel, and the injection site in tabNaming.ts when making the
change.

161-171: Overly defensive try/catch around debug logging.

The try/catch blocks wrapping console.debug calls appear throughout the file (lines 161-171, 190-201, 293-317, etc.). console.debug rarely throws, so this adds noise without meaningful protection.

Consider a helper function if error isolation is truly needed:

const safeDebug = (message: string, data?: object) => {
  try { console.debug(message, data); } catch { /* swallow */ }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 161 - 171, Remove the
repetitive try/catch wrappers around console.debug in this module and replace
them with a single safeDebug helper; create a top-level const safeDebug = (msg:
string, data?: any) => { try { console.debug(msg, data); } catch { /* swallow */
} } and then change each protected console.debug call (the ones in tabNaming.ts
that currently wrap sessionId/agentType/agentConfigModel/resolvedModelId and
similar debug blocks) to call safeDebug(message, data) instead; this keeps error
isolation but eliminates noisy duplicated try/catch blocks and centralizes
behavior.

319-374: Redundant model flag processing.

The deduplication logic (lines 323-357) extracts the last --model value and consolidates it, then lines 358-374 convert --model <value> to --model=<value>, and finally lines 376-424 strip all model tokens and re-inject the canonical one.

Since lines 376-424 remove all model tokens anyway, the earlier deduplication and conversion steps are effectively discarded. Consider simplifying to just:

  1. Remove all model tokens
  2. Inject the canonical model
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 319 - 374, The earlier
two-step dedupe/convert logic around finalArgs (the inner try using lastModelVal
and the rebuilt conversion into `--model=<value>`) is redundant because later
code already strips model tokens and reinjects a canonical model; remove the
whole deduplication block (the try { ... } that computes lastModelVal and the
subsequent loop that builds rebuilt) and replace it with a single pass before
the prompt separator: compute sepIndex from finalArgs, filter out any `--model`
and `--model=...` tokens in finalArgs.slice(0, sepIndex), then if you have a
canonical model id (use resolvedModelId or the previously determined canonical
variable), push `--model=<canonical>` into the filtered args, and set finalArgs
= [...filteredArgs, ...finalArgs.slice(sepIndex)]; keep the console.debug that
logs sessionId and the canonical value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 261-262: The current hasStreamJsonInput check can false-positive
because it only checks finalArgs.includes('--input-format') &&
finalArgs.includes('stream-json'); instead, find the index of '--input-format'
in finalArgs (e.g., const idx = finalArgs.indexOf('--input-format')) and set
hasStreamJsonInput = idx !== -1 && finalArgs[idx + 1] === 'stream-json' so you
verify adjacency; update the code around the hasStreamJsonInput variable in
tabNaming.ts accordingly.

---

Nitpick comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 135-152: The code uses (config as any).sessionCustomModel to read
sessionCustomModel and bypasses typing; add sessionCustomModel?: string to the
config type/interface used by the function that declares the config parameter so
the property is properly typed, then remove the casts and access
config.sessionCustomModel directly when setting resolvedModelId (the same
pattern should be preserved for resolvedModelId, agentConfigValues.model and
(agent as any).defaultModel — replace the (agent as any).defaultModel cast by
adding defaultModel?: string to the Agent type/interface or using the existing
Agent type so you can use agent.defaultModel directly).
- Around line 391-402: The code currently mutates resolvedModelId (in the block
that prefers agent.defaultModel when resolvedModelId lacks a provider prefix),
which alters the value later used for diagnostic logging; instead, introduce a
new variable (e.g., injectedModelId or cliModelId) to hold the model string that
will be injected as a CLI flag and leave resolvedModelId unchanged for logs.
Update the injection logic to assign to injectedModelId when you would have
reassigned resolvedModelId and use injectedModelId for any CLI/config insertion
while keeping resolvedModelId for later logging and diagnostics; reference
resolvedModelId, (agent as any).defaultModel, and the injection site in
tabNaming.ts when making the change.
- Around line 161-171: Remove the repetitive try/catch wrappers around
console.debug in this module and replace them with a single safeDebug helper;
create a top-level const safeDebug = (msg: string, data?: any) => { try {
console.debug(msg, data); } catch { /* swallow */ } } and then change each
protected console.debug call (the ones in tabNaming.ts that currently wrap
sessionId/agentType/agentConfigModel/resolvedModelId and similar debug blocks)
to call safeDebug(message, data) instead; this keeps error isolation but
eliminates noisy duplicated try/catch blocks and centralizes behavior.
- Around line 319-374: The earlier two-step dedupe/convert logic around
finalArgs (the inner try using lastModelVal and the rebuilt conversion into
`--model=<value>`) is redundant because later code already strips model tokens
and reinjects a canonical model; remove the whole deduplication block (the try {
... } that computes lastModelVal and the subsequent loop that builds rebuilt)
and replace it with a single pass before the prompt separator: compute sepIndex
from finalArgs, filter out any `--model` and `--model=...` tokens in
finalArgs.slice(0, sepIndex), then if you have a canonical model id (use
resolvedModelId or the previously determined canonical variable), push
`--model=<canonical>` into the filtered args, and set finalArgs =
[...filteredArgs, ...finalArgs.slice(sepIndex)]; keep the console.debug that
logs sessionId and the canonical value.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72c43b4 and f958e5b.

📒 Files selected for processing (1)
  • src/main/ipc/handlers/tabNaming.ts

@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR improves tab-naming reliability for agent-based sessions by introducing deterministic model resolution (session override → agent-config with /agent.defaultModel), canonicalizing --model flags before SSH wrapping, switching SSH execution to the new buildSshCommandWithStdin path that avoids all shell-escaping layers, and fixing extractTabName to accept fully-quoted outputs.

Key changes and findings:

  • Model resolution is now explicit and well-ordered; sessionCustomModel is finally threaded from the renderer through the IPC boundary.
  • Prompt double-injection in SSH mode (new bug): buildAgentArgs adds fullPrompt to finalArgs, which are included verbatim in the remote shell script by buildSshCommandWithStdin. Simultaneously, stdinInput: fullPrompt is appended after the exec line. The remote agent receives the same prompt twice — once as a shell-escaped CLI arg and once as raw stdin.
  • Dangling -m flag: the strip loop skips the -m flag when it appears as the last token in the prefix, leaving a valueless short-form flag in the final args.
  • Comment / intent mismatch: applyAgentConfigOverrides is described as "customArgs/env only" but still receives the full agentConfigValues including model; the model injection is silently overwritten by the canonicalization block, which works but contradicts the comment and is a future maintenance hazard.
  • The extractTabName quoted-string change and the SSH stdin-script approach are solid improvements; supporting tests are updated correctly.

Confidence Score: 2/5

  • Not safe to merge as-is: SSH tab naming will send the prompt twice to the remote agent, likely producing garbled or duplicated responses.
  • The prompt double-injection in SSH mode is a functional regression for any user running tab naming over SSH remote — the remote agent receives the full tab-naming prompt both as a CLI arg (shell-escaped inside the bash script) and as raw stdin. The dangling -m edge case is an uncommon but silent failure mode. Both issues are confined to tabNaming.ts and are straightforward to fix without touching other subsystems.
  • src/main/ipc/handlers/tabNaming.ts requires the most attention, specifically the buildAgentArgs/buildSshCommandWithStdin integration and the -m strip loop.

Important Files Changed

Filename Overview
src/main/ipc/handlers/tabNaming.ts Core logic file with significant new changes: deterministic model resolution, pre-SSH model canonicalization, and SSH stdin script support. Contains a prompt double-injection bug in SSH mode (fullPrompt present in both finalArgs and stdinInput), a dangling -m strip edge-case, and a misleading comment about applyAgentConfigOverrides model isolation.
src/main/process-manager/spawners/ChildProcessSpawner.ts Small, focused change: adds !!config.sshStdinScript to both promptViaStdin and isStreamJsonMode. The sshStdinScript branch for writing stdin already existed; this PR correctly wires up the new flag. No new issues introduced here.
src/main/agents/definitions.ts Adds optional defaultModel?: string field to AgentConfig. Clean, additive change with no issues.
src/tests/main/ipc/handlers/tabNaming.test.ts Updated tests to use buildSshCommandWithStdin instead of buildSshCommand, adjusts SSH mock to return a stdinScript, and flips the quoted-output expectation from null to 'Quoted Tab Name'. Tests reflect the new behavior accurately but the SSH test does not cover the prompt double-injection scenario.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[generateTabName called] --> B[getAgent]
    B --> C[Resolve resolvedModelId\nsession → agentConfig with / → defaultModel]
    C --> D[buildAgentArgs\nwith prompt added to finalArgs]
    D --> E[applyAgentConfigOverrides\nstill injects model from agentConfigValues]
    E --> F[Canonicalize model flags\nstrip all --model/-m\nre-inject resolvedModelId]
    F --> G{SSH enabled?}

    G -- No --> H[Final safety strip\nnon-string args]
    G -- Yes --> I[buildSshCommandWithStdin\nargs=finalArgs incl. prompt\nstdinInput=fullPrompt]
    I --> J[⚠️ Prompt in both\nCLI args AND stdinInput]
    J --> K[sshStdinScript set]
    K --> H

    H --> L[processManager.spawn\nwith sshStdinScript or plain args]
    L --> M{sshStdinScript?}
    M -- Yes --> N[Write full bash script\nto child stdin\nPrompt appears twice in remote]
    M -- No --> O[Prompt as CLI arg only]

    N --> P[extractTabName]
    O --> P
    P --> Q{Fully-wrapped quoted?}
    Q -- Yes --> R[Strip quotes, return inner value]
    Q -- No, starts with quote --> S[Discard line]
    Q -- No --> T[Apply length and keyword filters]
    T --> R
Loading

Last reviewed commit: 1a3f3cb

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 9 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +292 to +424
// Final safety sanitization: ensure args are all plain strings
try {
const nonStringItems = finalArgs.filter((a) => typeof a !== 'string');
if (nonStringItems.length > 0) {
// eslint-disable-next-line no-console
console.debug('[TabNaming] Removing non-string args before spawn', {
sessionId,
removed: nonStringItems.map((i) => ({ typeof: typeof i, preview: String(i) })),
});
finalArgs = finalArgs.filter((a) => typeof a === 'string');
}

// Extract model arg value for debugging (if present)
const modelIndex = finalArgs.indexOf('--model');
if (modelIndex !== -1 && finalArgs.length > modelIndex + 1) {
const modelVal = finalArgs[modelIndex + 1];
// eslint-disable-next-line no-console
console.debug('[TabNaming] Final --model value', {
sessionId,
value: modelVal,
type: typeof modelVal,
});
}
} catch (err) {
// swallow safety log errors
}

// Quote model values that contain slashes so they survive shell-based
// spawns (PowerShell can interpret unquoted tokens containing slashes).
try {
// Deduplicate --model flags and ensure exactly one is present before the prompt separator
try {
const sepIndex =
finalArgs.indexOf('--') >= 0 ? finalArgs.indexOf('--') : finalArgs.length;
let lastModelVal: string | undefined;
for (let i = 0; i < sepIndex; i++) {
if (finalArgs[i] === '--model' && finalArgs.length > i + 1) {
const cand = finalArgs[i + 1];
if (typeof cand === 'string' && cand.trim()) {
lastModelVal = cand;
}
}
}

if (lastModelVal !== undefined) {
const newArgs: string[] = [];
for (let i = 0; i < sepIndex; i++) {
if (finalArgs[i] === '--model') {
i++; // skip value
continue;
}
newArgs.push(finalArgs[i]);
}
// Insert the single canonical model flag
newArgs.push('--model', lastModelVal);
// Append remaining args (including '--' and prompt)
finalArgs = [...newArgs, ...finalArgs.slice(sepIndex)];
// eslint-disable-next-line no-console
console.debug('[TabNaming] Deduplicated --model flags', {
sessionId,
canonical: lastModelVal,
});
}
} catch (err) {
// ignore dedupe failures
}
// Convert separate --model <value> pairs into a single --model=<value>
// token so shells don't split values. Then enforce a single canonical
// CLI model token derived from our resolvedModelId (if available).
const rebuilt: string[] = [];
for (let i = 0; i < finalArgs.length; i++) {
const a = finalArgs[i];
if (a === '--model' && i + 1 < finalArgs.length) {
const raw = finalArgs[i + 1];
const val =
typeof raw === 'string' ? raw.replace(/^['\"]|['\"]$/g, '') : String(raw);
rebuilt.push(`--model=${val}`);
i++; // skip the value
} else {
rebuilt.push(a);
}
}
finalArgs = rebuilt;

// Remove any existing model tokens (either --model=... or -m/value)
const withoutModel: string[] = [];
for (let i = 0; i < finalArgs.length; i++) {
const a = finalArgs[i];
if (typeof a === 'string' && a.startsWith('--model')) {
// skip
continue;
}
if (a === '-m' && i + 1 < finalArgs.length) {
i++; // skip short form value
continue;
}
withoutModel.push(a);
}

// If we have a resolvedModelId (from session/agent/default), prefer inserting
// it explicitly as a CLI flag to avoid relying on OpenCode config/env.
if (resolvedModelId && typeof resolvedModelId === 'string') {
// If resolvedModelId doesn't look like provider/model, prefer agent.defaultModel
if (
!resolvedModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
resolvedModelId = (agent as any).defaultModel as string;
}

if (resolvedModelId && resolvedModelId.includes('/')) {
const modelToken = `--model=${resolvedModelId}`;
// Insert before the argument separator `--` if present
const sep = withoutModel.indexOf('--');
if (sep === -1) {
withoutModel.push(modelToken);
} else {
withoutModel.splice(sep, 0, modelToken);
}
// eslint-disable-next-line no-console
console.debug('[TabNaming] Injected canonical --model for spawn', {
sessionId,
model: resolvedModelId,
});
}
}

finalArgs = withoutModel;
} catch (err) {
// swallow
}
Copy link

Choose a reason for hiding this comment

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

Model injection runs after SSH command wrapping

The entire new sanitization and model-injection block (lines 292–424) executes after the SSH command is constructed (lines 244–290). When SSH is enabled, finalArgs at this point already contains the SSH-specific argument list (i.e., the remote agent command has been embedded as a single shell string or as SSH flags). The model-injection logic then scans those SSH args for -- separators and injects --model=<value> into the wrong position, corrupting the SSH command structure.

To fix this, the model injection must happen before buildSshCommand is called, so the model flag ends up inside the remote agent invocation, not in the SSH wrapper:

// ✅ Inject model flag here, before SSH wrapping
//    ... (sanitize finalArgs)

if (config.sessionSshRemoteConfig?.enabled && ...) {
    const sshCommand = await buildSshCommand(...)
    command = sshCommand.command;
    finalArgs = sshCommand.args;
}

// ❌ Do NOT modify finalArgs here for SSH sessions

Comment on lines +376 to +421
// Remove any existing model tokens (either --model=... or -m/value)
const withoutModel: string[] = [];
for (let i = 0; i < finalArgs.length; i++) {
const a = finalArgs[i];
if (typeof a === 'string' && a.startsWith('--model')) {
// skip
continue;
}
if (a === '-m' && i + 1 < finalArgs.length) {
i++; // skip short form value
continue;
}
withoutModel.push(a);
}

// If we have a resolvedModelId (from session/agent/default), prefer inserting
// it explicitly as a CLI flag to avoid relying on OpenCode config/env.
if (resolvedModelId && typeof resolvedModelId === 'string') {
// If resolvedModelId doesn't look like provider/model, prefer agent.defaultModel
if (
!resolvedModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
resolvedModelId = (agent as any).defaultModel as string;
}

if (resolvedModelId && resolvedModelId.includes('/')) {
const modelToken = `--model=${resolvedModelId}`;
// Insert before the argument separator `--` if present
const sep = withoutModel.indexOf('--');
if (sep === -1) {
withoutModel.push(modelToken);
} else {
withoutModel.splice(sep, 0, modelToken);
}
// eslint-disable-next-line no-console
console.debug('[TabNaming] Injected canonical --model for spawn', {
sessionId,
model: resolvedModelId,
});
}
}

finalArgs = withoutModel;
Copy link

Choose a reason for hiding this comment

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

All model flags stripped but not re-injected when model lacks a provider prefix

The withoutModel loop unconditionally removes every --model=… and -m token from finalArgs (lines 376–389). The model is only re-injected at line 405 when resolvedModelId.includes('/'). If resolvedModelId was resolved from agent.defaultModel and that string has no / component (e.g. just a model short-name), neither the fallback at line 395 nor the injection at line 404 fires, and the spawn happens with no model argument at all — silently, because the outer try/catch swallows everything.

Trace for a model without a provider prefix:

  1. resolvedModelId = "my-model" after initial resolution
  2. Line 395: !resolvedModelId.includes('/') is true, but if agent.defaultModel also lacks /, the inner if is not entered and resolvedModelId is unchanged
  3. Line 404: resolvedModelId.includes('/') is false → no token injected
  4. finalArgs = withoutModel — all previous model flags stripped, nothing added back

The agent CLI then runs with whatever its built-in default model is, silently diverging from the configured value. The fix is to inject the model token regardless of whether it contains a /, or at minimum, avoid stripping model tokens that cannot be re-injected.

Comment on lines +134 to +152
let resolvedModelId: string | undefined;
if (
typeof (config as any).sessionCustomModel === 'string' &&
(config as any).sessionCustomModel.trim()
) {
resolvedModelId = (config as any).sessionCustomModel.trim();
} else if (
agentConfigValues &&
typeof agentConfigValues.model === 'string' &&
agentConfigValues.model.trim() &&
agentConfigValues.model.includes('/')
) {
resolvedModelId = agentConfigValues.model.trim();
} else if (
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string'
) {
resolvedModelId = (agent as any).defaultModel as string;
}
Copy link

Choose a reason for hiding this comment

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

sessionCustomModel branch is dead code — field is not in the handler config type

The IPC handler's config parameter (lines 83–92) is typed as:

config: {
  userMessage: string;
  agentType: string;
  cwd: string;
  sessionSshRemoteConfig?: { ... };
}

sessionCustomModel is not part of this type. Accessing it via (config as any).sessionCustomModel will always evaluate to undefined, so the first branch of the model-resolution chain (lines 135–139) is never entered. The PR description lists "session override" as the highest-priority model source, but it will never be applied until sessionCustomModel is added to both the config type and the IPC caller.

Suggested change
let resolvedModelId: string | undefined;
if (
typeof (config as any).sessionCustomModel === 'string' &&
(config as any).sessionCustomModel.trim()
) {
resolvedModelId = (config as any).sessionCustomModel.trim();
} else if (
agentConfigValues &&
typeof agentConfigValues.model === 'string' &&
agentConfigValues.model.trim() &&
agentConfigValues.model.includes('/')
) {
resolvedModelId = agentConfigValues.model.trim();
} else if (
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string'
) {
resolvedModelId = (agent as any).defaultModel as string;
}
let resolvedModelId: string | undefined;
if (
typeof (config as any).sessionCustomModel === 'string' &&
(config as any).sessionCustomModel.trim()
) {
resolvedModelId = (config as any).sessionCustomModel.trim();
} else if (

Consider adding sessionCustomModel?: string to the config interface and threading the value through from the caller, or removing this branch if it is not yet implemented.

Comment on lines +393 to +402
if (resolvedModelId && typeof resolvedModelId === 'string') {
// If resolvedModelId doesn't look like provider/model, prefer agent.defaultModel
if (
!resolvedModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
resolvedModelId = (agent as any).defaultModel as string;
}
Copy link

Choose a reason for hiding this comment

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

resolvedModelId mutation inside a swallowed try/catch causes misleading debug logs

resolvedModelId is mutated at line 401 inside the outer try/catch whose errors are silently swallowed (line 422–424). This variable is captured by reference in the onExit closure (line 464) and will reflect the post-mutation value when logged after process exit — even if the mutation happened in a code path that was not supposed to be the "winning" resolution.

More importantly, resolvedModelId was already used as sessionCustomModel in applyAgentConfigOverrides (line 184) with the original value. The late mutation only affects the final injection (line 405) and the debug logs, creating an inconsistency between what was passed to applyAgentConfigOverrides and what is ultimately injected/logged.

Consider using a separate variable (e.g. finalModelId) for this late fallback so the original resolvedModelId remains stable for logging purposes.

@chr1syy chr1syy marked this pull request as draft March 1, 2026 10:58
…, and type safety

- Move model token stripping/re-injection before SSH wrapping (fixes corrupted SSH args)
- Inject canonical model unconditionally via cliModelId (fixes silent drop for bare model names)
- Add sessionCustomModel to config type and thread it from renderer through preload to handler (fixes dead-code branch)
- Use separate cliModelId variable for late fallback, leaving resolvedModelId stable for logging
- Fix stream-json adjacency check (indexOf + [idx+1] instead of two includes)
- Add safeDebug helper to replace repetitive try/catch blocks around console.debug
- Add defaultModel?: string to AgentConfig interface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chr1syy chr1syy marked this pull request as ready for review March 1, 2026 18:29
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: 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 `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 490-496: The early check that returns false for lines starting
with a quote (the condition using trimmed.startsWith('"') ||
trimmed.startsWith("'")) is dropping valid single-line quoted tab names; remove
or disable that early-return so quoted single-line outputs like "\"Fix CI flaky
tests\"" can be handled by the later unquoting logic (the unquoted =
trimmed.replace(/^['"]+|['"]+$/g, '') and subsequent return). After removing the
early-return, add a sanity guard to still reject lines that become empty or only
quotes after unquoting (e.g., check unquoted.length > 0) so you don't
re-introduce false positives.
- Around line 245-251: The canonical model reinjection currently hardcodes
'--model=<value>' which breaks agents that expect other flag styles (e.g.,
'-m'). Update the injection logic around cliModelId/sanitized/filteredPrefix
(the Re-inject block and where filteredPrefix is used) to preserve the original
agent's model flag style: inspect the existing args/filteredPrefix for a model
flag pattern (e.g., '-m', '--model', '--model=') and choose the same flag
tokenization (short vs long and joined with '=' vs separate token) when pushing
the sanitized value; fall back to '--model=' only if no model flag form is
detectable. Keep safeDebug and trimming as-is.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f958e5b and c48e74a.

📒 Files selected for processing (5)
  • src/main/agents/definitions.ts
  • src/main/ipc/handlers/tabNaming.ts
  • src/main/preload/tabNaming.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/input/useInputProcessing.ts

Comment on lines +206 to +214
if (
cliModelId &&
!cliModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
cliModelId = (agent as any).defaultModel as string;
}
Copy link

Choose a reason for hiding this comment

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

Fallback logic violates stated model precedence order by overriding session models.

If resolvedModelId = "gpt-4" (from session without /), and agent.defaultModel = "openai/gpt-4" (with /), this code replaces the session model with the agent default, violating the documented precedence: session > agent config > agent default.

Suggested change
if (
cliModelId &&
!cliModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
cliModelId = (agent as any).defaultModel as string;
}
// If resolvedModelId has no provider prefix and came from agent.defaultModel,
// keep it as-is. Only models from session/config should reach here.
// The agent.defaultModel fallback was already tried during resolution.
let cliModelId: string | undefined = resolvedModelId;

Comment on lines +155 to +156
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string'
Copy link

Choose a reason for hiding this comment

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

Unnecessary type casting - defaultModel is already part of AgentConfig

defaultModel?: string was added to the AgentConfig interface in this PR (line 101 of definitions.ts). The as any cast bypasses type safety.

Suggested change
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string'
} else if (
agent.defaultModel &&
typeof agent.defaultModel === 'string'
) {
resolvedModelId = agent.defaultModel;

Comment on lines +259 to +261
} catch (err) {
// swallow
}
Copy link

Choose a reason for hiding this comment

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

Empty catch violates CLAUDE.md error handling guidelines

CLAUDE.md states: "DO let exceptions bubble up" and "silently swallowing errors hides bugs from Sentry". If model canonicalization fails, finalArgs remains unchanged with no indication of the failure, which could lead to incorrect model selection.

Comment on lines +492 to +495
if (trimmed.startsWith('"') || trimmed.startsWith("'")) return false;
// Allow quoted single-line outputs to be cleaned later, but lines that begin
// with quotes are typically examples and should be ignored.
const unquoted = trimmed.replace(/^['"]+|['"]+$/g, '');
Copy link

Choose a reason for hiding this comment

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

Logic contradiction - quoted lines filtered before unquoted is used

Line 492 returns false for lines starting with quotes, so the unquoted variable at line 495 is only computed for lines that don't start with quotes. The comment at line 493 says "Allow quoted single-line outputs to be cleaned later", but those lines are already filtered out at line 492.

…cedence, and quote filter

- Use agent.modelArgs() for model re-injection instead of hardcoded --model=
  (Codex uses -m, other agents may have different styles)
- Remove cliModelId fallback that incorrectly overrode session model with
  agent.defaultModel when resolvedModelId lacked a provider prefix — resolvedModelId
  already encodes the correct session > agent-config > agent-default precedence
- Remove (agent as any).defaultModel casts — defaultModel is typed on AgentConfig
- Remove try/catch swallowing model canonicalization errors (bubbles per CLAUDE.md)
- Fix extractTabName quote filter: fully-wrapped quoted strings like "Fix CI tests"
  are now kept for the unquoting step instead of being discarded early

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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: 1

🧹 Nitpick comments (2)
src/main/ipc/handlers/tabNaming.ts (2)

358-386: Consider using logger.warn instead of console.warn for consistency.

The rest of the file uses the logger utility for warnings (e.g., line 119, 332), but line 376 uses console.warn directly. This may bypass any configured log formatting or transport.

♻️ Proposed fix for consistency
 							if (genericRegex.test(String(output))) {
-								console.warn(
+								logger.warn(
 									'[TabNaming] Agent returned a generic tab name candidate; consider adjusting prompt or model',
+									LOG_CONTEXT,
 									{
 										sessionId,
 										detected: String(output).trim().slice(0, 80),
 									}
 								);
 							}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 358 - 386, Replace the
direct console.warn call inside the TabNaming logging try-block with the
module's logger.warn to keep logging consistent; specifically, in the block that
checks genericRegex.test(String(output)) (inside the function handling tab
naming), call logger.warn with the same descriptive message and the same
metadata object ({ sessionId, detected: String(output).trim().slice(0, 80) }) so
formatting/transports are preserved—ensure logger is the same logger used
elsewhere in this file.

404-416: The outer try-catch is redundant since safeDebug already handles errors.

The safeDebug helper (lines 32-37) already wraps console.debug in a try-catch, so the additional try-catch here provides no extra safety.

♻️ Proposed simplification
 					// Spawn the process
 					// When using SSH with stdin, pass the flag so ChildProcessSpawner
 					// sends the prompt via stdin instead of command line args
-					try {
-						// Debug: log full finalArgs array and types just before spawn
-						// (kept in console.debug for diagnosis only)
-						safeDebug('[TabNaming] About to spawn with final args', {
-							sessionId,
-							command,
-							cwd,
-							sendPromptViaStdin: shouldSendPromptViaStdin,
-							finalArgsDetail: finalArgs.map((a) => ({ value: a, type: typeof a })),
-						});
-					} catch (err) {
-						// ignore logging failures
-					}
+					// Debug: log full finalArgs array and types just before spawn
+					// (kept in console.debug for diagnosis only)
+					safeDebug('[TabNaming] About to spawn with final args', {
+						sessionId,
+						command,
+						cwd,
+						sendPromptViaStdin: shouldSendPromptViaStdin,
+						finalArgsDetail: finalArgs.map((a) => ({ value: a, type: typeof a })),
+					});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 404 - 416, The outer
try-catch surrounding the diagnostic call is redundant because safeDebug already
catches errors; remove the wrapping try { ... } catch (err) { } and directly
call safeDebug(...) where the block currently is (the call that logs sessionId,
command, cwd, shouldSendPromptViaStdin and finalArgsDetail), leaving safeDebug,
finalArgs, sessionId, command and cwd references intact so the logging remains
but without the extra try-catch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 309-321: The current try-catch wraps both the
computation/filtering of nonStringItems and the call to safeDebug, which can
swallow errors from the filtering and leave finalArgs unfiltered; compute
nonStringItems and perform finalArgs = finalArgs.filter(...) outside any
try-catch so any unexpected errors propagate, then wrap only the safeDebug
logging call in try-catch to swallow logging errors; reference finalArgs and
safeDebug in your change and ensure the removal logic (nonStringItems.map and
finalArgs.filter) runs before any catch block.

---

Nitpick comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 358-386: Replace the direct console.warn call inside the TabNaming
logging try-block with the module's logger.warn to keep logging consistent;
specifically, in the block that checks genericRegex.test(String(output)) (inside
the function handling tab naming), call logger.warn with the same descriptive
message and the same metadata object ({ sessionId, detected:
String(output).trim().slice(0, 80) }) so formatting/transports are
preserved—ensure logger is the same logger used elsewhere in this file.
- Around line 404-416: The outer try-catch surrounding the diagnostic call is
redundant because safeDebug already catches errors; remove the wrapping try {
... } catch (err) { } and directly call safeDebug(...) where the block currently
is (the call that logs sessionId, command, cwd, shouldSendPromptViaStdin and
finalArgsDetail), leaving safeDebug, finalArgs, sessionId, command and cwd
references intact so the logging remains but without the extra try-catch.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c48e74a and b71fedb.

📒 Files selected for processing (1)
  • src/main/ipc/handlers/tabNaming.ts

…dundant try-catch

- Compute nonStringItems and filter finalArgs outside any try-catch so
  unexpected filter errors propagate; only wrap the safeDebug log call
- Replace console.warn with logger.warn (with LOG_CONTEXT) for consistent
  logging transports when a generic tab name candidate is detected
- Remove redundant try-catch around safeDebug pre-spawn diagnostic call
  since safeDebug already handles its own errors internally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 (1)
src/main/ipc/handlers/tabNaming.ts (1)

172-185: Model arguments may be added twice before canonicalization.

buildAgentArgs at line 177 adds model args via modelId: resolvedModelId, then applyAgentConfigOverrides at line 183 also receives sessionCustomModel: resolvedModelId which adds model args again through configOptions. While the canonicalization at lines 207-244 deduplicates these, this creates unnecessary work and could cause subtle bugs if canonicalization logic changes.

Consider passing modelId: undefined to buildAgentArgs since applyAgentConfigOverrides handles model injection, or vice versa.

♻️ Option A: Skip model in buildAgentArgs
 let finalArgs = buildAgentArgs(agent, {
 	baseArgs,
 	prompt: fullPrompt,
 	cwd: config.cwd,
 	readOnlyMode: true, // Always read-only since we're not modifying anything
-	modelId: resolvedModelId,
+	modelId: undefined, // Model handled by applyAgentConfigOverrides
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 172 - 185, The code is
adding model args twice: buildAgentArgs(...) is called with modelId:
resolvedModelId and then applyAgentConfigOverrides(..., { sessionCustomModel:
resolvedModelId }) injects model args again; change the buildAgentArgs
invocation in this block to omit the model (e.g., pass modelId: undefined or
remove the modelId property) so that applyAgentConfigOverrides is the single
source of model injection and then keep finalArgs = configResolution.args as-is
(referencing buildAgentArgs, applyAgentConfigOverrides, resolvedModelId,
finalArgs, and configResolution.args).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 172-185: The code is adding model args twice: buildAgentArgs(...)
is called with modelId: resolvedModelId and then applyAgentConfigOverrides(...,
{ sessionCustomModel: resolvedModelId }) injects model args again; change the
buildAgentArgs invocation in this block to omit the model (e.g., pass modelId:
undefined or remove the modelId property) so that applyAgentConfigOverrides is
the single source of model injection and then keep finalArgs =
configResolution.args as-is (referencing buildAgentArgs,
applyAgentConfigOverrides, resolvedModelId, finalArgs, and
configResolution.args).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b71fedb and f756526.

📒 Files selected for processing (1)
  • src/main/ipc/handlers/tabNaming.ts

@chr1syy chr1syy marked this pull request as draft March 2, 2026 16:00
Replace buildSshCommand with buildSshCommandWithStdin in tabNaming.ts so
the prompt is passed via a bash stdin script rather than embedded in shell
args. This fixes tab naming on SSH remotes for all agents regardless of
supportsStreamJsonInput capability.

Also fix ChildProcessSpawner to include sshStdinScript in the promptViaStdin
check, preventing the prompt from being appended as a CLI arg to the SSH
command (which caused bash to interpret the tab naming prompt as a shell
script). Remove duplicate model injection from buildAgentArgs call, making
applyAgentConfigOverrides the single source of model flag injection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chr1syy chr1syy marked this pull request as ready for review March 2, 2026 16:49
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/__tests__/main/ipc/handlers/tabNaming.test.ts (1)

482-512: ⚠️ Potential issue | 🟠 Major

This test now contradicts extractTabName behavior for wrapped quoted outputs.

"Quoted Tab Name" is now a valid candidate (kept, then unquoted), so expecting null preserves old behavior and can fail against current implementation.

Suggested test update
-		it('filters out lines starting with quotes (example inputs)', async () => {
+		it('accepts fully wrapped quoted tab names', async () => {
@@
-			// Simulate output with quotes - lines starting with " are filtered
+			// Fully wrapped quoted output should be unquoted and accepted
 			onDataCallback?.('tab-naming-mock-uuid-1234', '"Quoted Tab Name"');
@@
-			// Lines starting with quotes are filtered out as example inputs
-			expect(result).toBeNull();
+			expect(result).toBe('Quoted Tab Name');
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/ipc/handlers/tabNaming.test.ts` around lines 482 - 512,
The test "filters out lines starting with quotes (example inputs)" is now
inconsistent with extractTabName which accepts wrapped quoted outputs (it will
unquote and return "Quoted Tab Name"); update the test to assert the handler
returns the unquoted string instead of null: locate the test using
invokeHandler('tabNaming:generateTabName', ...) and the mocked callbacks, feed
onDataCallback with '"Quoted Tab Name"' and change the final expectation to
expect(result).toBe('Quoted Tab Name') (or adjust the input to a truly ignored
example like '" > Example"' if you want to preserve a null expectation).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 339-350: The debug call to safeDebug in tabNaming (and the similar
block around lines 385-391) is logging potentially sensitive data (fullPrompt,
output, finalArgs) directly; change these to non-sensitive metadata instead by
removing rawPrompt/rawOutput/finalArgs contents and replacing them with safe
fields such as promptLength (String(fullPrompt)?.length), outputLength
(String(output)?.length), and a redactedPreview or hashPreview (e.g., a
fixed-length masked string or a short checksum) for finalArgs and fullPrompt,
while keeping sessionId, config.agentType, agentConfigValues?.model and
resolvedModelId; ensure no raw prompt/output/arg strings are logged by updating
the safeDebug call sites that reference fullPrompt, output, and finalArgs
accordingly.
- Around line 181-194: The debug field configResolution.modelSource is
misleading because we pass resolvedModelId as sessionCustomModel into
applyAgentConfigOverrides; stop passing resolvedModelId into
applyAgentConfigOverrides so modelSource reflects the true origin. Specifically,
call applyAgentConfigOverrides(agent, finalArgs, { agentConfigValues }) (omit
sessionCustomModel), then after receiving configResolution set finalArgs =
configResolution.args and, if resolvedModelId is present, apply it to finalArgs
explicitly (or annotate a new debug field like resolvedModelId) before calling
safeDebug so safeDebug logs both configResolution.modelSource and the actual
resolvedModelId/where it came from; refer to applyAgentConfigOverrides,
resolvedModelId, configResolution.modelSource, finalArgs, and safeDebug to
locate and change the code.

---

Outside diff comments:
In `@src/__tests__/main/ipc/handlers/tabNaming.test.ts`:
- Around line 482-512: The test "filters out lines starting with quotes (example
inputs)" is now inconsistent with extractTabName which accepts wrapped quoted
outputs (it will unquote and return "Quoted Tab Name"); update the test to
assert the handler returns the unquoted string instead of null: locate the test
using invokeHandler('tabNaming:generateTabName', ...) and the mocked callbacks,
feed onDataCallback with '"Quoted Tab Name"' and change the final expectation to
expect(result).toBe('Quoted Tab Name') (or adjust the input to a truly ignored
example like '" > Example"' if you want to preserve a null expectation).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f756526 and 33b2586.

📒 Files selected for processing (3)
  • src/__tests__/main/ipc/handlers/tabNaming.test.ts
  • src/main/ipc/handlers/tabNaming.ts
  • src/main/process-manager/spawners/ChildProcessSpawner.ts

@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Additional Comments (1)

src/__tests__/main/ipc/handlers/tabNaming.test.ts, line 512
Test now contradicts implementation - fully wrapped quoted strings are allowed

The implementation change at tabNaming.ts:451-452 specifically allows fully wrapped quoted strings (e.g. "Quoted Tab Name") and unquotes them, but this test still expects them to be filtered out. With the new logic, "Quoted Tab Name" should pass through and become Quoted Tab Name.

		// Tab name with quotes gets unquoted
		onDataCallback?.('tab-naming-mock-uuid-1234', '"Quoted Tab Name"');
		onExitCallback?.('tab-naming-mock-uuid-1234');

		const result = await resultPromise;
		expect(result).toBe('Quoted Tab Name');

…elSource

- Replace raw prompt/output/args content in safeDebug calls with safe
  metadata (promptLength, outputLength, finalArgsCount) to avoid
  logging sensitive data
- Remove sessionCustomModel from applyAgentConfigOverrides call so
  modelSource accurately reflects the true origin (agent-config or
  default) rather than always reporting 'session'; log resolvedModelId
  as a separate debug field instead
- Fix test: 'filters out lines starting with quotes' was asserting null
  but extractTabName accepts fully-wrapped quoted strings and unquotes
  them; update expectation to 'Quoted Tab Name'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chr1syy chr1syy marked this pull request as draft March 2, 2026 19:52
…ning sensitive log fields

- Remove inner try-catch wrapping safeDebug in the non-string args
  filter block (CLAUDE.md: don't swallow errors); safeDebug already
  has its own internal guard
- Replace preview: String(i) in the removed-args log with removedCount
  to avoid logging raw argument values
- Replace detected: output.slice(0,80) in the generic-output warn with
  outputLength to avoid logging raw agent output
- Replace tokens: modelArgTokens in the model-injection log with
  tokenCount: modelArgTokens.length (model name is not logged)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chr1syy chr1syy marked this pull request as ready for review March 2, 2026 20:10
@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Additional Comments (3)

src/main/ipc/handlers/tabNaming.ts, line 183
Prompt double-injected in SSH mode

buildAgentArgs at line 172 adds fullPrompt directly to finalArgs (via the prompt field). Those args are then passed verbatim to buildSshCommandWithStdin at line 275 (args: finalArgs). Inside buildSshCommandWithStdin, the full remoteOptions.args array — including the prompt token — is shell-escaped and embedded into the remote command string (line 422 of ssh-command-builder.ts). At the same time, stdinInput: fullPrompt (line 279) appends the raw prompt to the script after the exec line (lines 451-454 of ssh-command-builder.ts).

The remote agent therefore receives the tab-naming prompt twice: once as a shell-escaped CLI argument in the script and once as raw stdin content. This can cause:

  • The agent to process duplicate context (inflating the prompt and wasting tokens)
  • Unexpected behavior for agents that treat CLI args and stdin differently

The fix is to call buildAgentArgs without the prompt field when SSH is configured, or alternatively strip the prompt token from finalArgs before handing it off to buildSshCommandWithStdin:

// Build args WITHOUT embedding the prompt when SSH stdin passthrough will be used
let finalArgs = buildAgentArgs(agent, {
    baseArgs,
    // Only add prompt to CLI args for non-SSH paths; SSH will inject via stdinInput
    prompt: config.sessionSshRemoteConfig?.enabled ? undefined : fullPrompt,
    cwd: config.cwd,
    readOnlyMode: true,
});

src/main/ipc/handlers/tabNaming.ts, line 186
Comment inaccurate — model is still injected by applyAgentConfigOverrides

The comment claims this call applies "customArgs/env only" and explicitly avoids model injection. However, agentConfigValues is passed in full — including any model field it contains — and applyAgentConfigOverrides will inject a --model flag from it when present.

The net result is correct (the canonicalization block on the lines that follow strips every --model token and re-injects resolvedModelId), but the comment creates a false expectation. A future reader who trusts the comment might remove the canonicalization block thinking it is redundant, breaking model resolution.

Either update the comment to acknowledge that the model injection is stripped immediately afterwards, or filter model from the config values before passing:

const configResolution = applyAgentConfigOverrides(agent, finalArgs, {
    // Pass a copy without 'model' — canonicalization below is the single source of
    // model injection, so we don't want applyAgentConfigOverrides to race with it.
    agentConfigValues: { ...agentConfigValues, model: undefined },
});

src/main/ipc/handlers/tabNaming.ts, line 225
Lone -m at end of prefix silently survives the strip

The guard for the short form reads:

if (a === '-m' && i + 1 < prefix.length) {
    i++; // drop short form + value
    continue;
}

When -m is the very last token in prefix (i.e. i + 1 === prefix.length), the condition is false, so the bare -m token is pushed into filteredPrefix without its value. This leaves a dangling flag that will likely cause the spawned agent process to exit with an argument-parsing error — silently, because the outer try/catch swallows it.

The safe fix is to drop the trailing -m unconditionally:

if (a === '-m') {
    if (i + 1 < prefix.length) i++; // drop value token when present
    continue; // always drop the flag itself
}

@chr1syy
Copy link
Contributor Author

chr1syy commented Mar 3, 2026

@coderabbitai re-review please

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

@chr1syy: I'll re-review the PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 1

🧹 Nitpick comments (1)
src/main/ipc/handlers/tabNaming.ts (1)

230-232: Redundant sanitization of resolvedModelId.

The trailing-slash removal and trim are already performed at lines 159-162. This second sanitization is unnecessary.

♻️ Suggested simplification
 				if (resolvedModelId) {
-					const sanitized = resolvedModelId.replace(/\/+$/, '').trim();
-					if (sanitized) {
+					// resolvedModelId is already sanitized at resolution time
 						const modelArgTokens = agent.modelArgs
-							? agent.modelArgs(sanitized)
-							: [`--model=${sanitized}`];
+							? agent.modelArgs(resolvedModelId)
+							: [`--model=${resolvedModelId}`];
 						filteredPrefix.push(...modelArgTokens);
 						safeDebug('[TabNaming] Injected canonical model flag for spawn', {
 							sessionId,
-							modelLength: sanitized.length,
+							modelLength: resolvedModelId.length,
 							tokenCount: modelArgTokens.length,
 						});
-					}
 				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 230 - 232, The second
sanitization is redundant: remove the inner replace/trim and the temporary
sanitized variable and just use the already-sanitized resolvedModelId (or rely
on the earlier sanitization where resolvedModelId is assigned) in the
conditional and subsequent logic; update the if check that currently reads if
(resolvedModelId) { const sanitized = resolvedModelId.replace(/\/+$/,
'').trim(); if (sanitized) { ... } } to simply if (resolvedModelId) { ... } (or
rename to a clear variable if needed) and ensure all uses inside reference
resolvedModelId instead of sanitized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 209-225: The loop that strips model flags in tabNaming.ts can
wrongly consume the next token when encountering '--model' or '-m'; update the
logic inside the for loop that inspects prefix[i] (and handles '--model' and
'-m') to only advance i and drop the next element if that next element exists,
is a string, and does not start with '-' (i.e., looks like a value); otherwise
treat the flag as not having a value and only drop the flag itself (push no
increment), ensuring filteredPrefix and prefix handling remains unchanged.

---

Nitpick comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 230-232: The second sanitization is redundant: remove the inner
replace/trim and the temporary sanitized variable and just use the
already-sanitized resolvedModelId (or rely on the earlier sanitization where
resolvedModelId is assigned) in the conditional and subsequent logic; update the
if check that currently reads if (resolvedModelId) { const sanitized =
resolvedModelId.replace(/\/+$/, '').trim(); if (sanitized) { ... } } to simply
if (resolvedModelId) { ... } (or rename to a clear variable if needed) and
ensure all uses inside reference resolvedModelId instead of sanitized.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33b2586 and 1a3f3cb.

📒 Files selected for processing (2)
  • src/__tests__/main/ipc/handlers/tabNaming.test.ts
  • src/main/ipc/handlers/tabNaming.ts

Comment on lines +209 to +225
for (let i = 0; i < prefix.length; i++) {
const a = prefix[i];
if (typeof a === 'string') {
if (a.startsWith('--model=')) {
continue; // drop explicit --model=value
}
if (a === '--model') {
i++; // drop flag + value
continue;
}
if (a === '-m' && i + 1 < prefix.length) {
i++; // drop short form + value
continue;
}
}
filteredPrefix.push(a);
}
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

Model flag stripping may incorrectly consume a following argument if --model appears without a value.

If finalArgs contains a malformed sequence like ['--model', '--verbose', ...], the i++ at line 216 will skip --verbose, treating it as the model value.

Consider adding a guard to ensure the next element looks like a model value (not another flag):

🛡️ Proposed defensive check
 						if (a === '--model') {
+							// Only consume next arg if it doesn't look like another flag
+							if (i + 1 < prefix.length && !String(prefix[i + 1]).startsWith('-')) {
+								i++; // drop flag + value
+							}
-							i++; // drop flag + value
 							continue;
 						}
 						if (a === '-m' && i + 1 < prefix.length) {
+							// Only consume next arg if it doesn't look like another flag
+							if (!String(prefix[i + 1]).startsWith('-')) {
+								i++; // drop short form + value
+							}
-							i++; // drop short form + value
 							continue;
 						}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let i = 0; i < prefix.length; i++) {
const a = prefix[i];
if (typeof a === 'string') {
if (a.startsWith('--model=')) {
continue; // drop explicit --model=value
}
if (a === '--model') {
i++; // drop flag + value
continue;
}
if (a === '-m' && i + 1 < prefix.length) {
i++; // drop short form + value
continue;
}
}
filteredPrefix.push(a);
}
for (let i = 0; i < prefix.length; i++) {
const a = prefix[i];
if (typeof a === 'string') {
if (a.startsWith('--model=')) {
continue; // drop explicit --model=value
}
if (a === '--model') {
// Only consume next arg if it doesn't look like another flag
if (i + 1 < prefix.length && !String(prefix[i + 1]).startsWith('-')) {
i++; // drop flag + value
}
continue;
}
if (a === '-m' && i + 1 < prefix.length) {
// Only consume next arg if it doesn't look like another flag
if (!String(prefix[i + 1]).startsWith('-')) {
i++; // drop short form + value
}
continue;
}
}
filteredPrefix.push(a);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 209 - 225, The loop that
strips model flags in tabNaming.ts can wrongly consume the next token when
encountering '--model' or '-m'; update the logic inside the for loop that
inspects prefix[i] (and handles '--model' and '-m') to only advance i and drop
the next element if that next element exists, is a string, and does not start
with '-' (i.e., looks like a value); otherwise treat the flag as not having a
value and only drop the flag itself (push no increment), ensuring filteredPrefix
and prefix handling remains unchanged.

@chr1syy chr1syy marked this pull request as draft March 3, 2026 19:56
…e redundant sanitization

- --model and -m now only consume the next token as a value if it exists,
  is a string, and does not start with '-'; prevents wrongly skipping an
  adjacent flag when no value follows
- Remove inner sanitized variable and redundant .replace/trim around
  resolvedModelId — it is already sanitized at assignment; use it directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant