#18499 · @anduimagui · opened Mar 21, 2026 at 8:16 AM UTC · last updated Mar 21, 2026 at 8:18 AM UTC

feat(app): add /restart session command

appfeat
51
+157214 files

Score breakdown

Impact

7.0

Clarity

8.0

Urgency

3.0

Ease Of Review

6.0

Guidelines

8.0

Readiness

8.0

Size

4.0

Trust

6.0

Traction

0.0

Summary

This PR adds a /restart session command to the app, which creates a new session seeded with the first user prompt. It includes a new e2e test for verification, but lacks screenshots.

Open in GitHub

Description

Issue for this PR

Closes #18495

Type of change

  • [ ] Bug fix
  • [x] New feature
  • [x] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

Adds a /restart session command that opens a fresh session draft seeded with the first user message from the current session, making it clear that this action goes back to the user's initial query instead of starting a blank session.

/fork still branches from any selected earlier message, and its small helper now stays local to DialogFork so this PR only touches the app behavior needed for /restart.

How did you verify your code works?

I verified the app package locally:

  • bun test
  • bun typecheck
  • bun run build

I also added a restart e2e regression in packages/app/e2e/session/session-restart.spec.ts to cover seeding a new session draft from the first user prompt.

Screenshots / recordings

Not included.

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

Linked Issues

#18495 [FEATURE]: add /restart session command

View issue

Comments

No comments.

Changed Files

packages/app/e2e/session/session-restart.spec.ts

+870
@@ -0,0 +1,87 @@
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { runPromptSlash, withSession } from "../actions"
import { createSdk } from "../utils"
import { promptSelector } from "../selectors"
async function seedConversation(input: {
page: Page
sdk: ReturnType<typeof createSdk>
sessionID: string
token: string
}) {
const messages = async () =>
await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
const seeded = await messages()
const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
await input.sdk.session.promptAsync({
sessionID: input.sessionID,
noReply: true,
parts: [{ type: "text", text: input.token }],
})
let userMessageID: string | undefined
await expect
.poll(
async () => {
const users = (await messages()).filter(
(m) =>
!userIDs.has(m.info.id) &&
m.info.role === "user" &&
m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
)
if (users.length === 0) return f

packages/app/src/components/dialog-fork.tsx

+4521
@@ -2,15 +2,15 @@ import { Component, createMemo } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { usePrompt, type Prompt } from "@/context/prompt"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { showToast } from "@opencode-ai/ui/toast"
import { extractPromptFromParts } from "@/utils/prompt"
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
import { base64Encode } from "@opencode-ai/util/encode"
import { useLanguage } from "@/context/language"
import { extractPromptFromParts } from "@/utils/prompt"
interface ForkableMessage {
id: string
@@ -22,6 +22,37 @@ function formatTime(date: Date): string {
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
}
async function fork(opts: {
fork: (input: { sessionID: string; messageID: string }) => Promise<{ data?: { id: string } }>
sessionID: string
messageID: string
prompt: Prompt
directory: string

packages/app/src/i18n/en.ts

+20
@@ -81,6 +81,8 @@ export const dict = {
"command.session.redo.description": "Redo the last undone message",
"command.session.compact": "Compact session",
"command.session.compact.description": "Summarize the session to reduce context size",
"command.session.restart": "Restart from first prompt",
"command.session.restart.description": "Fork a new session from the user's initial query",
"command.session.fork": "Fork from message",
"command.session.fork.description": "Create a new session from a previous message",
"command.session.share": "Share session",

packages/app/src/pages/session/use-session-commands.tsx

+230
@@ -15,6 +15,7 @@ import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { DialogFork } from "@/components/dialog-fork"
import { promptLength } from "@/components/prompt-input/history"
import { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/util/array"
import { createSessionTabs } from "@/pages/session/helpers"
@@ -94,6 +95,20 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
layout.fileTree.setTab("all")
}
const restart = async () => {
const dir = params.dir
if (!dir) return
const msg = userMessages()[0]
if (!msg) return
const value = extractPromptFromParts(sync.data.part[msg.id] ?? [], {
directory: sdk.directory,
attachmentName: language.t("common.attachment"),
})
prompt.set(value, promptLength(value), { dir })
navigate(`/${dir}/session`)
}
const selectionPreview = (path: string, selection: FileSelection) => {
const content = file.get(path)?.content?.content
if (!content) return u