You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(tui): Show skillset picker on startup when skillset_per_session is enabled (#351)
## Summary
🤖 Generated with [Nori](https://www.npmjs.com/package/nori-ai)
- Remove the `.worktrees/` directory guard from the
`skillset_per_session` startup checks so the skillset picker always
appears when the setting is enabled, regardless of directory context
- Add `on_dismiss` callback to `SelectionViewParams` so dismissing the
picker without selection triggers deferred agent spawn (behaving as if
the feature is disabled)
- Add `SkillsetPickerDismissed` AppEvent variant to handle picker
dismissal gracefully
## Test Plan
- [x] 3 new tests: `on_dismiss_callback_fires_on_ctrl_c`,
`on_dismiss_callback_not_fired_on_accept`,
`test_skillset_picker_dismiss_sends_event`
- [x] All 918 existing tests pass with no regressions
- [x] Clippy passes clean
- [ ] Manual: Enable `skillset_per_session` in config, launch nori
outside a `.worktrees/` directory, verify picker appears
- [ ] Manual: Dismiss the picker (Escape), verify the agent starts
normally
- [ ] Manual: Select a skillset from the picker, verify the agent starts
with the skillset active
Share Nori with your team: https://www.npmjs.com/package/nori-ai
Co-authored-by: Nori <contact@tilework.tech>
Copy file name to clipboardExpand all lines: codex-rs/tui/docs.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -136,6 +136,8 @@ During background system info collection on unix, `check_worktree_cleanup()` run
136
136
137
137
The `desc_col` is computed once per render pass from the widest visible name plus 2 columns of padding. The stacked fallback prevents descriptions from being squeezed into 1-2 characters of horizontal space on narrow terminals. Because both `render_rows()` and `measure_rows_height()` call the same `wrap_row()` function, layout and height calculation are always consistent.
138
138
139
+
`SelectionViewParams` supports an optional `on_dismiss: Option<SelectionAction>` callback that fires when the picker is dismissed without selection (Escape or Ctrl-C). The callback is invoked in `ListSelectionView::on_ctrl_c()` before marking the view as complete. It does not fire when the user makes a selection via `accept()`. This is used by the skillset picker to send `SkillsetPickerDismissed` when the deferred agent spawn needs a fallback trigger.
140
+
139
141
**Undo Snapshot Picker (`/undo`):**
140
142
141
143
The `/undo` slash command sends `Op::UndoList` (not `Op::Undo`) to the ACP backend. When the backend responds with `UndoListResult`, the TUI opens a `ListSelectionView` modal (the same pattern used by the approvals popup, etc.) displaying all available snapshots. Each item shows `[short_id] truncated_label` where the label is truncated to 60 characters. Selecting a snapshot dispatches `Op::UndoTo { index }` to restore to that point. If no snapshots are available, an info message is displayed instead of the modal.
@@ -199,9 +201,9 @@ The `/switch-skillset` command integrates with the external `nori-skillsets` CLI
199
201
200
202
The worktree context is detected by `handle_switch_skillset_command()`: if the cwd's parent directory is named `.worktrees`, the cwd is passed as `install_dir`. When `skillset_per_session` is enabled, the cwd is used as `install_dir` even when not in a worktree. This enables per-worktree or per-session skillset installation.
201
203
202
-
When `skillset_per_session` is enabled in `NoriConfig`, the skillset picker is automatically triggered at startup in `App::run()`, regardless of whether the session is in a worktree.
204
+
When `skillset_per_session` is enabled in `NoriConfig`, the skillset picker is automatically triggered at startup in `App::run()`, regardless of whether the session is in a worktree. The agent spawn is deferred (`ChatWidgetInit::deferred_spawn = true`) so that `nori-skillsets switch` can write `.claude/CLAUDE.md` to disk before the agent reads it. During the deferred period, a dummy channel is created in `constructors.rs` so the widget has a valid `op_tx`. The real agent spawns after the user picks a skillset (`SkillsetSwitchResult` triggers `spawn_deferred_agent()`). If the user dismisses the picker without selecting a skillset (Escape/Ctrl-C), the picker's `on_dismiss` callback sends `AppEvent::SkillsetPickerDismissed`, which also triggers `spawn_deferred_agent()` -- the agent starts without a skillset, behaving as if the feature were disabled. The `server_for_deferred_spawn` field on `App` holds the `ConversationManager` until one of these paths consumes it via `.take()`.
The "Per Session Skillsets" toggle in `/config` is built in `nori/config_picker.rs`. Toggling it on emits `AppEvent::OpenSkillsetPerSessionWorktreeChoice`, which opens a worktree choice modal (`skillset_worktree_choice_params()`) letting the user choose between "With Auto Worktrees" and "Without Auto Worktrees". The choice determines whether `auto_worktree` is also enabled. Toggling it off emits `AppEvent::SetConfigSkillsetPerSession`, handled in `app/config_persistence.rs` via `persist_skillset_per_session_setting()` to write `skillset_per_session` under `[tui]` in `config.toml`. The "Auto Worktree" toggle is always independently toggleable regardless of the `skillset_per_session` state.
0 commit comments