@webinson/peer 0.4.16 → 0.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs
2
+ index 9b6cd84..6e22c31 100644
3
+ --- a/codex-rs/config/src/lib.rs
4
+ +++ b/codex-rs/config/src/lib.rs
5
+ @@ -34,6 +34,7 @@ mod model_provider_defaults;
6
+ mod model_provider_requirements;
7
+ mod overrides;
8
+ mod path_context;
9
+ +pub mod peer_thread_mark;
10
+ pub mod permissions_toml;
11
+ mod plugin_edit;
12
+ pub mod profile_toml;
13
+ diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs
14
+ index a6640ad..7b0dd75 100644
15
+ --- a/codex-rs/core/src/config/mod.rs
16
+ +++ b/codex-rs/core/src/config/mod.rs
17
+ @@ -192,6 +192,7 @@ pub use codex_config::Constrained;
18
+ pub use codex_config::ConstraintError;
19
+ pub use codex_config::ConstraintResult;
20
+ pub use codex_config::LoaderOverrides;
21
+ +pub use codex_config::peer_thread_mark;
22
+ pub use codex_network_proxy::NetworkProxyAuditMetadata;
23
+ use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
24
+ pub use codex_sandboxing::system_bwrap_warning;
25
+ diff --git a/codex-rs/core/src/session/mcp_runtime.rs b/codex-rs/core/src/session/mcp_runtime.rs
26
+ index ac0bc4a..a1a0b76 100644
27
+ --- a/codex-rs/core/src/session/mcp_runtime.rs
28
+ +++ b/codex-rs/core/src/session/mcp_runtime.rs
29
+ @@ -116,7 +116,8 @@ impl Session {
30
+ session_source: session_configuration.session_source.clone(),
31
+ environments: resolved_environments.clone(),
32
+ local_process_cwd,
33
+ - disabled_plugin_ids: session_configuration.disabled_plugin_ids.clone(),
34
+ + disabled_plugin_ids: self
35
+ + .peer_disabled_plugins(&session_configuration.disabled_plugin_ids),
36
+ };
37
+ self.publish_mcp_runtime(
38
+ &desired,
39
+ diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
40
+ index bc2f4c2..d609646 100644
41
+ --- a/codex-rs/core/src/session/mod.rs
42
+ +++ b/codex-rs/core/src/session/mod.rs
43
+ @@ -4169,7 +4169,7 @@ impl Session {
44
+ .with_session_source(&turn_context.session_source)
45
+ .with_selected_environments(&environments.all_selections()),
46
+ &mcp.config().plugins,
47
+ - &turn_context.disabled_plugin_ids,
48
+ + &self.peer_disabled_plugins(&turn_context.disabled_plugin_ids),
49
+ )
50
+ .await;
51
+ for contributor in self.services.extensions.tool_contributors() {
52
+ @@ -4722,7 +4722,7 @@ impl Session {
53
+ .plugins_manager
54
+ .plugins_for_config(&turn_context.config.plugins_config_input())
55
+ .await
56
+ - .without_plugins(&turn_context.disabled_plugin_ids);
57
+ + .without_plugins(&self.peer_disabled_plugins(&turn_context.disabled_plugin_ids));
58
+ let recommended_plugin_candidates = if turn_context
59
+ .config
60
+ .features
61
+ diff --git a/codex-rs/core/src/session/plugin_selection.rs b/codex-rs/core/src/session/plugin_selection.rs
62
+ index 0a9580d..59c2e54 100644
63
+ --- a/codex-rs/core/src/session/plugin_selection.rs
64
+ +++ b/codex-rs/core/src/session/plugin_selection.rs
65
+ @@ -6,11 +6,16 @@ use super::Session;
66
+ use super::turn_context::TurnContext;
67
+
68
+ impl Session {
69
+ + pub(super) fn peer_disabled_plugins(&self, configured: &[String]) -> Vec<String> {
70
+ + codex_config::peer_thread_mark::disabled_plugins(&self.thread_id.to_string(), configured)
71
+ + }
72
+ +
73
+ pub(crate) async fn activate_plugin_selection(&self, turn_context: &TurnContext) {
74
+ + let disabled = self.peer_disabled_plugins(&turn_context.disabled_plugin_ids);
75
+ let config = {
76
+ let mut state = self.state.lock().await;
77
+ - if state.active_disabled_plugin_ids != turn_context.disabled_plugin_ids {
78
+ - state.active_disabled_plugin_ids = turn_context.disabled_plugin_ids.clone();
79
+ + if state.active_disabled_plugin_ids != disabled {
80
+ + state.active_disabled_plugin_ids.clone_from(&disabled);
81
+ self.mark_mcp_runtime_dirty();
82
+ }
83
+ Arc::clone(&state.session_configuration.original_config_do_not_use)
84
+ @@ -20,7 +25,7 @@ impl Session {
85
+ .plugins_manager
86
+ .plugins_for_config(&config.plugins_config_input())
87
+ .await
88
+ - .without_plugins(&turn_context.disabled_plugin_ids);
89
+ + .without_plugins(&disabled);
90
+ // Cache changes from another process do not notify this session's hook runtime.
91
+ if !self.hooks().matches_plugin_hooks(
92
+ plugin_outcome.iter_effective_plugin_hook_sources(),
93
+ diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs
94
+ index 8e2d7b3..e513f3e 100644
95
+ --- a/codex-rs/core/src/session/session.rs
96
+ +++ b/codex-rs/core/src/session/session.rs
97
+ @@ -1214,7 +1214,11 @@ impl Session {
98
+ let thread_extension_data_for_mcp = &thread_extension_data;
99
+ let mcp_originator = session_configuration.originator.clone();
100
+ let mcp_session_source = session_configuration.session_source.clone();
101
+ - let mcp_disabled_plugin_ids = session_configuration.disabled_plugin_ids.clone();
102
+ + let peer_disabled_plugin_ids = codex_config::peer_thread_mark::disabled_plugins(
103
+ + &thread_id.to_string(),
104
+ + &session_configuration.disabled_plugin_ids,
105
+ + );
106
+ + let mcp_disabled_plugin_ids = peer_disabled_plugin_ids.clone();
107
+ let auth_and_mcp_fut = async move {
108
+ let auth = mcp_auth.await;
109
+ if config_for_mcp.features.plugin_recommendations_enabled() {
110
+ @@ -1576,7 +1580,9 @@ impl Session {
111
+ if let Some(pin) = thread_extension_data.remove::<ReasoningEffortPin>() {
112
+ state.reasoning_effort_pin = pin.as_ref().clone();
113
+ }
114
+ - state.active_disabled_plugin_ids = session_configuration.disabled_plugin_ids.clone();
115
+ + state
116
+ + .active_disabled_plugin_ids
117
+ + .clone_from(&peer_disabled_plugin_ids);
118
+ let managed_network_requirements_configured = config
119
+ .config_layer_stack
120
+ .requirements_toml()
121
+ @@ -1657,7 +1663,7 @@ impl Session {
122
+ &config,
123
+ plugins_manager.as_ref(),
124
+ resolved_environments.single_local_environment(),
125
+ - &session_configuration.disabled_plugin_ids,
126
+ + &peer_disabled_plugin_ids,
127
+ )
128
+ .await;
129
+ let (hooks, async_hook_results) = Hooks::new(
130
+ @@ -1986,7 +1992,7 @@ impl Session {
131
+ .current_auth_matches(latest_auth.as_ref()),
132
+ session_source: &session_configuration.session_source,
133
+ originator: &session_configuration.originator,
134
+ - disabled_plugin_ids: &session_configuration.disabled_plugin_ids,
135
+ + disabled_plugin_ids: &peer_disabled_plugin_ids,
136
+ environments: McpEnvironmentScope::Selected(
137
+ &resolved_environment_selections,
138
+ ),
139
+ diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs
140
+ index 5cd4d77..92beb82 100644
141
+ --- a/codex-rs/core/src/session/turn.rs
142
+ +++ b/codex-rs/core/src/session/turn.rs
143
+ @@ -1038,7 +1038,7 @@ async fn required_mcp_servers_for_input(
144
+ .plugins_manager
145
+ .plugins_for_config(&turn_context.config.plugins_config_input())
146
+ .await
147
+ - .without_plugins(&turn_context.disabled_plugin_ids);
148
+ + .without_plugins(&sess.peer_disabled_plugins(&turn_context.disabled_plugin_ids));
149
+ let current_config = sess.services.mcp_runtime.current_config();
150
+ let mentioned_plugins =
151
+ collect_explicit_plugin_mentions(user_input, loaded_plugins.capability_summaries());
152
+ @@ -1900,7 +1900,7 @@ pub(crate) async fn prepare_tool_recommendations(
153
+ .plugins_manager
154
+ .plugins_for_config(&turn_context.config.plugins_config_input())
155
+ .await
156
+ - .without_plugins(&turn_context.disabled_plugin_ids);
157
+ + .without_plugins(&sess.peer_disabled_plugins(&turn_context.disabled_plugin_ids));
158
+ let tool_suggest_is_enabled = tool_suggest_enabled(turn_context);
159
+ let auth = if tool_suggest_is_enabled {
160
+ sess.services.auth_manager.auth().await
161
+ diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs
162
+ index a25dd60..bdd0c03 100644
163
+ --- a/codex-rs/core/src/session/turn_context.rs
164
+ +++ b/codex-rs/core/src/session/turn_context.rs
165
+ @@ -1311,7 +1311,9 @@ impl Session {
166
+ .plugins_manager
167
+ .plugins_for_config(&plugins_input)
168
+ .await
169
+ - .without_plugins(&session_configuration.disabled_plugin_ids);
170
+ + .without_plugins(
171
+ + &self.peer_disabled_plugins(&session_configuration.disabled_plugin_ids),
172
+ + );
173
+ let trusted_plugin_roots = TrustedPluginRoots::from_plugin_load_outcome(
174
+ &plugin_outcome,
175
+ per_turn_config.codex_home.as_path(),
176
+ diff --git a/codex-rs/ext/memories/src/extension.rs b/codex-rs/ext/memories/src/extension.rs
177
+ index 3d68e6f..f58f2af 100644
178
+ --- a/codex-rs/ext/memories/src/extension.rs
179
+ +++ b/codex-rs/ext/memories/src/extension.rs
180
+ @@ -1,6 +1,7 @@
181
+ use std::sync::Arc;
182
+
183
+ use codex_core::config::Config;
184
+ +use codex_core::config::peer_thread_mark;
185
+ use codex_core::context::ContextualUserFragment;
186
+ use codex_core::context::MemoryContextFragment;
187
+ use codex_extension_api::ConfigContributor;
188
+ @@ -63,7 +64,10 @@ impl ContextContributor for MemoriesExtension {
189
+ let Some(config) = thread_store.get::<MemoriesExtensionConfig>() else {
190
+ return Vec::new();
191
+ };
192
+ - if !config.enabled {
193
+ + if !config.enabled
194
+ + || peer_thread_mark::mode(thread_store.level_id())
195
+ + == peer_thread_mark::Mode::Jellyfish
196
+ + {
197
+ return Vec::new();
198
+ }
199
+
200
+ @@ -145,7 +149,10 @@ impl ToolContributor for MemoriesExtension {
201
+ let Some(config) = thread_store.get::<MemoriesExtensionConfig>() else {
202
+ return Vec::new();
203
+ };
204
+ - if !config.enabled || !config.dedicated_tools {
205
+ + if !config.enabled
206
+ + || !config.dedicated_tools
207
+ + || peer_thread_mark::mode(thread_store.level_id()) == peer_thread_mark::Mode::Jellyfish
208
+ + {
209
+ return Vec::new();
210
+ }
211
+
212
+ diff --git a/codex-rs/hooks/src/engine/dispatcher.rs b/codex-rs/hooks/src/engine/dispatcher.rs
213
+ index cc45564..a2d750b 100644
214
+ --- a/codex-rs/hooks/src/engine/dispatcher.rs
215
+ +++ b/codex-rs/hooks/src/engine/dispatcher.rs
216
+ @@ -211,6 +211,49 @@ pub(super) async fn execute_handler(
217
+ ) -> HandlerRunResult {
218
+ match &handler.kind {
219
+ ConfiguredHandlerKind::Command { command, env, .. } => {
220
+ + if let Some(home) = codex_config::peer_thread_mark::home()
221
+ + && env
222
+ + .get("PLUGIN_ROOT")
223
+ + .is_some_and(|root| super::peer_memory::is_openviking(root))
224
+ + {
225
+ + let mut fallback = event_input.clone();
226
+ + fallback["transcript_path"] = Value::Null;
227
+ + if let Some(path) = fallback.get_mut("agent_transcript_path") {
228
+ + *path = Value::Null;
229
+ + }
230
+ + let input = event_input.clone();
231
+ + let projection_home = home.clone();
232
+ + let projected = tokio::task::spawn_blocking(move || {
233
+ + super::peer_memory::project(&projection_home, input)
234
+ + })
235
+ + .await
236
+ + .unwrap_or(fallback);
237
+ + let mut env = env.clone();
238
+ + let enabled = if event_input["session_id"].as_str().is_some_and(|id| {
239
+ + codex_config::peer_thread_mark::mode_at(&home, id)
240
+ + == codex_config::peer_thread_mark::Mode::Elephant
241
+ + }) {
242
+ + "1"
243
+ + } else {
244
+ + "0"
245
+ + };
246
+ + env.insert("OPENVIKING_AUTO_RECALL".into(), enabled.into());
247
+ + env.insert("OPENVIKING_AUTO_CAPTURE".into(), enabled.into());
248
+ + // The startup sweep must not see state from an unfiltered Codex run.
249
+ + env.insert(
250
+ + "OPENVIKING_CODEX_STATE_DIR".to_string(),
251
+ + home.join("codex-capture/openviking").display().to_string(),
252
+ + );
253
+ + return run_command(
254
+ + &engine.command_runtime,
255
+ + handler,
256
+ + command,
257
+ + &env,
258
+ + &projected.to_string(),
259
+ + cwd,
260
+ + )
261
+ + .await;
262
+ + }
263
+ run_command(
264
+ &engine.command_runtime,
265
+ handler,
266
+ diff --git a/codex-rs/hooks/src/engine/mod.rs b/codex-rs/hooks/src/engine/mod.rs
267
+ index 5e3daf5..b83cf63 100644
268
+ --- a/codex-rs/hooks/src/engine/mod.rs
269
+ +++ b/codex-rs/hooks/src/engine/mod.rs
270
+ @@ -4,6 +4,7 @@ pub(crate) mod dispatcher;
271
+ mod matcher;
272
+ pub(crate) mod mcp_runner;
273
+ pub(crate) mod output_parser;
274
+ +mod peer_memory;
275
+ mod plugin_fingerprint;
276
+ mod plugin_trust;
277
+ pub(crate) mod schema_loader;
278
+ diff --git a/codex-rs/memories/write/src/rollout_input.rs b/codex-rs/memories/write/src/rollout_input.rs
279
+ index fa86e25..1c22b55 100644
280
+ --- a/codex-rs/memories/write/src/rollout_input.rs
281
+ +++ b/codex-rs/memories/write/src/rollout_input.rs
282
+ @@ -42,13 +42,18 @@ pub(crate) async fn read_recent_rollout_items(path: &Path) -> anyhow::Result<Vec
283
+ let mut header = String::new();
284
+ std::io::BufReader::new((&mut reader).take(MAX_RECORD_BYTES as u64))
285
+ .read_line(&mut header)?;
286
+ - anyhow::ensure!(
287
+ - matches!(
288
+ - codex_rollout::parse_rollout_line(&header)?.item,
289
+ - RolloutItem::SessionMeta(_)
290
+ - ),
291
+ - "memory extraction requires valid session metadata within the record limit"
292
+ - );
293
+ + let RolloutItem::SessionMeta(session) = codex_rollout::parse_rollout_line(&header)?.item
294
+ + else {
295
+ + anyhow::bail!(
296
+ + "memory extraction requires valid session metadata within the record limit"
297
+ + );
298
+ + };
299
+ + let windows = codex_config::peer_thread_mark::home().map_or_else(Vec::new, |home| {
300
+ + codex_config::peer_thread_mark::windows(&home, &session.meta.id.to_string())
301
+ + });
302
+ + let has_jellyfish = windows
303
+ + .iter()
304
+ + .any(|window| window.mode == codex_config::peer_thread_mark::Mode::Jellyfish);
305
+ let mut scanner = ReverseJsonlScanner::new(reader)?.with_max_record_bytes(MAX_RECORD_BYTES);
306
+ let mut remaining = MAX_ROLLOUT_BYTES;
307
+ let mut items = Vec::new();
308
+ @@ -59,6 +64,19 @@ pub(crate) async fn read_recent_rollout_items(path: &Path) -> anyhow::Result<Vec
309
+ let ScanOutcome::Parsed(mut line) = outcome else {
310
+ continue;
311
+ };
312
+ + if has_jellyfish
313
+ + && chrono::DateTime::parse_from_rfc3339(&line.timestamp)
314
+ + .ok()
315
+ + .is_none_or(|at| {
316
+ + codex_config::peer_thread_mark::contains(
317
+ + &windows,
318
+ + codex_config::peer_thread_mark::Mode::Jellyfish,
319
+ + at.timestamp_millis(),
320
+ + )
321
+ + })
322
+ + {
323
+ + continue;
324
+ + }
325
+ if !matches!(
326
+ &line.item,
327
+ RolloutItem::ResponseItem(_) | RolloutItem::InterAgentCommunication(_)
@@ -0,0 +1,308 @@
1
+ diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
2
+ index fbc1090..96690b2 100644
3
+ --- a/codex-rs/tui/src/app.rs
4
+ +++ b/codex-rs/tui/src/app.rs
5
+ @@ -239,6 +239,7 @@ mod managed_worktree_creation;
6
+ mod misalignment_policy;
7
+ mod model_defaults;
8
+ mod new_session;
9
+ +mod peer_thread_mark;
10
+ mod quick_dials;
11
+ mod turn_tips;
12
+ pub(crate) use new_session::has_launch_setting;
13
+ diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs
14
+ index 3d31b74..5a4fc05 100644
15
+ --- a/codex-rs/tui/src/app/event_dispatch.rs
16
+ +++ b/codex-rs/tui/src/app/event_dispatch.rs
17
+ @@ -2832,6 +2832,12 @@ impl App {
18
+ AppEvent::ResetMemories => {
19
+ self.reset_memories_with_app_server(app_server).await;
20
+ }
21
+ + AppEvent::PeerThreadMark { thread_id, action } => {
22
+ + self.peer_thread_request(app_server, thread_id, action).await;
23
+ + }
24
+ + AppEvent::PeerThreadMarkResult { thread_id, action, result } => {
25
+ + self.peer_thread_result(app_server, thread_id, action, result).await;
26
+ + }
27
+ AppEvent::UpdateRateLimitSwitchPromptHidden(hidden) => {
28
+ self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden);
29
+ }
30
+ diff --git a/codex-rs/tui/src/app/owned_transcript.rs b/codex-rs/tui/src/app/owned_transcript.rs
31
+ index 312185e..a73d4fe 100644
32
+ --- a/codex-rs/tui/src/app/owned_transcript.rs
33
+ +++ b/codex-rs/tui/src/app/owned_transcript.rs
34
+ @@ -461,6 +461,9 @@ impl App {
35
+ let size = tui.prepare_draw_size()?;
36
+ self.render_owned_transcript(tui, size)?;
37
+ }
38
+ + if self.chat_widget.handle_peer_mouse(*mouse) {
39
+ + return Ok(true);
40
+ + }
41
+ if self.chat_widget.no_modal_or_popup_active()
42
+ && self
43
+ .chat_widget
44
+ diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
45
+ index 2adc303..032bcc0 100644
46
+ --- a/codex-rs/tui/src/app_event.rs
47
+ +++ b/codex-rs/tui/src/app_event.rs
48
+ @@ -1500,6 +1500,16 @@ pub(crate) enum AppEvent {
49
+ /// Clear all persisted local memory artifacts via the app-server.
50
+ ResetMemories,
51
+
52
+ + PeerThreadMark {
53
+ + thread_id: ThreadId,
54
+ + action: crate::chatwidget::peer_thread_mark::PeerAction,
55
+ + },
56
+ + PeerThreadMarkResult {
57
+ + thread_id: ThreadId,
58
+ + action: crate::chatwidget::peer_thread_mark::PeerAction,
59
+ + result: Result<crate::chatwidget::peer_thread_mark::PeerReply, String>,
60
+ + },
61
+ +
62
+ /// Update whether the rate limit switch prompt has been acknowledged for the session.
63
+ UpdateRateLimitSwitchPromptHidden(bool),
64
+
65
+ diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
66
+ index 68868be..9bdc5ab 100644
67
+ --- a/codex-rs/tui/src/chatwidget.rs
68
+ +++ b/codex-rs/tui/src/chatwidget.rs
69
+ @@ -327,6 +327,7 @@ mod model_command;
70
+ mod model_popup_state;
71
+ mod model_popups;
72
+ mod notifications;
73
+ +pub(crate) mod peer_thread_mark;
74
+ pub(crate) mod quick_dials;
75
+ mod session_model_selection;
76
+ use self::notifications::Notification;
77
+ @@ -681,6 +682,7 @@ pub(crate) struct ChatWidget {
78
+ pet_image_support_override: Option<crate::pets::PetImageSupport>,
79
+ thread_id: Option<ThreadId>,
80
+ thread_name: Option<String>,
81
+ + peer_mark: peer_thread_mark::PeerBandState,
82
+ thread_rename_block_message: Option<String>,
83
+ active_side_conversation: bool,
84
+ blocks_direct_input: bool,
85
+ diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs
86
+ index 7b2d08c..5f6101d 100644
87
+ --- a/codex-rs/tui/src/chatwidget/constructor.rs
88
+ +++ b/codex-rs/tui/src/chatwidget/constructor.rs
89
+ @@ -263,6 +263,7 @@ impl ChatWidget {
90
+ last_rendered_width: std::cell::Cell::new(None),
91
+ feedback,
92
+ current_rollout_path: None,
93
+ + peer_mark: Default::default(),
94
+ current_cwd,
95
+ workspace_command_runner,
96
+ instruction_source_paths: Vec::new(),
97
+ diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs
98
+ index be7da50..2f7d2f8 100644
99
+ --- a/codex-rs/tui/src/chatwidget/protocol.rs
100
+ +++ b/codex-rs/tui/src/chatwidget/protocol.rs
101
+ @@ -444,6 +444,9 @@ impl ChatWidget {
102
+ let mut question_drafts = None;
103
+ let live_turn =
104
+ replay_kind.is_none() && self.last_turn_id() == Some(notification.turn.id.as_str());
105
+ + if live_turn {
106
+ + self.request_peer_action(peer_thread_mark::PeerAction::Refresh);
107
+ + }
108
+ let mut recourse = self.safety_recourse.take_if(|_| live_turn);
109
+ let was_replaying_turn_completion = self.thread_usage.replaying_turn_completion;
110
+ self.thread_usage.replaying_turn_completion = replay_kind.is_some();
111
+ diff --git a/codex-rs/tui/src/chatwidget/rendering.rs b/codex-rs/tui/src/chatwidget/rendering.rs
112
+ index 4f31674..e2ceac2 100644
113
+ --- a/codex-rs/tui/src/chatwidget/rendering.rs
114
+ +++ b/codex-rs/tui/src/chatwidget/rendering.rs
115
+ @@ -258,17 +258,21 @@ impl ChatWidget {
116
+ options.separate_status_line = options.command_popup_placement
117
+ != crate::bottom_pane::CommandPopupPlacement::AboveComposer;
118
+ let bottom_pane = self.bottom_pane.backdrop_with_options(options);
119
+ + let mut flex = 1;
120
+ let above_composer = if self.quick_dial_menu {
121
+ RenderableItem::Owned(Box::new(self.quick_dial_hud()))
122
+ } else if let Some(pending) = &self.safety_recourse
123
+ && !self.bottom_pane.has_active_view()
124
+ {
125
+ RenderableItem::Borrowed(&pending.notice)
126
+ + } else if let Some(band) = self.peer_band() {
127
+ + flex = 0;
128
+ + RenderableItem::Owned(Box::new(band))
129
+ } else {
130
+ return bottom_pane;
131
+ };
132
+ let mut column = FlexRenderable::new();
133
+ - column.push(/*flex*/ 1, above_composer);
134
+ + column.push(flex, above_composer);
135
+ column.push(/*flex*/ 0, bottom_pane);
136
+ RenderableItem::Owned(Box::new(column))
137
+ }
138
+ diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs
139
+ index a1ff412..9b55d39 100644
140
+ --- a/codex-rs/tui/src/chatwidget/session_flow.rs
141
+ +++ b/codex-rs/tui/src/chatwidget/session_flow.rs
142
+ @@ -135,6 +135,7 @@ impl ChatWidget {
143
+ self.current_rollout_path = session.rollout_path.clone();
144
+ self.current_cwd = Some(session.cwd.to_path_buf());
145
+ self.config.cwd = session.cwd.clone();
146
+ + self.reset_peer_band();
147
+ self.config.model_provider_id = session.model_provider_id.clone();
148
+ self.set_daybreak_enabled(self.daybreak_enabled);
149
+ if connector_scope_changed {
150
+ @@ -417,6 +418,7 @@ impl ChatWidget {
151
+ ) {
152
+ if self.thread_id == Some(thread_id) {
153
+ self.thread_name = thread_name;
154
+ + self.restore_peer_title();
155
+ self.refresh_status_surfaces();
156
+ self.request_redraw();
157
+ self.maybe_send_next_queued_input();
158
+ diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs
159
+ index 79a9018..91c2c4f 100644
160
+ --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs
161
+ +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs
162
+ @@ -304,6 +304,7 @@ impl ChatWidget {
163
+ self.request_redraw();
164
+ }
165
+ SlashCommand::Clear => {
166
+ + self.stop_peer_before_clear();
167
+ self.app_event_tx.send(AppEvent::ClearUi { name: None });
168
+ }
169
+ SlashCommand::Resume => {
170
+ @@ -483,10 +484,22 @@ impl ChatWidget {
171
+ SlashCommand::Memories => {
172
+ self.open_memories_popup();
173
+ }
174
+ + SlashCommand::Peer => {
175
+ + self.request_peer_action(peer_thread_mark::PeerAction::Start(None))
176
+ + }
177
+ + SlashCommand::PeerMode => self.open_peer_modes(),
178
+ + SlashCommand::PeerList => self.request_peer_action(peer_thread_mark::PeerAction::List),
179
+ + SlashCommand::PeerThread => {
180
+ + self.request_peer_action(peer_thread_mark::PeerAction::Thread)
181
+ + }
182
+ + SlashCommand::PeerAll => {
183
+ + self.request_peer_action(peer_thread_mark::PeerAction::All(String::new()))
184
+ + }
185
+ SlashCommand::Quit | SlashCommand::Exit => {
186
+ self.request_quit_without_confirmation();
187
+ }
188
+ SlashCommand::Logout => {
189
+ + self.stop_peer_before_clear();
190
+ self.app_event_tx.send(AppEvent::Logout);
191
+ }
192
+ SlashCommand::Copy => {
193
+ @@ -805,6 +818,27 @@ impl ChatWidget {
194
+ return;
195
+ }
196
+ match cmd {
197
+ + SlashCommand::Peer => {
198
+ + use peer_thread_mark::PeerAction;
199
+ + let action = match trimmed {
200
+ + "start" => PeerAction::Start(None),
201
+ + "stop" => PeerAction::Stop,
202
+ + "elephant" => PeerAction::Elephant,
203
+ + "jellyfish" => PeerAction::Jellyfish,
204
+ + _ => PeerAction::Start(Some(self.prepared_inline_user_message(
205
+ + args,
206
+ + text_elements,
207
+ + local_images,
208
+ + remote_image_urls,
209
+ + mention_bindings,
210
+ + source,
211
+ + ))),
212
+ + };
213
+ + self.request_peer_action(action);
214
+ + }
215
+ + SlashCommand::PeerAll => {
216
+ + self.request_peer_action(peer_thread_mark::PeerAction::All(args))
217
+ + }
218
+ SlashCommand::Model if !trimmed.is_empty() => {
219
+ self.handle_model_command_args(trimmed);
220
+ self.app_event_tx.send(AppEvent::SettingsSelectionClosed);
221
+ @@ -933,6 +967,7 @@ impl ChatWidget {
222
+ );
223
+ }
224
+ SlashCommand::Clear if !trimmed.is_empty() => {
225
+ + self.stop_peer_before_clear();
226
+ self.app_event_tx.send(AppEvent::ClearUi {
227
+ name: Some(trimmed.to_string()),
228
+ });
229
+ @@ -1350,6 +1385,11 @@ impl ChatWidget {
230
+ | SlashCommand::Experimental
231
+ | SlashCommand::AutoReview
232
+ | SlashCommand::Memories
233
+ + | SlashCommand::Peer
234
+ + | SlashCommand::PeerMode
235
+ + | SlashCommand::PeerList
236
+ + | SlashCommand::PeerThread
237
+ + | SlashCommand::PeerAll
238
+ | SlashCommand::Quit
239
+ | SlashCommand::Exit
240
+ | SlashCommand::Logout
241
+ diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs
242
+ index dfd0538..5858bca 100644
243
+ --- a/codex-rs/tui/src/slash_command.rs
244
+ +++ b/codex-rs/tui/src/slash_command.rs
245
+ @@ -25,6 +25,11 @@ pub enum SlashCommand {
246
+ #[strum(to_string = "approve")]
247
+ AutoReview,
248
+ Memories,
249
+ + Peer,
250
+ + PeerMode,
251
+ + PeerList,
252
+ + PeerThread,
253
+ + PeerAll,
254
+ Skills,
255
+ Import,
256
+ Hooks,
257
+ @@ -153,6 +158,11 @@ impl SlashCommand {
258
+ SlashCommand::Experimental => "toggle experimental features",
259
+ SlashCommand::AutoReview => "approve one retry of a recent auto-review denial",
260
+ SlashCommand::Memories => "configure memory use and generation",
261
+ + SlashCommand::Peer => "share this thread, or stop sharing",
262
+ + SlashCommand::PeerMode => "choose Elephant, Jellyfish, or Inactive",
263
+ + SlashCommand::PeerList => "browse the workspace's shared threads",
264
+ + SlashCommand::PeerThread => "browse memories saved by this thread",
265
+ + SlashCommand::PeerAll => "search your OpenViking memories",
266
+ SlashCommand::Mcp => "list MCP tools; use /mcp verbose or /mcp login <name>",
267
+ SlashCommand::Apps => "manage apps",
268
+ SlashCommand::Plugins => "browse plugins",
269
+ @@ -173,6 +183,8 @@ impl SlashCommand {
270
+ matches!(
271
+ self,
272
+ SlashCommand::Review
273
+ + | SlashCommand::Peer
274
+ + | SlashCommand::PeerAll
275
+ | SlashCommand::Model
276
+ | SlashCommand::Permissions
277
+ | SlashCommand::Title
278
+ @@ -262,6 +274,8 @@ impl SlashCommand {
279
+ | SlashCommand::ElevateSandbox
280
+ | SlashCommand::Experimental
281
+ | SlashCommand::Memories
282
+ + | SlashCommand::Peer
283
+ + | SlashCommand::PeerMode
284
+ | SlashCommand::Import
285
+ | SlashCommand::Review
286
+ | SlashCommand::Plan
287
+ @@ -297,6 +311,9 @@ impl SlashCommand {
288
+ | SlashCommand::Mcp
289
+ | SlashCommand::Apps
290
+ | SlashCommand::Plugins
291
+ + | SlashCommand::PeerList
292
+ + | SlashCommand::PeerThread
293
+ + | SlashCommand::PeerAll
294
+ | SlashCommand::Title
295
+ | SlashCommand::Statusline
296
+ | SlashCommand::AutoReview
297
+ @@ -318,6 +335,11 @@ impl SlashCommand {
298
+ SlashCommand::Copy => !cfg!(target_os = "android"),
299
+ SlashCommand::App => cfg!(any(target_os = "macos", target_os = "windows")),
300
+ SlashCommand::Voice => true,
301
+ + SlashCommand::Peer
302
+ + | SlashCommand::PeerMode
303
+ + | SlashCommand::PeerList
304
+ + | SlashCommand::PeerThread
305
+ + | SlashCommand::PeerAll => codex_config::peer_thread_mark::home().is_some(),
306
+ SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions),
307
+ _ => true,
308
+ }