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
27 changes: 13 additions & 14 deletions crates/agent/src/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::{
TurnStatus, UserInputOption, UserInputQuestion, selection_bool, selection_str,
};

/// T3's exact message denied to `ExitPlanMode` once the plan is captured.
/// Denial returned to `ExitPlanMode` after the client captures the plan.
const EXIT_PLAN_DENY_MESSAGE: &str = "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.";

/// First Claude Code build whose headless control protocol is verified to
Expand Down Expand Up @@ -403,8 +403,8 @@ fn mcp_args(registrations: &[crate::McpRegistration]) -> Vec<String> {
struct ClaudeLaunchOptions {
/// Model id with a `[1m]` suffix appended for the 1M context window.
model_id: Option<String>,
/// `--effort` value after T3's compatibility transforms (`None` when the
/// selection is `ultrathink`, which is a prompt-prefix mode).
/// Normalized `--effort` value (`None` when the selection is `ultrathink`,
/// which is a prompt-prefix mode).
effort: Option<String>,
/// `--settings` JSON string (fastMode / ultracode / alwaysThinkingEnabled).
settings_json: Option<String>,
Expand Down Expand Up @@ -831,7 +831,7 @@ async fn handle_command(
}
SessionCommand::SetInteractionMode(mode) => {
// Stored now; the `set_permission_mode` switch is issued before the
// next `SendTurn` (matching T3's per-message application).
// next `SendTurn`, so the mode changes between messages.
mapper.interaction_mode = mode;
ControlFlow::Continue(())
}
Expand Down Expand Up @@ -2982,9 +2982,8 @@ fn has_boolean_option(spec: &ModelSpec, id: &str) -> bool {
}

/// Resolve the effort selection against the model's `reasoningEffort`
/// descriptor: an accepted listed value wins, else the descriptor default
/// (T3's `resolveClaudeEffort` / `getProviderOptionDescriptors`). `None` when
/// the model has no reasoning selector (e.g. Haiku).
/// descriptor: an accepted listed value wins, else the descriptor default.
/// Returns `None` when the model has no reasoning selector (e.g. Haiku).
fn resolve_claude_effort(spec: Option<&ModelSpec>, raw: Option<&str>) -> Option<String> {
let spec = spec?;
let (options, default_value) = spec.options.iter().find_map(|o| match o {
Expand All @@ -3004,9 +3003,10 @@ fn resolve_claude_effort(spec: Option<&ModelSpec>, raw: Option<&str>) -> Option<
default_value.clone()
}

/// T3's `normalizeClaudeCliEffort`: `ultrathink` → no flag (prompt prefix);
/// `ultracode` → `xhigh`; `xhigh` → `max` except Fable 5.x / Opus 5 /
/// Opus 4.8 / Sonnet 5; Sonnet 4.6 `max` → `high`; otherwise passthrough.
/// Normalize special effort modes for the Claude CLI: `ultrathink` → no flag
/// (prompt prefix); `ultracode` → `xhigh`; `xhigh` → `max` except Fable 5.x /
/// Opus 5 / Opus 4.8 / Sonnet 5; Sonnet 4.6 `max` → `high`; otherwise
/// passthrough.
fn normalize_claude_cli_effort(effort: Option<&str>, model: Option<&str>) -> Option<String> {
let effort = effort?;
if effort == "ultrathink" {
Expand Down Expand Up @@ -3094,8 +3094,7 @@ fn model(id: &str, display_name: &str, options: Vec<OptionDescriptor>) -> ModelS
}
}

/// The full static Claude catalog (unfiltered by version). Mirrors T3's
/// `BUILT_IN_MODELS`.
/// The full static Claude catalog, unfiltered by installed CLI version.
fn built_in_models() -> Vec<ModelSpec> {
vec![
model(
Expand Down Expand Up @@ -4551,7 +4550,7 @@ mod tests {

#[test]
fn deny_cancel_and_session_approval_wire_strings() {
// Deny → T3's exact "declined" message.
// Denials use the user-facing message expected by the approval flow.
let mut m = Mapper::new();
feed(
&mut m,
Expand Down Expand Up @@ -4617,7 +4616,7 @@ mod tests {
}

#[test]
fn classification_matrix_covers_t3_substring_quirks() {
fn classification_matrix_covers_substring_priority() {
use ClaudeRequestType::*;
let cases = [
("Read", FileRead), // exact lowercase "read"
Expand Down
21 changes: 10 additions & 11 deletions crates/agent/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod developer_instructions;
use developer_instructions::{DEFAULT_MODE_INSTRUCTIONS, PLAN_MODE_INSTRUCTIONS};

/// Fallback model slug for `collaborationMode.settings.model` when the session
/// has no resolved model yet (mirrors T3's `DEFAULT_MODEL`).
/// has no resolved model yet.
const DEFAULT_MODEL: &str = "gpt-5-codex";
const ELICITATION_URL_ACK_LABEL: &str = "I've opened the link";
const ELICITATION_URL_CANCEL_LABEL: &str = "Cancel";
Expand Down Expand Up @@ -65,9 +65,8 @@ pub async fn start(opts: SessionOptions) -> Result<SessionHandle, AgentError> {
.await
}

/// Spawn `codex app-server`, page through `model/list`, and tear the process
/// down. Mirrors T3's `requestAllCodexModels` (initial `{}`, then `{cursor}`
/// until `nextCursor` is empty).
/// Spawn `codex app-server`, page through `model/list` (initial `{}`, then
/// `{cursor}` until `nextCursor` is empty), and tear the process down.
pub async fn list_models(
binary_path: Option<PathBuf>,
launch_env: LaunchEnv,
Expand Down Expand Up @@ -262,7 +261,7 @@ fn map_model(model: &Value) -> Option<ModelSpec> {
}

/// Derive service-tier options from `serviceTiers` (preferred) or, absent that,
/// `additionalSpeedTiers` (`fast` `Fast`), matching T3's mapping.
/// `additionalSpeedTiers`, displaying the `fast` value as `Fast`.
fn service_tiers(model: &Value) -> Vec<SelectOption> {
if let Some(tiers) = model.get("serviceTiers").and_then(Value::as_array)
&& !tiers.is_empty()
Expand Down Expand Up @@ -310,7 +309,8 @@ fn service_tiers(model: &Value) -> Vec<SelectOption> {
Vec::new()
}

/// `gpt…` → `GPT…`, and capitalize the letter after each hyphen (T3 transform).
/// Format model ids for display: `gpt…` → `GPT…`, capitalizing the letter
/// after each hyphen.
fn codex_display_name(raw: &str) -> String {
let base = if raw.get(..3).is_some_and(|p| p.eq_ignore_ascii_case("gpt")) {
format!("GPT{}", &raw[3..])
Expand Down Expand Up @@ -1151,8 +1151,7 @@ impl Actor {
}

/// Build `turn/start` params, applying per-turn overrides on top of the
/// session's persisted effort / service tier / interaction mode. Mirrors
/// T3's `buildTurnStartParams` + `buildCodexCollaborationMode`.
/// session's persisted effort, service tier, and interaction mode.
fn build_turn_params(
&self,
text: &str,
Expand Down Expand Up @@ -2404,8 +2403,8 @@ fn tool_output(item: &Value) -> Option<String> {
.map(Value::to_string)
}

/// Map one `turn/plan/updated` step (status fallback `pending`, step text
/// fallback `"step"`), mirroring T3's CodexAdapter plan mapping.
/// Map one `turn/plan/updated` step, falling back to `pending` status and
/// `"step"` text when those fields are absent or unusable.
fn map_plan_step(step: &Value) -> PlanStep {
let text = step
.get("step")
Expand Down Expand Up @@ -3746,7 +3745,7 @@ mod tests {
assert_eq!(questions[0].options.len(), 1, "empty-label option dropped");
assert_eq!(questions[0].options[0].label, "macOS");
assert!(!questions[0].multi_select);
// Free-text-only question kept with empty options (T3 bug fix).
// Free-text-only questions remain valid with empty options.
assert_eq!(questions[1].id, "free");
assert!(questions[1].options.is_empty());
}
Expand Down
16 changes: 8 additions & 8 deletions crates/agent/src/codex/developer_instructions.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
//! Codex `collaborationMode.settings.developer_instructions` texts, ported
//! verbatim from T3 Code's `CodexDeveloperInstructions.ts` (plan + default
//! variants, each with the shared `<collaboration_mode>` wrapper and the
//! T3 Code collaborative-browser tool instructions appended before the closing
//! tag). Do not paraphrase — these are wire-exact so plan-mode behavior matches.
//! Codex `collaborationMode.settings.developer_instructions` texts for plan and
//! default modes, each with a shared `<collaboration_mode>` wrapper and preview
//! browser instructions. The `<proposed_plan>` block shape and the
//! `request_user_input` / `update_plan` rules are consumed by tcode's Codex
//! adapter and plan UI; edit those contracts together.

macro_rules! mode_instructions {
($body:literal) => {
concat!($body, "\n", "

## T3 Code collaborative browser
## tcode preview browser

You are running inside T3 Code. The `t3-code` MCP server is the product-native collaborative browser shared with the user. When it exposes `preview_*` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.
You are running inside tcode. The `tcode_preview` MCP server is the embedded preview browser shared with the user. When it exposes `preview_*` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.

For browser work, first call `preview_status`. If no automation-capable preview is attached, call `preview_open` before concluding that the browser is unavailable. Then use `preview_navigate`, `preview_snapshot`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.

Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or `preview_open` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.
Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the tcode preview tools are absent, the user explicitly requests another browser, or `preview_open` returns an explicit unsupported/unavailable error. A failed tcode preview tool call should be inspected and retried with corrected arguments when the error is actionable.
", "\n</collaboration_mode>")
};
}
Expand Down
4 changes: 2 additions & 2 deletions crates/agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ pub fn claude_mcp_config_json<'a>(
serde_json::json!({ "mcpServers": servers }).to_string()
}

/// One model a provider offers, with its selectable options (T3-style descriptors).
/// One model a provider offers, with its selectable options.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModelSpec {
pub id: String, // provider-native id sent on the wire
Expand Down Expand Up @@ -672,7 +672,7 @@ pub enum ProviderCommandKind {
Skill,
}

/// Interaction mode (T3: Build/Plan).
/// Whether the agent should execute work or propose a plan.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InteractionMode {
Expand Down
32 changes: 14 additions & 18 deletions crates/core/src/git.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Git quick-actions support (ported from T3's `GitActionsControl.logic.ts`,
//! `GitWorkflowService.ts` and `GitVcsDriverCore.ts`, trimmed to the local,
//! single-SCM subset — no PR/MR, no publish-to-provider wizard).
//! Git quick-actions for the local, single-SCM workflow. This excludes PR/MR
//! operations and publish-to-provider wizards.
//!
//! This core owns status parsing, the adaptive quick-action state machine,
//! path-spec selection, slug generation, and prompt builders. Process-backed
Expand Down Expand Up @@ -45,8 +44,8 @@ pub struct GitFileEntry {
pub deletions: u32,
}

/// A snapshot of a repository's state, driving the adaptive quick-action
/// button. Mirrors the subset of T3's `VcsStatusResult` we act on.
/// A snapshot of the repository state needed by the adaptive quick-action
/// button.
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct GitStatus {
/// `cwd` is inside a git working tree.
Expand Down Expand Up @@ -153,9 +152,8 @@ impl QuickAction {
/// Resolve the primary quick-action for `status`. `is_busy` is true while an
/// action is already running (the button is disabled with an in-progress hint).
///
/// Ported/trimmed from T3's `resolveQuickAction` — the PR/MR and
/// publish-repository branches collapse to `PublishBranch` (push `-u`) and the
/// disabled hints.
/// PR/MR and publish-repository workflows are out of scope; a branch without
/// an upstream instead resolves to `PublishBranch` (push `-u`).
pub fn quick_action(status: &GitStatus, is_busy: bool) -> QuickAction {
if is_busy {
return QuickAction::hint(GitAction::Commit, GitHint::InProgress);
Expand Down Expand Up @@ -207,9 +205,8 @@ pub struct MenuItem {
pub hint: Option<GitHint>,
}

/// Build the quick-action dropdown items for `status`. Always offers the
/// actions that make sense for the repo shape, disabling the inapplicable ones
/// with a reason (T3's `buildMenuItems` + the exact disabled hints).
/// Build the quick-action dropdown items for `status`. Offers the actions that
/// make sense for the repository shape and gives disabled actions a reason.
pub fn menu_items(status: &GitStatus, is_busy: bool) -> Vec<MenuItem> {
if !status.is_repo {
return vec![MenuItem {
Expand Down Expand Up @@ -298,8 +295,7 @@ fn pull_disabled_hint(status: &GitStatus) -> GitHint {
///
/// Returns `None` when nothing is excluded (stage everything: `git add -A`),
/// otherwise `Some(included)` — the checked subset, staged explicitly so
/// unchecked files are left out of the commit. Ported from T3's
/// `selectedFiles`/`filePaths` handling in `GitActionsControl.tsx`.
/// unchecked files are left out of the commit.
pub fn included_paths(all: &[GitFileEntry], excluded: &HashSet<String>) -> Option<Vec<String>> {
if excluded.is_empty() {
return None;
Expand All @@ -312,9 +308,9 @@ pub fn included_paths(all: &[GitFileEntry], excluded: &HashSet<String>) -> Optio
)
}

/// Sanitize an arbitrary string into a lowercase git ref fragment (T3's
/// `sanitizeBranchFragment`): strip quotes, collapse separators, cap at 48
/// chars. Falls back to `"update"` when empty.
/// Sanitize an arbitrary string into a lowercase git ref fragment: strip
/// quotes, collapse separators, and cap at 48 chars. Falls back to `"update"`
/// when empty.
pub fn sanitize_branch_fragment(raw: &str) -> String {
let is_valid =
|c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '/' | '_' | '-');
Expand Down Expand Up @@ -342,7 +338,7 @@ pub fn sanitize_branch_fragment(raw: &str) -> String {
}
}

// Collapse runs of '/' and of '-' (underscores are preserved, as in T3).
// Collapse runs of '/' and of '-'; underscores are preserved.
let mut collapsed = String::with_capacity(out.len());
let mut prev: Option<char> = None;
for ch in out.chars() {
Expand Down Expand Up @@ -797,7 +793,7 @@ mod tests {
sanitize_branch_fragment("Add: Feature!! Foo"),
"add-feature-foo"
);
// Underscores are preserved (T3 semantics); separator edges are trimmed.
// Underscores are preserved; separator edges are trimmed.
assert_eq!(
sanitize_branch_fragment(" --Weird__Name-- "),
"weird__name"
Expand Down
12 changes: 6 additions & 6 deletions crates/core/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fn review_fence(contents: &str) -> String {
format!("{fence}diff\n{}\n{fence}", contents.trim_end())
}

/// Serialize review notes using T3's exact `<review_comment ...>` wire format.
/// Serialize review notes as `<review_comment ...>` blocks in the agent prompt.
pub fn append_review_comments_to_prompt(prompt: &str, comments: &[ReviewComment]) -> String {
if comments.is_empty() {
return prompt.to_string();
Expand Down Expand Up @@ -1238,9 +1238,9 @@ pub fn plan_title(markdown: &str) -> Option<String> {
None
}

/// The exact implementation prompt sent when a proposed plan is accepted
/// (`Implement` / `Implement in a new thread`): the T3 verbatim prefix plus the
/// trimmed plan markdown.
/// Build the implementation prompt sent when a proposed plan is accepted
/// (`Implement` / `Implement in a new thread`). The runtime's plan-accept flow
/// and tests expect this prefix, followed by the trimmed plan markdown.
pub fn implement_prompt(markdown: &str) -> String {
format!("PLEASE IMPLEMENT THIS PLAN:\n{}", markdown.trim())
}
Expand Down Expand Up @@ -2357,7 +2357,7 @@ mod tests {
}

#[test]
fn implement_prompt_uses_verbatim_prefix() {
fn implement_prompt_uses_plan_accept_prefix() {
assert_eq!(
implement_prompt(" # Plan\nDo the thing\n "),
"PLEASE IMPLEMENT THIS PLAN:\n# Plan\nDo the thing"
Expand Down Expand Up @@ -2733,7 +2733,7 @@ mod tests {
}

#[test]
fn review_comment_serialization_matches_t3_format() {
fn review_comment_serialization_matches_prompt_format() {
let comment = ReviewComment::new(
"src/lib.rs".into(),
7,
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub fn provider_key(provider: ProviderKind) -> &'static str {
}
}

/// The provider's short, T3-style display name (the card title / picker label).
/// The provider's short display name used for card titles and picker labels.
pub fn provider_label(provider: ProviderKind) -> &'static str {
match provider {
ProviderKind::Codex => "Codex",
Expand Down
5 changes: 2 additions & 3 deletions crates/preview-mcp/src/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ pub const STATUS: &str = r#"(() => ({
loading: document.readyState !== "complete"
}))()"#;

/// Build a DOM outline: page metadata plus an array of visible interactive
/// elements with `{ tag, role, name, selector, x, y, width, height }`. Ported
/// (reduced) from T3's `captureAutomationSnapshot` in-page script.
/// Build a compact DOM outline: page metadata plus an array of visible
/// interactive elements with `{ tag, role, name, selector, x, y, width, height }`.
pub const SNAPSHOT: &str = r##"(() => {
const MAX_ELEMENTS = 100;
const MAX_TEXT = 4000;
Expand Down
6 changes: 3 additions & 3 deletions crates/runtime/src/app/active_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ impl QueuedMessage {
}
}

/// Providers require non-empty turn text: an image-only message goes on the
/// wire with T3's synthetic placeholder while the transcript records the
/// user's (empty) text plus the attachments.
/// Providers require non-empty turn text: an image-only message uses a
/// synthetic placeholder on the wire while the transcript records the user's
/// empty text plus the attachments.
pub(super) fn wire_text_with_placeholder(text: String, attachments: &[Attachment]) -> String {
if text.trim().is_empty() && !attachments.is_empty() {
tcode_core::attachments::IMAGE_ONLY_MESSAGE.to_string()
Expand Down
15 changes: 7 additions & 8 deletions crates/runtime/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4560,7 +4560,7 @@ fn ultrathink_rides_with_the_queued_message() {
}

/// An image-only send keeps its empty text in the transcript (the bubble
/// renders just the thumbnails) while the wire carries T3's placeholder.
/// renders just the thumbnails) while the wire carries a placeholder.
#[test]
fn image_only_message_gets_placeholder_on_the_wire_only() {
let (commands, receiver) = smol::channel::unbounded();
Expand Down Expand Up @@ -5507,8 +5507,8 @@ fn stop_then_new_thread_keeps_the_first_message_visible() {
cx,
);

// Stop. The provider reports an error + an interrupted turn — the
// truncated-error moment in the T3 repro.
// Stop. The provider reports an error and an interrupted turn while
// preserving the complete multi-line error for later presentation.
state.host.interrupt(state.selected.as_deref().unwrap_or_default(), cx);
assert!(matches!(
commands_a.try_recv(),
Expand Down Expand Up @@ -5829,11 +5829,10 @@ fn turn_running_for_is_independent_of_active_or_parked_location() {
});
}

/// The T3 Code session-reaper failure class, our variant: switching to
/// another thread must NOT kill a session whose turn is still running. The
/// session parks in the background — process and queue alive, events still
/// recorded, sidebar still "Working" — and selecting it again re-adopts it
/// with the streamed-while-parked content visible.
/// Switching to another thread must not kill a session whose turn is still
/// running. The session parks in the background — process and queue alive,
/// events still recorded, sidebar still "Working" — and selecting it again
/// re-adopts it with the streamed-while-parked content visible.
#[test]
fn switching_threads_parks_a_working_session_instead_of_killing_it() {
let cx = &mut TestAppContext::default();
Expand Down
Loading
Loading