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
18 changes: 18 additions & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ pub trait API: Sync + Send {
/// disk footprint. Safe to call at any time; safe to call repeatedly.
async fn optimize_fts_index(&self) -> Result<()>;

/// Re-binds a subagent conversation to a different parent. Pass `None`
/// for `new_parent_id` to detach (promotes the subagent to a top-level
/// session). Atomic single-row update; does not recurse into descendants.
async fn update_parent_id(
&self,
conversation_id: &ConversationId,
new_parent_id: Option<&ConversationId>,
) -> Result<()>;
Comment on lines +125 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Expand the method documentation to include explicit # Arguments and # Errors sections for this newly added public API method. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a newly added public trait method with parameters and a fallible return type, so the custom rule requires Rustdoc # Arguments and # Errors sections. The existing code only has a brief description and does not include those required sections, so the violation is real.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_api/src/api.rs
**Line:** 125:132
**Comment:**
	*Custom Rule: Expand the method documentation to include explicit `# Arguments` and `# Errors` sections for this newly added public API method.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


/// Retrieves conversations whose `cwd` column matches the given path
/// exactly. Used by the session viewer to filter by current working
/// directory (per-project scoping).
async fn get_conversations_by_cwd(
&self,
cwd: &str,
limit: Option<usize>,
) -> Result<Option<Vec<Conversation>>>;
Comment on lines +134 to +141

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Update this new public method's docs to include # Arguments for cwd and limit, and # Errors to describe failure conditions. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is also a newly added public method with parameters and a Result return type. The docs are missing the required # Arguments and # Errors sections, so it violates the Rustdoc rule.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_api/src/api.rs
**Line:** 134:141
**Comment:**
	*Custom Rule: Update this new public method's docs to include `# Arguments` for `cwd` and `limit`, and `# Errors` to describe failure conditions.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


/// Renames a conversation by setting its title
///
/// # Arguments
Expand Down
18 changes: 18 additions & 0 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,24 @@ impl<
self.services.optimize_fts_index().await
}

async fn update_parent_id(
&self,
conversation_id: &ConversationId,
new_parent_id: Option<&ConversationId>,
) -> Result<()> {
self.services
.update_parent_id(conversation_id, new_parent_id)
.await
}
Comment on lines +245 to +253

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: This new reparent API path blindly forwards success from lower layers and does not enforce the "conversation must exist" contract, so invalid IDs can be reported as successful operations when the DB update affects zero rows. Ensure the repository/service returns a not-found error on zero affected rows and propagate it here. [logic error]

Severity Level: Major ⚠️
- ⚠️ Reparent CLI command misreports success for nonexistent conversations.
- ⚠️ Forge API update_parent_id cannot signal not-found errors.
- ⚠️ ConversationRepository contract about missing rows is violated.
Steps of Reproduction ✅
1. Trigger the reparent feature via the CLI `:reparent` command, which is parsed into
`AppCommand::Reparent { target }` in `crates/forge_main/src/model.rs:690-713`, while
`self.state.conversation_id` in the UI points to a conversation ID that no longer exists
in the database (e.g., deleted externally or from another workspace).

2. The UI handler `UI::handle_reparent` in `crates/forge_main/src/ui.rs:2333-2345` reads
`self.state.conversation_id` into `conversation_id` and then calls
`self.api.update_parent_id(&conversation_id, new_parent_id.as_ref())` at
`crates/forge_main/src/ui.rs:2372-2374`.

3. The `ForgeAPI` implementation in `crates/forge_api/src/forge_api.rs:245-253` forwards
this call unchanged to the service layer: `self.services.update_parent_id(conversation_id,
new_parent_id).await`, which in turn is forwarded by
`ForgeConversationService::update_parent_id` at
`crates/forge_services/src/conversation.rs:15-23` to `ForgeRepo::update_parent_id` at
`crates/forge_repo/src/forge_repo.rs:190-22`, and finally to
`ConversationRepo::update_parent_id` in
`crates/forge_repo/src/conversation/conversation_repo.rs:328-22`.

4. In `ConversationRepo::update_parent_id`, the Diesel update is executed as
`diesel::update(...).execute(connection)?;` at
`crates/forge_repo/src/conversation/conversation_repo.rs:338-345` and the returned
affected-row count is ignored, so when `conversation_id` does not exist the function still
returns `Ok(())`. This violates the documented contract in
`ConversationRepository::update_parent_id` (`crates/forge_domain/src/repo.rs:190-17`,
which promises an error "if the update fails or the conversation does not exist) and
causes the UI to print a success message `"Re-parented current session to ..."` at
`crates/forge_main/src/ui.rs:2375-2379` even though no row was updated.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_api/src/forge_api.rs
**Line:** 245:253
**Comment:**
	*Logic Error: This new reparent API path blindly forwards success from lower layers and does not enforce the "conversation must exist" contract, so invalid IDs can be reported as successful operations when the DB update affects zero rows. Ensure the repository/service returns a not-found error on zero affected rows and propagate it here.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


async fn get_conversations_by_cwd(
&self,
cwd: &str,
limit: Option<usize>,
) -> Result<Option<Vec<Conversation>>> {
self.services.get_conversations_by_cwd(cwd, limit).await
}

async fn rename_conversation(
&self,
conversation_id: &ConversationId,
Expand Down
38 changes: 38 additions & 0 deletions crates/forge_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,24 @@ pub trait ConversationService: Send + Sync {
/// back into a single segment, reducing query-time shadow-walk cost and
/// disk footprint. Safe to call at any time; safe to call repeatedly.
async fn optimize_fts_index(&self) -> anyhow::Result<()>;

/// Re-binds a subagent conversation to a different parent. Pass `None`
/// for `new_parent_id` to detach (promotes the subagent to a top-level
/// session). Atomic single-row update; does not recurse into descendants.
async fn update_parent_id(
&self,
conversation_id: &ConversationId,
new_parent_id: Option<&ConversationId>,
) -> anyhow::Result<()>;
Comment on lines +302 to +309

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Expand the method documentation to include both # Arguments and # Errors sections since this public API takes parameters and returns a fallible result. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a newly added public trait method with parameters and a fallible
return type. The docs currently provide a description, but they do not
include the required # Arguments and # Errors sections, so the rule
violation is real.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_app/src/services.rs
**Line:** 302:309
**Comment:**
	*Custom Rule: Expand the method documentation to include both `# Arguments` and `# Errors` sections since this public API takes parameters and returns a fallible result.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


/// Retrieves conversations whose `cwd` column matches the given path
/// exactly. Used by the session viewer to filter by current working
/// directory (per-project scoping).
async fn get_conversations_by_cwd(
&self,
cwd: &str,
limit: Option<usize>,
) -> anyhow::Result<Option<Vec<Conversation>>>;
Comment on lines +311 to +318

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Update the documentation to include # Arguments and # Errors sections so this new public method meets the required Rustdoc format for parameterized fallible APIs. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is also a newly added public method with parameters and a fallible
return type. The current Rustdoc lacks the required # Arguments and
# Errors sections, so the suggestion correctly identifies a real rule
violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_app/src/services.rs
**Line:** 311:318
**Comment:**
	*Custom Rule: Update the documentation to include `# Arguments` and `# Errors` sections so this new public method meets the required Rustdoc format for parameterized fallible APIs.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}

#[async_trait::async_trait]
Expand Down Expand Up @@ -723,6 +741,26 @@ impl<I: Services> ConversationService for I {
async fn optimize_fts_index(&self) -> anyhow::Result<()> {
self.conversation_service().optimize_fts_index().await
}

async fn update_parent_id(
&self,
conversation_id: &ConversationId,
new_parent_id: Option<&ConversationId>,
) -> anyhow::Result<()> {
self.conversation_service()
.update_parent_id(conversation_id, new_parent_id)
.await
}

async fn get_conversations_by_cwd(
&self,
cwd: &str,
limit: Option<usize>,
) -> anyhow::Result<Option<Vec<Conversation>>> {
self.conversation_service()
.get_conversations_by_cwd(cwd, limit)
.await
}
}
#[async_trait::async_trait]
impl<I: Services> ProviderService for I {
Expand Down
14 changes: 14 additions & 0 deletions crates/forge_domain/src/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ pub struct Conversation {
pub metadata: MetaData,
pub parent_id: Option<ConversationId>,
pub source: Option<String>,
/// Working directory of the agent when the conversation was created.
/// Used for grouping / filtering in the session selector and for FTS5
/// search so a user can find sessions by cwd fragment (e.g. "forgecode").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
/// Number of message entries in `context.messages` at the time of the
/// last write. Used to display a turn count in the session selector
/// and as a stable secondary sort key when the user picks "by turns".
/// Kept as a column (not a derived getter) so the selector does not
/// have to deserialize the full Context blob for every row.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_count: Option<i32>,
Comment on lines +54 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Since these newly added optional configuration fields are part of a setter-driven domain struct, update the struct-level setter derive configuration to include strip_option (alongside into) so callers can set plain values without wrapping in Some(...). [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The Conversation struct is a domain data type with derive_setters::Setters, but it only uses #[setters(into)] and does not include strip_option. Because the newly added fields are Option<T> configuration fields, the custom rule applies and this is a real violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_domain/src/conversation.rs
**Line:** 54:62
**Comment:**
	*Custom Rule: Since these newly added optional configuration fields are part of a setter-driven domain struct, update the struct-level setter derive configuration to include `strip_option` (alongside `into`) so callers can set plain values without wrapping in `Some(...)`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}

#[derive(Debug, Setters, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -75,6 +87,8 @@ impl Conversation {
context: None,
parent_id: None,
source: None,
cwd: None,
Comment on lines 89 to +90

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 Badge Populate cwd for newly saved conversations

New conversations still default cwd to None, and the normal creation/save paths never fill it from the environment before this value is persisted. For sessions created after this migration, the database column remains NULL, so exact cwd lookups and cwd FTS matches cannot find the very conversations the feature is meant to group by project.

Useful? React with 👍 / 👎.

message_count: None,
}
}
/// Creates a new conversation with a new conversation ID.
Expand Down
41 changes: 41 additions & 0 deletions crates/forge_domain/src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,47 @@ pub trait ConversationRepository: Send + Sync {
/// # Errors
/// Returns an error if the optimize statement fails to execute.
async fn optimize_fts_index(&self) -> Result<()>;

/// Re-binds a subagent conversation to a different parent. Pass `None`
/// for `new_parent_id` to detach the conversation entirely (promotes it
/// to a top-level session).
///
/// The existing `parent_id` (if any) is replaced atomically; no other
/// columns are touched. This does not recurse into descendants —
/// subagents of the reparented conversation remain linked to *this*
/// conversation.
///
/// # Arguments
/// * `conversation_id` - The conversation to reparent.
/// * `new_parent_id` - The new parent, or `None` to detach.
///
/// # Errors
/// Returns an error if the update fails or the conversation does not
/// exist.
async fn update_parent_id(
&self,
conversation_id: &ConversationId,
new_parent_id: Option<&ConversationId>,
) -> Result<()>;

/// Retrieves conversations by working directory (cwd).
///
/// Used by the session viewer to scope by cwd (per-project filtering).
/// The match is an exact equality on the `cwd` column, not a fuzzy
/// search — combine with [`Self::search_conversations`] for substring
/// matching.
///
/// # Arguments
/// * `cwd` - Exact cwd to match.
/// * `limit` - Optional cap on returned rows.
///
/// # Errors
/// Returns an error if the query fails.
async fn get_conversations_by_cwd(
&self,
cwd: &str,
limit: Option<usize>,
) -> Result<Option<Vec<Conversation>>>;
}

#[async_trait::async_trait]
Expand Down
25 changes: 25 additions & 0 deletions crates/forge_main/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,29 @@ pub enum AppCommand {
#[command(alias = "p")]
Parent,

/// Re-bind the current (subagent) conversation to a different parent.
/// Usage: `:reparent <parent_id>` or `:reparent --detach` to promote to a
/// top-level session.
#[strum(props(usage = "Re-parent the current session. Usage: :reparent <id>|--detach"))]
#[command(alias = "rp")]
Reparent {
/// New parent conversation ID, or `--detach` to promote this
/// session to top-level.
#[arg(trailing_var_arg = true, num_args = 0..)]
target: Vec<String>,
},

/// Filter conversations by working directory. Usage: `:cwd <path>` or
/// `:cwd --current` to scope to the current shell cwd.
#[strum(props(usage = "Filter conversations by cwd. Usage: :cwd <path>|--current"))]
#[command(alias = "cw")]
Cwd {
/// Cwd to filter by (exact match), or `--current` to use the
/// current shell working directory.
#[arg(trailing_var_arg = true, num_args = 0..)]
target: Vec<String>,
},

/// Full-text search over conversation titles and contents (FTS5 BM25).
/// Usage: `:search <query>` or `:search "rust refactor"`.
#[strum(props(usage = "Search conversation history. Usage: :search <query>"))]
Expand Down Expand Up @@ -787,6 +810,8 @@ impl AppCommand {
AppCommand::Goal { .. } => "goal",
AppCommand::Loop { .. } => "loop",
AppCommand::Parent => "parent",
AppCommand::Reparent { .. } => "reparent",
AppCommand::Cwd { .. } => "cwd",
AppCommand::Search { .. } => "search",
AppCommand::Delete => "delete",
AppCommand::Rename { .. } => "rename",
Expand Down
14 changes: 13 additions & 1 deletion crates/forge_main/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub struct UIState {
pub goal: Option<String>,
pub loop_enabled: bool,
pub last_activity: Instant,
/// CWD filter for the conversation selector. When set, the selector
/// scopes its results to conversations whose `cwd` column matches.
/// This is the "filter by project directory" UX.
pub cwd_filter: Option<String>,
Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The new cwd filter state is introduced but never read anywhere in the conversation listing flow, so setting :cwd only updates in-memory state and does not actually change which conversations are fetched/displayed. Wire this field into the list query path (or remove it) so the command has real effect. [incomplete implementation]

Severity Level: Major ⚠️
- ❌ :cwd command does not change conversation listing.
- ⚠️ New cwd_filter state unused, confusing session filtering UX.
Steps of Reproduction ✅
1. Enter the `:cwd <path>` command in the TUI, which is parsed as `AppCommand::Cwd {
target }` in `crates/forge_main/src/model.rs:704-723` (including the `#[command(alias =
"cw")]` wiring for this subcommand).

2. The UI routes this to `UI::handle_cwd` in `crates/forge_main/src/ui.rs:2387-2464`,
which validates flags and then sets `self.state.cwd_filter = Some(cwd.clone());` at
`crates/forge_main/src/ui.rs:2459-2461` (and similarly clears it at `ui.rs:2399-2402`),
while the field itself is defined on `UIState` at `crates/forge_main/src/state.rs:17-20`.

3. A search for `cwd_filter` in `crates/forge_main/src` (see Grep results) shows it is
only defined in `state.rs:17-20` and written in `ui.rs:2399` and `ui.rs:2418`, with no
reads such as `if let Some(cwd) = self.state.cwd_filter` or any use in
conversation-fetching logic; therefore setting the filter only mutates in-memory state.

4. When the user later opens the conversation selector (e.g., via
`SelectCommand::Conversation`), the UI still fetches conversations using
`self.api.get_parent_conversations(Some(max_conversations)).await?` in `UI::handle_select`
at `crates/forge_main/src/ui.rs:913-916` and passes the full list to
`ConversationSelector::select_conversation` at
`crates/forge_main/src/conversation_selector.rs:49-60`, never calling
`get_conversations_by_cwd` from the API nor consulting `self.state.cwd_filter`, so the
`:cwd` command has no effect on which conversations are displayed.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/forge_main/src/state.rs
**Line:** 17:20
**Comment:**
	*Incomplete Implementation: The new cwd filter state is introduced but never read anywhere in the conversation listing flow, so setting `:cwd` only updates in-memory state and does not actually change which conversations are fetched/displayed. Wire this field into the list query path (or remove it) so the command has real effect.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
Comment on lines +17 to 21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The PR description mentions a :sort turns|updated|created command, a sort field on UIState, and handle_sort in ui.rs. However, none of these changes are present in the actual code diff. It appears the sorting UI implementation was omitted from this pull request.


impl Default for UIState {
Expand All @@ -24,12 +28,20 @@ impl Default for UIState {
goal: None,
loop_enabled: false,
last_activity: Instant::now(),
cwd_filter: None,
}
}
}

impl UIState {
pub fn new(env: Environment) -> Self {
Self { cwd: env.cwd, conversation_id: Default::default(), goal: None, loop_enabled: false, last_activity: Instant::now() }
Self {
cwd: env.cwd,
conversation_id: Default::default(),
goal: None,
loop_enabled: false,
last_activity: Instant::now(),
cwd_filter: None,
}
}
}
101 changes: 101 additions & 0 deletions crates/forge_main/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2328,6 +2328,101 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
Ok(())
}

/// Re-binds the current (subagent) conversation to a different parent.
/// Usage:
/// - `:reparent <parent_id>` → attach to the given parent
/// - `:reparent --detach` → promote this session to top-level
/// - `:reparent` → no-arg; shows usage hint
async fn handle_reparent(&mut self, target: Vec<String>) -> anyhow::Result<()> {
let conversation_id = match self.state.conversation_id {
Some(id) => id,
None => {
self.writeln_title(TitleFormat::error(
"No active session. Start a conversation first.",
))?;
return Ok(());
}
};

if target.is_empty() {
self.writeln_title(TitleFormat::info(
"Usage: :reparent <parent_id> | :reparent --detach",
))?;
return Ok(());
}

// `:reparent --detach` → detach (None)
// `:reparent <anything else>` → parse as a ConversationId
let new_parent_id = if target.iter().any(|t| t == "--detach") {
None
} else {
let raw = target.join(" ").trim().to_string();
match ConversationId::parse(&raw) {
Ok(id) => Some(id),
Err(err) => {
self.writeln_title(TitleFormat::error(format!(
"Invalid parent ID {raw:?}: {err}"
)))?;
return Ok(());
}
}
};
Comment on lines +2356 to +2369

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There is no check to prevent a conversation from being reparented to itself. If new_parent_id is equal to conversation_id, it will create a self-referential cycle in the database, which can cause infinite loops or stack overflows during recursive tree traversals. We should reject this case early.

        let new_parent_id = if target.iter().any(|t| t == "--detach") {
            None
        } else {
            let raw = target.join(" ").trim().to_string();
            match ConversationId::parse(&raw) {
                Ok(id) => {
                    if id == conversation_id {
                        self.writeln_title(TitleFormat::error(
                            "A session cannot be parented to itself.",
                        ))?;
                        return Ok(());
                    }
                    Some(id)
                }
                Err(err) => {
                    self.writeln_title(TitleFormat::error(format!(
                        "Invalid parent ID {raw:?}: {err}"
                    )))?;
                    return Ok(());
                }
            }
        };


self.api
.update_parent_id(&conversation_id, new_parent_id.as_ref())
.await?;

let msg = match new_parent_id {
Some(pid) => format!(
"Re-parented current session to {}.",
pid.into_string().bold()
),
None => "Detached current session — promoted to top-level.".to_string(),
};
self.writeln_title(TitleFormat::info(msg))?;
Ok(())
}

/// Filters the conversation list by working directory. Usage:
/// - `:cwd <path>` → exact-match cwd filter
/// - `:cwd --current` → use the current shell working directory
/// - `:cwd --clear` → clear the cwd filter
async fn handle_cwd(&mut self, target: Vec<String>) -> anyhow::Result<()> {
Comment on lines +2386 to +2390

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

While handle_cwd successfully sets self.state.cwd_filter, this filter is never actually applied anywhere in ui.rs (such as in list_conversations or on_show_conversations). Both methods continue to fetch all parent conversations unfiltered. To make this feature functional, you should update those methods to use self.api.get_conversations_by_cwd when self.state.cwd_filter is set.

if target.is_empty() || target.iter().any(|t| t == "--help" || t == "-h") {
self.writeln_title(TitleFormat::info(
"Usage: :cwd <path> | :cwd --current | :cwd --clear",
))?;
return Ok(());
}

if target.iter().any(|t| t == "--clear") {
self.state.cwd_filter = None;
self.writeln_title(TitleFormat::info("Cleared cwd filter."))?;
return Ok(());
}

let cwd = if target.iter().any(|t| t == "--current") {
match std::env::current_dir() {
Ok(p) => p.to_string_lossy().to_string(),
Err(err) => {
self.writeln_title(TitleFormat::error(format!(
"Failed to read current dir: {err}"
)))?;
return Ok(());
}
}
} else {
target.join(" ").trim().to_string()
};
Comment on lines +2414 to +2416

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When a relative path is passed to :cwd, it is stored as-is in self.state.cwd_filter. However, the database stores absolute paths (e.g., resolved from env.cwd). An exact match on a relative path filter will fail to match the absolute paths in the database. We should resolve relative paths against self.state.cwd to ensure exact matching works correctly.

        } else {
            let raw_path = target.join(" ").trim().to_string();
            let path = std::path::Path::new(&raw_path);
            if path.is_relative() {
                self.state.cwd.join(path).to_string_lossy().to_string()
            } else {
                raw_path
            }
        };


self.state.cwd_filter = Some(cwd.clone());

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 Badge Wire the cwd filter into conversation listing

This stores a cwd filter and reports success, but the selector paths still call get_parent_conversations unconditionally and cwd_filter is never read after being set or cleared. As a result, running :cwd <path> appears to enable project scoping but the next :conversations list is still the unfiltered workspace list, and the new get_conversations_by_cwd API is unused.

Useful? React with 👍 / 👎.

self.writeln_title(TitleFormat::info(format!(
"Cwd filter set to {}",
cwd.bold()
)))?;
Ok(())
}

fn user_initiated_conversations(conversations: Vec<Conversation>) -> Vec<Conversation> {
let related_ids: HashSet<ConversationId> = conversations
.iter()
Expand Down Expand Up @@ -2382,6 +2477,12 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
AppCommand::Parent => {
self.handle_parent().await?;
}
AppCommand::Reparent { target } => {
self.handle_reparent(target).await?;
}
AppCommand::Cwd { target } => {
self.handle_cwd(target).await?;
}
AppCommand::Search { query } => {
self.handle_search(query).await?;
}
Expand Down
Loading
Loading