summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-08-15 10:16:08 -0500
committeradamdotdevin <[email protected]>2025-08-15 10:16:08 -0500
commit07dbc30c637cb500c58c887bfbc8857ae5a8f44e (patch)
tree0fa38250363a3489e16a5504af442c33c1203a03 /packages
parent1ae38c90a3192849ac843a9729d749f4baf3b35b (diff)
downloadopencode-07dbc30c637cb500c58c887bfbc8857ae5a8f44e.tar.gz
opencode-07dbc30c637cb500c58c887bfbc8857ae5a8f44e.zip
feat(tui): navigate child sessions (subagents)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/config/config.ts6
-rw-r--r--packages/opencode/src/server/server.ts12
-rw-r--r--packages/opencode/src/session/index.ts4
-rw-r--r--packages/opencode/src/tool/task.ts6
-rw-r--r--packages/tui/internal/commands/command.go78
-rw-r--r--packages/tui/internal/components/chat/message.go24
-rw-r--r--packages/tui/internal/components/chat/messages.go66
-rw-r--r--packages/tui/internal/tui/tui.go154
-rw-r--r--packages/web/src/content/docs/docs/agents.mdx7
-rw-r--r--packages/web/src/content/docs/docs/keybinds.mdx2
10 files changed, 294 insertions, 65 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index b625dfd4f..1b30bc6b5 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -218,6 +218,12 @@ export namespace Config {
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
+ session_child_cycle: z.string().optional().default("ctrl+right").describe("Cycle to next child session"),
+ session_child_cycle_reverse: z
+ .string()
+ .optional()
+ .default("ctrl+left")
+ .describe("Cycle to previous child session"),
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 0b1f91679..e661471ae 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -293,8 +293,18 @@ export namespace Server {
},
},
}),
+ zValidator(
+ "json",
+ z
+ .object({
+ parentID: z.string().optional(),
+ title: z.string().optional(),
+ })
+ .optional(),
+ ),
async (c) => {
- const session = await Session.create()
+ const body = c.req.valid("json") ?? {}
+ const session = await Session.create(body.parentID, body.title)
return c.json(session)
},
)
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 843b1f02d..1173de0d5 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -163,12 +163,12 @@ export namespace Session {
},
)
- export async function create(parentID?: string) {
+ export async function create(parentID?: string, title?: string) {
const result: Info = {
id: Identifier.descending("session"),
version: Installation.VERSION,
parentID,
- title: createDefaultTitle(!!parentID),
+ title: title ?? createDefaultTitle(!!parentID),
time: {
created: Date.now(),
updated: Date.now(),
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 0b518b2ba..a959611e6 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -23,11 +23,11 @@ export const TaskTool = Tool.define("task", async () => {
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
}),
async execute(params, ctx) {
- const session = await Session.create(ctx.sessionID)
- const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
- if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
const agent = await Agent.get(params.subagent_type)
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
+ const session = await Session.create(ctx.sessionID, params.description + ` (@${agent.name} subagent)`)
+ const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
+ if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index 64a78acf1..70a267a52 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -107,39 +107,41 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
}
const (
- AppHelpCommand CommandName = "app_help"
- AppExitCommand CommandName = "app_exit"
- ThemeListCommand CommandName = "theme_list"
- ProjectInitCommand CommandName = "project_init"
- EditorOpenCommand CommandName = "editor_open"
- ToolDetailsCommand CommandName = "tool_details"
- ThinkingBlocksCommand CommandName = "thinking_blocks"
- SessionNewCommand CommandName = "session_new"
- SessionListCommand CommandName = "session_list"
- SessionShareCommand CommandName = "session_share"
- SessionUnshareCommand CommandName = "session_unshare"
- SessionInterruptCommand CommandName = "session_interrupt"
- SessionCompactCommand CommandName = "session_compact"
- SessionExportCommand CommandName = "session_export"
- MessagesPageUpCommand CommandName = "messages_page_up"
- MessagesPageDownCommand CommandName = "messages_page_down"
- MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
- MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
- MessagesFirstCommand CommandName = "messages_first"
- MessagesLastCommand CommandName = "messages_last"
- MessagesCopyCommand CommandName = "messages_copy"
- MessagesUndoCommand CommandName = "messages_undo"
- MessagesRedoCommand CommandName = "messages_redo"
- ModelListCommand CommandName = "model_list"
- ModelCycleRecentCommand CommandName = "model_cycle_recent"
- ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse"
- AgentListCommand CommandName = "agent_list"
- AgentCycleCommand CommandName = "agent_cycle"
- AgentCycleReverseCommand CommandName = "agent_cycle_reverse"
- InputClearCommand CommandName = "input_clear"
- InputPasteCommand CommandName = "input_paste"
- InputSubmitCommand CommandName = "input_submit"
- InputNewlineCommand CommandName = "input_newline"
+ AppHelpCommand CommandName = "app_help"
+ AppExitCommand CommandName = "app_exit"
+ ThemeListCommand CommandName = "theme_list"
+ ProjectInitCommand CommandName = "project_init"
+ EditorOpenCommand CommandName = "editor_open"
+ ToolDetailsCommand CommandName = "tool_details"
+ ThinkingBlocksCommand CommandName = "thinking_blocks"
+ SessionNewCommand CommandName = "session_new"
+ SessionListCommand CommandName = "session_list"
+ SessionShareCommand CommandName = "session_share"
+ SessionUnshareCommand CommandName = "session_unshare"
+ SessionInterruptCommand CommandName = "session_interrupt"
+ SessionCompactCommand CommandName = "session_compact"
+ SessionExportCommand CommandName = "session_export"
+ SessionChildCycleCommand CommandName = "session_child_cycle"
+ SessionChildCycleReverseCommand CommandName = "session_child_cycle_reverse"
+ MessagesPageUpCommand CommandName = "messages_page_up"
+ MessagesPageDownCommand CommandName = "messages_page_down"
+ MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
+ MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
+ MessagesFirstCommand CommandName = "messages_first"
+ MessagesLastCommand CommandName = "messages_last"
+ MessagesCopyCommand CommandName = "messages_copy"
+ MessagesUndoCommand CommandName = "messages_undo"
+ MessagesRedoCommand CommandName = "messages_redo"
+ ModelListCommand CommandName = "model_list"
+ ModelCycleRecentCommand CommandName = "model_cycle_recent"
+ ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse"
+ AgentListCommand CommandName = "agent_list"
+ AgentCycleCommand CommandName = "agent_cycle"
+ AgentCycleReverseCommand CommandName = "agent_cycle_reverse"
+ InputClearCommand CommandName = "input_clear"
+ InputPasteCommand CommandName = "input_paste"
+ InputSubmitCommand CommandName = "input_submit"
+ InputNewlineCommand CommandName = "input_newline"
)
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
@@ -225,6 +227,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Trigger: []string{"compact", "summarize"},
},
{
+ Name: SessionChildCycleCommand,
+ Description: "cycle to next child session",
+ Keybindings: parseBindings("ctrl+right"),
+ },
+ {
+ Name: SessionChildCycleReverseCommand,
+ Description: "cycle to previous child session",
+ Keybindings: parseBindings("ctrl+left"),
+ },
+ {
Name: ToolDetailsCommand,
Description: "toggle tool details",
Keybindings: parseBindings("<leader>d"),
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 46dd02d14..eecfe2611 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -14,6 +14,7 @@ import (
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
+ "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/diff"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -479,6 +480,8 @@ func renderToolDetails(
backgroundColor := t.BackgroundPanel()
borderColor := t.BackgroundPanel()
defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render
+ baseStyle := styles.NewStyle().Background(backgroundColor).Foreground(t.Text()).Render
+ mutedStyle := styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render
permissionContent := ""
if permission.ID != "" {
@@ -602,14 +605,15 @@ func renderToolDetails(
}
}
case "bash":
- command := toolInputMap["command"].(string)
- body = fmt.Sprintf("```console\n$ %s\n", command)
- output := metadata["output"]
- if output != nil {
- body += ansi.Strip(fmt.Sprintf("%s", output))
+ if command, ok := toolInputMap["command"].(string); ok {
+ body = fmt.Sprintf("```console\n$ %s\n", command)
+ output := metadata["output"]
+ if output != nil {
+ body += ansi.Strip(fmt.Sprintf("%s", output))
+ }
+ body += "```"
+ body = util.ToMarkdown(body, width, backgroundColor)
}
- body += "```"
- body = util.ToMarkdown(body, width, backgroundColor)
case "webfetch":
if format, ok := toolInputMap["format"].(string); ok && result != nil {
body = *result
@@ -653,6 +657,12 @@ func renderToolDetails(
steps = append(steps, step)
}
body = strings.Join(steps, "\n")
+
+ body += "\n\n"
+ body += baseStyle(app.Keybind(commands.SessionChildCycleCommand)) +
+ mutedStyle(", ") +
+ baseStyle(app.Keybind(commands.SessionChildCycleReverseCommand)) +
+ mutedStyle(" navigate child sessions")
}
body = defaultStyle(body)
default:
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 1c6bce904..109a734ad 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -180,6 +180,8 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.tail = true
return m, m.renderView()
}
+ case app.SessionSelectedMsg:
+ m.viewport.GotoBottom()
case app.MessageRevertedMsg:
if msg.Session.ID == m.app.Session.ID {
m.cache.Clear()
@@ -782,8 +784,17 @@ func (m *messagesComponent) renderHeader() string {
headerWidth := m.width
t := theme.CurrentTheme()
- base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
- muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
+ bgColor := t.Background()
+ borderColor := t.BackgroundElement()
+
+ isChildSession := m.app.Session.ParentID != ""
+ if isChildSession {
+ bgColor = t.BackgroundElement()
+ borderColor = t.Accent()
+ }
+
+ base := styles.NewStyle().Foreground(t.Text()).Background(bgColor).Render
+ muted := styles.NewStyle().Foreground(t.TextMuted()).Background(bgColor).Render
sessionInfo := ""
tokens := float64(0)
@@ -815,20 +826,44 @@ func (m *messagesComponent) renderHeader() string {
sessionInfoText := formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)
sessionInfo = styles.NewStyle().
Foreground(t.TextMuted()).
- Background(t.Background()).
+ Background(bgColor).
Render(sessionInfoText)
shareEnabled := m.app.Config.Share != opencode.ConfigShareDisabled
+
+ navHint := ""
+ if isChildSession {
+ navHint = base(" "+m.app.Keybind(commands.SessionChildCycleReverseCommand)) + muted(" back")
+ }
+
headerTextWidth := headerWidth
- if !shareEnabled {
- // +1 is to ensure there is always at least one space between header and session info
- headerTextWidth -= len(sessionInfoText) + 1
+ if isChildSession {
+ headerTextWidth -= lipgloss.Width(navHint)
+ } else if !shareEnabled {
+ headerTextWidth -= lipgloss.Width(sessionInfoText)
}
headerText := util.ToMarkdown(
"# "+m.app.Session.Title,
headerTextWidth,
- t.Background(),
+ bgColor,
)
+ if isChildSession {
+ headerText = layout.Render(
+ layout.FlexOptions{
+ Background: &bgColor,
+ Direction: layout.Row,
+ Justify: layout.JustifySpaceBetween,
+ Align: layout.AlignStretch,
+ Width: headerTextWidth,
+ },
+ layout.FlexItem{
+ View: headerText,
+ },
+ layout.FlexItem{
+ View: navHint,
+ },
+ )
+ }
var items []layout.FlexItem
if shareEnabled {
@@ -841,10 +876,9 @@ func (m *messagesComponent) renderHeader() string {
items = []layout.FlexItem{{View: headerText}, {View: sessionInfo}}
}
- background := t.Background()
headerRow := layout.Render(
layout.FlexOptions{
- Background: &background,
+ Background: &bgColor,
Direction: layout.Row,
Justify: layout.JustifySpaceBetween,
Align: layout.AlignStretch,
@@ -860,14 +894,14 @@ func (m *messagesComponent) renderHeader() string {
header := strings.Join(headerLines, "\n")
header = styles.NewStyle().
- Background(t.Background()).
+ Background(bgColor).
Width(headerWidth).
PaddingLeft(2).
PaddingRight(2).
BorderLeft(true).
BorderRight(true).
BorderBackground(t.Background()).
- BorderForeground(t.BackgroundElement()).
+ BorderForeground(borderColor).
BorderStyle(lipgloss.ThickBorder()).
Render(header)
@@ -914,7 +948,7 @@ func formatTokensAndCost(
formattedCost := fmt.Sprintf("$%.2f", cost)
return fmt.Sprintf(
- "%s/%d%% (%s)",
+ " %s/%d%% (%s)",
formattedTokens,
int(percentage),
formattedCost,
@@ -923,20 +957,22 @@ func formatTokensAndCost(
func (m *messagesComponent) View() string {
t := theme.CurrentTheme()
+ bgColor := t.Background()
+
if m.loading {
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
- styles.NewStyle().Background(t.Background()).Render(""),
- styles.WhitespaceStyle(t.Background()),
+ styles.NewStyle().Background(bgColor).Render(""),
+ styles.WhitespaceStyle(bgColor),
)
}
viewport := m.viewport.View()
return styles.NewStyle().
- Background(t.Background()).
+ Background(bgColor).
Render(m.header + "\n" + viewport)
}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 97e5527bc..3b543fc52 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -391,11 +391,41 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, toast.NewErrorToast(msg.Error())
case app.SendPrompt:
a.showCompletionDialog = false
- a.app, cmd = a.app.SendPrompt(context.Background(), msg)
- cmds = append(cmds, cmd)
+ // If we're in a child session, switch back to parent before sending prompt
+ if a.app.Session.ParentID != "" {
+ parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
+ if err != nil {
+ slog.Error("Failed to get parent session", "error", err)
+ return a, toast.NewErrorToast("Failed to get parent session")
+ }
+ a.app.Session = parentSession
+ a.app, cmd = a.app.SendPrompt(context.Background(), msg)
+ cmds = append(cmds, tea.Sequence(
+ util.CmdHandler(app.SessionSelectedMsg(parentSession)),
+ cmd,
+ ))
+ } else {
+ a.app, cmd = a.app.SendPrompt(context.Background(), msg)
+ cmds = append(cmds, cmd)
+ }
case app.SendShell:
- a.app, cmd = a.app.SendShell(context.Background(), msg.Command)
- cmds = append(cmds, cmd)
+ // If we're in a child session, switch back to parent before sending prompt
+ if a.app.Session.ParentID != "" {
+ parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
+ if err != nil {
+ slog.Error("Failed to get parent session", "error", err)
+ return a, toast.NewErrorToast("Failed to get parent session")
+ }
+ a.app.Session = parentSession
+ a.app, cmd = a.app.SendShell(context.Background(), msg.Command)
+ cmds = append(cmds, tea.Sequence(
+ util.CmdHandler(app.SessionSelectedMsg(parentSession)),
+ cmd,
+ ))
+ } else {
+ a.app, cmd = a.app.SendShell(context.Background(), msg.Command)
+ cmds = append(cmds, cmd)
+ }
case app.SetEditorContentMsg:
// Set the editor content without sending
a.editor.SetValueWithAttachments(msg.Text)
@@ -1111,6 +1141,122 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
}
// TODO: block until compaction is complete
a.app.CompactSession(context.Background())
+ case commands.SessionChildCycleCommand:
+ if a.app.Session.ID == "" {
+ return a, nil
+ }
+ cmds = append(cmds, func() tea.Msg {
+ parentSessionID := a.app.Session.ID
+ var parentSession *opencode.Session
+ if a.app.Session.ParentID != "" {
+ parentSessionID = a.app.Session.ParentID
+ session, err := a.app.Client.Session.Get(context.Background(), parentSessionID)
+ if err != nil {
+ slog.Error("Failed to get parent session", "error", err)
+ return toast.NewErrorToast("Failed to get parent session")
+ }
+ parentSession = session
+ } else {
+ parentSession = a.app.Session
+ }
+
+ children, err := a.app.Client.Session.Children(context.Background(), parentSessionID)
+ if err != nil {
+ slog.Error("Failed to get session children", "error", err)
+ return toast.NewErrorToast("Failed to get session children")
+ }
+
+ // Reverse sort the children (newest first)
+ slices.Reverse(*children)
+
+ // Create combined array: [parent, child1, child2, ...]
+ sessions := []*opencode.Session{parentSession}
+ for i := range *children {
+ sessions = append(sessions, &(*children)[i])
+ }
+
+ if len(sessions) == 1 {
+ return toast.NewInfoToast("No child sessions available")
+ }
+
+ // Find current session index in combined array
+ currentIndex := -1
+ for i, session := range sessions {
+ if session.ID == a.app.Session.ID {
+ currentIndex = i
+ break
+ }
+ }
+
+ // If session not found, default to parent (shouldn't happen)
+ if currentIndex == -1 {
+ currentIndex = 0
+ }
+
+ // Cycle to next session (parent or child)
+ nextIndex := (currentIndex + 1) % len(sessions)
+ nextSession := sessions[nextIndex]
+
+ return app.SessionSelectedMsg(nextSession)
+ })
+ case commands.SessionChildCycleReverseCommand:
+ if a.app.Session.ID == "" {
+ return a, nil
+ }
+ cmds = append(cmds, func() tea.Msg {
+ parentSessionID := a.app.Session.ID
+ var parentSession *opencode.Session
+ if a.app.Session.ParentID != "" {
+ parentSessionID = a.app.Session.ParentID
+ session, err := a.app.Client.Session.Get(context.Background(), parentSessionID)
+ if err != nil {
+ slog.Error("Failed to get parent session", "error", err)
+ return toast.NewErrorToast("Failed to get parent session")
+ }
+ parentSession = session
+ } else {
+ parentSession = a.app.Session
+ }
+
+ children, err := a.app.Client.Session.Children(context.Background(), parentSessionID)
+ if err != nil {
+ slog.Error("Failed to get session children", "error", err)
+ return toast.NewErrorToast("Failed to get session children")
+ }
+
+ // Reverse sort the children (newest first)
+ slices.Reverse(*children)
+
+ // Create combined array: [parent, child1, child2, ...]
+ sessions := []*opencode.Session{parentSession}
+ for i := range *children {
+ sessions = append(sessions, &(*children)[i])
+ }
+
+ if len(sessions) == 1 {
+ return toast.NewInfoToast("No child sessions available")
+ }
+
+ // Find current session index in combined array
+ currentIndex := -1
+ for i, session := range sessions {
+ if session.ID == a.app.Session.ID {
+ currentIndex = i
+ break
+ }
+ }
+
+ // If session not found, default to parent (shouldn't happen)
+ if currentIndex == -1 {
+ currentIndex = 0
+ }
+
+ // Cycle to previous session (parent or child)
+ nextIndex := (currentIndex - 1 + len(sessions)) % len(sessions)
+ nextSession := sessions[nextIndex]
+
+ return app.SessionSelectedMsg(nextSession)
+ })
case commands.SessionExportCommand:
if a.app.Session.ID == "" {
return a, toast.NewErrorToast("No active session to export.")
diff --git a/packages/web/src/content/docs/docs/agents.mdx b/packages/web/src/content/docs/docs/agents.mdx
index beb1b29ae..ce1b885f1 100644
--- a/packages/web/src/content/docs/docs/agents.mdx
+++ b/packages/web/src/content/docs/docs/agents.mdx
@@ -90,6 +90,13 @@ A general-purpose agent for researching complex questions, searching for code, a
@general help me search for this function
```
+3. **Navigation between sessions**: When subagents create their own child sessions, you can navigate between the parent session and all child sessions using:
+
+ - **Ctrl+Right** (or your configured `session_child_cycle` keybind) to cycle forward through parent → child1 → child2 → ... → parent
+ - **Ctrl+Left** (or your configured `session_child_cycle_reverse` keybind) to cycle backward through parent ← child1 ← child2 ← ... ← parent
+
+ This allows you to seamlessly switch between the main conversation and specialized subagent work.
+
---
## Configure
diff --git a/packages/web/src/content/docs/docs/keybinds.mdx b/packages/web/src/content/docs/docs/keybinds.mdx
index 60b9a5cd3..6fd6148e1 100644
--- a/packages/web/src/content/docs/docs/keybinds.mdx
+++ b/packages/web/src/content/docs/docs/keybinds.mdx
@@ -24,6 +24,8 @@ opencode has a list of keybinds that you can customize through the opencode conf
"session_unshare": "none",
"session_interrupt": "esc",
"session_compact": "<leader>c",
+ "session_child_cycle": "ctrl+right",
+ "session_child_cycle_reverse": "ctrl+left",
"messages_page_up": "pgup",
"messages_page_down": "pgdown",
"messages_half_page_up": "ctrl+alt+u",