Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,6 @@ Canonical triage label vocabulary (`needs-triage`, `needs-info`, `ready-for-agen
### Domain docs

Single-context layout — `CONTEXT.md` (domain glossary) + `docs/adr/` (decision log) at the repo root. Read `CONTEXT.md` before naming concepts and `docs/adr/` before working in an area; both are extended by `grill-with-docs`. See `docs/agents/domain.md`.

<!-- test line: added 2026-06-18 -->

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"scripts": {
"audit": "bun audit --audit-level high",
"check:dead-code": "bun scripts/dead-code/find-dead-code.ts --strict",
"dead-code:apply": "bun scripts/dead-code/apply-dead-code.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Misleading name: dead-code:apply does a dry-run, not an apply. Rename to signal dry-run behavior (e.g. dead-code:dry-run) and reserve dead-code:apply for the actual deletion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.json, line 17:

<comment>Misleading name: `dead-code:apply` does a dry-run, not an apply. Rename to signal dry-run behavior (e.g. `dead-code:dry-run`) and reserve `dead-code:apply` for the actual deletion.</comment>

<file context>
@@ -14,6 +14,8 @@
 	"scripts": {
 		"audit": "bun audit --audit-level high",
 		"check:dead-code": "bun scripts/dead-code/find-dead-code.ts --strict",
+		"dead-code:apply": "bun scripts/dead-code/apply-dead-code.ts",
+		"dead-code:apply:force": "bun scripts/dead-code/apply-dead-code.ts --apply",
 		"test": "bun scripts/forbid-structural-tests.ts packages && bun run --cwd packages/ui test && bun run --cwd packages/desktop test && bun run --cwd packages/website test"
</file context>

"dead-code:apply:force": "bun scripts/dead-code/apply-dead-code.ts --apply",
"test": "bun scripts/forbid-structural-tests.ts packages && bun run --cwd packages/ui test && bun run --cwd packages/desktop test && bun run --cwd packages/website test"
},
"engines": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ impl ClaudeCcSdkClient {
return;
};
if available_model_ids.is_empty() {
self.pending_model_id = None;
return;
}
if !available_model_ids
Expand Down
10 changes: 10 additions & 0 deletions packages/desktop/src-tauri/src/acp/client/cc_sdk_client/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,16 @@ fn bind_pending_creation_attempt_seeds_model_and_mode() {
assert_eq!(client.pending_mode_id.as_deref(), Some("plan"));
}

#[test]
fn sanitize_pending_model_for_connect_clears_model_when_catalog_is_empty() {
let mut client = make_test_client();
client.pending_model_id = Some("stale-model".to_string());

client.sanitize_pending_model_for_connect(&[]);

assert!(client.pending_model_id.is_none());
}

#[test]
fn sanitize_pending_model_for_connect_clears_invalid_model() {
let mut client = make_test_client();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,12 @@ pub async fn acp_new_session(
true,
)
})?;
let pending_model_id = attempt.as_ref().and_then(|row| row.model_id.clone());
let pending_mode_id = attempt.as_ref().and_then(|row| row.mode_id.clone());
let attempt = require_creation_attempt_for_deferred_bind(
&creation_attempt_id,
attempt,
)?;
let pending_model_id = attempt.model_id.clone();
let pending_mode_id = attempt.mode_id.clone();
client.bind_pending_creation_attempt(
Some(creation_attempt_id.clone()),
pending_model_id,
Expand Down Expand Up @@ -414,3 +418,66 @@ pub async fn acp_new_session(
}
.await)
}

fn require_creation_attempt_for_deferred_bind(
attempt_id: &str,
attempt: Option<crate::db::repository::CreationAttemptRow>,
) -> Result<crate::db::repository::CreationAttemptRow, SerializableAcpError> {
attempt.ok_or_else(|| {
creation_failure(
CreationFailureKind::MetadataCommitFailed,
format!("Creation attempt {attempt_id} missing for deferred bind"),
None,
Some(attempt_id.to_string()),
true,
)
})
}

#[cfg(test)]
mod deferred_bind_tests {
use super::require_creation_attempt_for_deferred_bind;
use crate::db::repository::{CreationAttemptRow, CreationAttemptStatus};
use chrono::Utc;

fn sample_attempt(id: &str) -> CreationAttemptRow {
let now = Utc::now();
CreationAttemptRow {
id: id.to_string(),
project_path: "/project".to_string(),
agent_id: "claude-code".to_string(),
worktree_path: None,
launch_token: None,
status: CreationAttemptStatus::Pending.as_str().to_string(),
failure_reason: None,
provider_session_id: None,
sequence_id: Some(1),
model_id: Some("claude-sonnet-4-6".to_string()),
mode_id: None,
created_at: now,
updated_at: now,
}
}

#[test]
fn require_creation_attempt_for_deferred_bind_returns_row_when_present() {
let attempt = sample_attempt("attempt-1");
let loaded = require_creation_attempt_for_deferred_bind("attempt-1", Some(attempt.clone()))
.expect("attempt should load");

assert_eq!(loaded.id, attempt.id);
assert_eq!(loaded.model_id, attempt.model_id);
}

#[test]
fn require_creation_attempt_for_deferred_bind_fails_when_row_is_missing() {
let error = require_creation_attempt_for_deferred_bind("attempt-1", None)
.expect_err("missing attempt should fail closed");

assert!(
error
.to_string()
.contains("Creation attempt attempt-1 missing for deferred bind")
);
}
}
38 changes: 38 additions & 0 deletions packages/desktop/src-tauri/src/db/repository_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,44 @@ mod session_metadata_tests {
assert_eq!(promoted.sequence_id, Some(1));
}

#[tokio::test]
async fn promoting_creation_attempt_allocates_sequence_for_legacy_null_attempt_sequence() {
use crate::db::entities::creation_attempt;
use crate::db::entities::prelude::CreationAttempt;
use sea_orm::{ActiveModelTrait, EntityTrait, Set};

let db = setup_test_db().await;
let attempt = SessionMetadataRepository::create_creation_attempt(
&db,
"/project",
"claude-code",
None,
None,
None,
)
.await
.unwrap();

let mut active: creation_attempt::ActiveModel = CreationAttempt::find_by_id(&attempt.id)
.one(&db)
.await
.unwrap()
.expect("attempt row should exist")
.into();
active.sequence_id = Set(None);
active.update(&db).await.unwrap();

let promoted = SessionMetadataRepository::promote_creation_attempt(
&db,
&attempt.id,
"provider-canonical-id",
)
.await
.unwrap();

assert_eq!(promoted.sequence_id, Some(1));
}

#[tokio::test]
async fn promoting_creation_attempt_allocates_sequence_inside_canonical_session_transaction() {
let db = setup_test_db().await;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function resolveResolvableToolbarModelId(input: {
readonly provisionalModelId: string | null;
readonly resolvedToolbarModelId: string | null;
}): string | null {
if (input.provisionalModelId) {
return input.provisionalModelId;
}

return input.resolvedToolbarModelId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";

import { resolveResolvableToolbarModelId } from "./resolve-resolvable-toolbar-model-id.js";

describe("resolveResolvableToolbarModelId", () => {
it("keeps the explicit provisional pick before capabilities hydrate during connecting", () => {
expect(
resolveResolvableToolbarModelId({
provisionalModelId: "claude-sonnet-4-6",
resolvedToolbarModelId: null,
})
).toBe("claude-sonnet-4-6");
});

it("falls back to the resolved toolbar model when no provisional pick exists", () => {
expect(
resolveResolvableToolbarModelId({
provisionalModelId: null,
resolvedToolbarModelId: "claude-opus-4-6",
})
).toBe("claude-opus-4-6");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
resolveToolbarModeId,
resolveToolbarModelId,
} from "./toolbar-state.js";
import { resolveResolvableToolbarModelId } from "./resolve-resolvable-toolbar-model-id.js";

describe("toolbar-state", () => {
const visibleModes = [
Expand Down Expand Up @@ -104,6 +105,15 @@ describe("toolbar-state", () => {
).toBe("claude-opus");
});

it("captures an explicit sonnet pick instead of the default opus displayed id", () => {
expect(
resolveInitialModelIdForNewSession({
sessionId: null,
displayedModelId: "claude-sonnet-4-6",
})
).toBe("claude-sonnet-4-6");
});

it("does not send an initial model for an existing session", () => {
expect(
resolveInitialModelIdForNewSession({
Expand All @@ -114,6 +124,17 @@ describe("toolbar-state", () => {
});
});

describe("resolveResolvableToolbarModelId", () => {
it("prefers the explicit provisional sonnet pick over the default opus toolbar id", () => {
expect(
resolveResolvableToolbarModelId({
provisionalModelId: "claude-sonnet-4-6",
resolvedToolbarModelId: "claude-opus-4-6",
})
).toBe("claude-sonnet-4-6");
});
});

describe("resolvePendingToolbarSelections", () => {
it("returns pending mode and model applications for valid provisional selections", () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
shouldShowActiveModeChip,
shouldShowSlashCommandDropdown,
} from "../composer-controller.js";
import { resolveResolvableToolbarModelId } from "../logic/resolve-resolvable-toolbar-model-id.js";
import type { AgentInputState } from "./agent-input-state.svelte.js";
import type { AgentInputProps } from "../types/agent-input-props.js";

Expand Down Expand Up @@ -325,6 +326,13 @@ export class ComposerViewController {
})
);

readonly resolvableToolbarModelId = $derived.by(() =>
resolveResolvableToolbarModelId({
provisionalModelId: this.effectiveComposerProvisionalModelId,
resolvedToolbarModelId: this.effectiveCurrentModelId,
})
);

readonly toolbarConfigOptions = $derived.by((): AgentInputConfigOption[] => {
if (this.sessionConfigOptions.length === 0) {
return [];
Expand Down Expand Up @@ -542,7 +550,7 @@ export class ComposerViewController {
this.effectiveCapabilityProviderMetadata?.preconnectionCapabilityMode ??
"unsupported",
}),
resolvableModelId: this.effectiveCurrentModelId,
resolvableModelId: this.resolvableToolbarModelId,
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,6 @@ vi.mock("../../messages/user-message.svelte", async () => ({
default: (await import("./fixtures/user-message-stub.svelte")).default,
}));

vi.mock("../../messages/assistant-message.svelte", async () => ({
default: (await import("./fixtures/user-message-stub.svelte")).default,
}));

vi.mock("../../project-selection-panel.svelte", async () => ({
default: (await import("./fixtures/user-message-stub.svelte")).default,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { AgentPanelRootState } from "../state/agent-panel-root-state.svelte.js";
import { shouldAutoScrollOnPanelActivation } from "../logic/should-auto-scroll-on-panel-activation.js";
import { isInteractiveClickTarget } from "../logic/panel-focus-guard.js";
import { deriveAgentPanelHeaderDisplayTitle } from "../logic/agent-panel-header-title.js";
import { resolveAgentPanelHeaderSequenceId } from "../logic/agent-panel-header-sequence-id.js";
import { resolveAgentPanelProviderBrand } from "../logic/agent-panel-provider-brand.js";
import { resolveWorktreeToggleProjectPath } from "../logic/worktree-toggle-project-path.js";
import { buildTodoMarkdown } from "./agent-panel-pure-helpers.js";
Expand Down Expand Up @@ -566,7 +567,18 @@ const displayProjectName = $derived.by(() => {
return effectiveProjectName ?? "Project";
});

const sequenceId = $derived(sessionController.sessionMetadata ? (sessionController.sessionMetadata.sequenceId ?? null) : null);
const sequenceId = $derived.by(() => {
const id = sessionId;
if (id === null) {
return null;
}
const pendingCreation = sessionStore.getPendingCreationSession(id);
return resolveAgentPanelHeaderSequenceId({
sessionMetadataSequenceId: sessionController.sessionMetadata?.sequenceId,
pendingCreationSequenceId: pendingCreation?.sequenceId ?? null,
hasPendingCreationSession: sessionStore.hasPendingCreationSession(id),
});
Comment on lines +575 to +580

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 getPendingCreationSession(id) already returns null when there is no pending creation, so calling hasPendingCreationSession(id) separately is redundant — pendingCreation !== null is the exact same check. The two calls also go through separate delegation chains, making them subtly susceptible to diverging if the underlying map is mutated between them.

Suggested change
const pendingCreation = sessionStore.getPendingCreationSession(id);
return resolveAgentPanelHeaderSequenceId({
sessionMetadataSequenceId: sessionController.sessionMetadata?.sequenceId,
pendingCreationSequenceId: pendingCreation?.sequenceId ?? null,
hasPendingCreationSession: sessionStore.hasPendingCreationSession(id),
});
const pendingCreation = sessionStore.getPendingCreationSession(id);
return resolveAgentPanelHeaderSequenceId({
sessionMetadataSequenceId: sessionController.sessionMetadata?.sequenceId,
pendingCreationSequenceId: pendingCreation?.sequenceId ?? null,
hasPendingCreationSession: pendingCreation !== null,
});

Fix in Claude Code Fix in Cursor Fix in Codex

});

const displayTitle = $derived.by(() => {
return deriveAgentPanelHeaderDisplayTitle({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";

import { resolveAgentPanelHeaderSequenceId } from "../agent-panel-header-sequence-id.js";

describe("resolveAgentPanelHeaderSequenceId", () => {
it("prefers session metadata when sequence id is already materialized", () => {
expect(
resolveAgentPanelHeaderSequenceId({
sessionMetadataSequenceId: 4,
pendingCreationSequenceId: 7,
hasPendingCreationSession: true,
})
).toBe(4);
});

it("projects pending creation sequence id while metadata is absent during first-send connecting", () => {
expect(
resolveAgentPanelHeaderSequenceId({
sessionMetadataSequenceId: undefined,
pendingCreationSequenceId: 7,
hasPendingCreationSession: true,
})
).toBe(7);
});

it("does not project pending sequence id after pending creation completes", () => {
expect(
resolveAgentPanelHeaderSequenceId({
sessionMetadataSequenceId: undefined,
pendingCreationSequenceId: 7,
hasPendingCreationSession: false,
})
).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Resolves the sequence id shown in agent panel header chrome.
*
* Today only session metadata is consulted — pending creation identity is ignored.
*/
Comment thread
greptile-apps[bot] marked this conversation as resolved.
export function resolveAgentPanelHeaderSequenceId(input: {
readonly sessionMetadataSequenceId: number | null | undefined;
readonly pendingCreationSequenceId: number | null | undefined;
readonly hasPendingCreationSession: boolean;
}): number | null {
if (input.sessionMetadataSequenceId != null) {
return input.sessionMetadataSequenceId;
}

if (
input.hasPendingCreationSession &&
input.pendingCreationSequenceId != null
) {
return input.pendingCreationSequenceId;
}

return null;
}
Loading
Loading