Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
mode: store.mode,
commentCount: commentCount(),
example: language.t(EXAMPLES[store.placeholder]),
agent: local.agent.current(),
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
}),
)
Expand Down
30 changes: 29 additions & 1 deletion packages/app/src/components/prompt-input/placeholder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { describe, expect, test } from "bun:test"
import { promptPlaceholder } from "./placeholder"

describe("promptPlaceholder", () => {
const t = (key: string, params?: Record<string, string>) => `${key}${params?.example ? `:${params.example}` : ""}`
const t = (key: string, params?: Record<string, string>) => {
const parts = [key]
if (params?.example) parts.push(params.example)
if (params?.name) parts.push(params.name)
if (params?.description) parts.push(params.description)
return parts.join(":")
}

test("returns shell placeholder in shell mode", () => {
const value = promptPlaceholder({
Expand All @@ -23,6 +29,28 @@ describe("promptPlaceholder", () => {
)
})

test("returns agent description placeholder when agent has description", () => {
const value = promptPlaceholder({
mode: "normal",
commentCount: 0,
example: "example",
agent: { name: "build", description: "a coding agent" },
t,
})
expect(value).toBe("prompt.placeholder.agent:build:a coding agent")
})

test("returns default placeholder when agent has no description", () => {
const value = promptPlaceholder({
mode: "normal",
commentCount: 0,
example: "translated-example",
agent: { name: "build" },
t,
})
expect(value).toBe("prompt.placeholder.normal:translated-example")
})

test("returns default placeholder with example", () => {
const value = promptPlaceholder({
mode: "normal",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/components/prompt-input/placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ type PromptPlaceholderInput = {
mode: "normal" | "shell"
commentCount: number
example: string
agent?: { name: string; description?: string }
t: (key: string, params?: Record<string, string>) => string
}

export function promptPlaceholder(input: PromptPlaceholderInput) {
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
if (input.agent?.description)
return input.t("prompt.placeholder.agent", { name: input.agent.name, description: input.agent.description })
return input.t("prompt.placeholder.normal", { example: input.example })
}
1 change: 1 addition & 0 deletions packages/app/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const dict = {
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
"prompt.placeholder.agent": "أنا {{name}}، {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc للخروج",
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const dict = {
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
"prompt.placeholder.summarizeComments": "Resumir comentários…",
"prompt.placeholder.summarizeComment": "Resumir comentário…",
"prompt.placeholder.agent": "Eu sou {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc para sair",
"prompt.example.1": "Corrigir um TODO no código",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/bs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export const dict = {
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
"prompt.placeholder.agent": "Ja sam {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc za izlaz",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const dict = {
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
"prompt.placeholder.agent": "Jeg er {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc for at afslutte",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export const dict = {
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
"prompt.placeholder.agent": "Ich bin {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc zum Verlassen",
"prompt.example.1": "Ein TODO in der Codebasis beheben",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const dict = {
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
"prompt.placeholder.summarizeComments": "Summarize comments…",
"prompt.placeholder.summarizeComment": "Summarize comment…",
"prompt.placeholder.agent": "I am {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc to exit",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const dict = {
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
"prompt.placeholder.summarizeComment": "Resumir comentario…",
"prompt.placeholder.agent": "Soy {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc para salir",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const dict = {
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
"prompt.placeholder.agent": "Je suis {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc pour quitter",
"prompt.example.1": "Corriger un TODO dans la base de code",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export const dict = {
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
"prompt.placeholder.summarizeComments": "コメントを要約…",
"prompt.placeholder.summarizeComment": "コメントを要約…",
"prompt.placeholder.agent": "私は {{name}} です。{{description}}",
"prompt.mode.shell": "シェル",
"prompt.mode.shell.exit": "escで終了",
"prompt.example.1": "コードベースのTODOを修正",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export const dict = {
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
"prompt.placeholder.summarizeComments": "댓글 요약…",
"prompt.placeholder.summarizeComment": "댓글 요약…",
"prompt.placeholder.agent": "저는 {{name}}입니다. {{description}}",
"prompt.mode.shell": "셸",
"prompt.mode.shell.exit": "종료하려면 esc",
"prompt.example.1": "코드베이스의 TODO 수정",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/no.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export const dict = {
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
"prompt.placeholder.agent": "Jeg er {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "ESC for å avslutte",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export const dict = {
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
"prompt.placeholder.agent": "Jestem {{name}}, {{description}}",
"prompt.mode.shell": "Terminal",
"prompt.mode.shell.exit": "esc aby wyjść",
"prompt.example.1": "Napraw TODO w bazie kodu",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const dict = {
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
"prompt.placeholder.agent": "Я {{name}}, {{description}}",
"prompt.mode.shell": "Оболочка",
"prompt.mode.shell.exit": "esc для выхода",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const dict = {
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
"prompt.placeholder.agent": "ฉันคือ {{name}}, {{description}}",
"prompt.mode.shell": "เชลล์",
"prompt.mode.shell.exit": "กด esc เพื่อออก",

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ export const dict = {
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
"prompt.placeholder.summarizeComments": "总结评论…",
"prompt.placeholder.summarizeComment": "总结该评论…",
"prompt.placeholder.agent": "我是 {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "按 esc 退出",
"prompt.example.1": "修复代码库中的一个 TODO",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/zht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const dict = {
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
"prompt.placeholder.summarizeComments": "摘要評論…",
"prompt.placeholder.summarizeComment": "摘要這則評論…",
"prompt.placeholder.agent": "我是 {{name}}, {{description}}",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "按 esc 退出",

Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/pages/directory-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { decode64 } from "@/utils/base64"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"

import { base64Encode } from "@opencode-ai/util/encode"

export default function Layout(props: ParentProps) {
const params = useParams()
const navigate = useNavigate()
Expand Down Expand Up @@ -61,6 +63,26 @@ export default function Layout(props: ParentProps) {

const syncSession = (sessionID: string) => sync.session.sync(sessionID)

const undoMessage = async (sessionID: string, messageID: string) => {
const status = sync.data.session_status[sessionID]
if (status?.type !== "idle") {
await sdk.client.session.abort({ sessionID }).catch(() => {})
}
await sdk.client.session.revert({ sessionID, messageID })
}

const forkMessage = async (sessionID: string, messageID: string) => {
const msgs = sync.data.message[sessionID]
let cutoffID: string | undefined
if (msgs) {
const idx = msgs.findIndex((m) => m.id === messageID)
if (idx !== -1 && idx + 1 < msgs.length) cutoffID = msgs[idx + 1]!.id
}
const result = await sdk.client.session.fork({ sessionID, messageID: cutoffID })
if (!result.data) return
navigate(`/${base64Encode(sdk.directory)}/session/${result.data.id}`)
}

return (
<DataProvider
data={sync.data}
Expand All @@ -71,6 +93,8 @@ export default function Layout(props: ParentProps) {
onNavigateToSession={navigateToSession}
onSessionHref={sessionHref}
onSyncSession={syncSession}
onUndoMessage={undoMessage}
onForkMessage={forkMessage}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useLayout } from "@/context/layout"
import { checksum, base64Encode } from "@opencode-ai/util/encode"
import { findLast } from "@opencode-ai/util/array"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useData } from "@opencode-ai/ui/context"
import { DialogSelectFile } from "@/components/dialog-select-file"
import FileTree from "@/components/file-tree"
import { useCommand } from "@/context/command"
Expand All @@ -40,6 +41,7 @@ import { showToast } from "@opencode-ai/ui/toast"
import { SessionHeader, SessionContextTab, SortableTab, FileVisual, NewSessionView } from "@/components/session"
import { navMark, navParams } from "@/utils/perf"
import { same } from "@/utils/same"
import { extractPromptFromParts } from "@/utils/prompt"
import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "@/pages/session/helpers"
import { createScrollSpy } from "@/pages/session/scroll-spy"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
Expand Down Expand Up @@ -100,6 +102,7 @@ export default function Page() {
const navigate = useNavigate()
const sdk = useSDK()
const prompt = usePrompt()
const data = useData()
const comments = useComments()
const permission = usePermission()

Expand All @@ -117,6 +120,18 @@ export default function Page() {

const blocked = createMemo(() => !!permRequest() || !!questionRequest())

createEffect(
on(data.pendingRestore, (parts) => {
if (!parts) return
const restored = extractPromptFromParts(parts, {
directory: sdk.directory,
attachmentName: language.t("common.attachment"),
})
data.consumeRestore()
requestAnimationFrame(() => prompt.set(restored))
}),
)

const [ui, setUi] = createStore({
responding: false,
pendingMessage: undefined as string | undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const icons = {
"layout-bottom-full": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
undo: `<path d="M4.16667 7.5H12.0833C14.3845 7.5 16.25 9.36548 16.25 11.6667C16.25 13.9679 14.3845 15.8333 12.0833 15.8333H10" stroke="currentColor" stroke-linecap="square"/><path d="M7.5 4.16667L4.16667 7.5L7.5 10.8333" stroke="currentColor" stroke-linecap="square"/>`,
copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/components/message-part.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,17 @@
color: var(--syntax-type);
}

[data-slot="user-message-copy-wrapper"] {
[data-slot="user-message-actions"] {
position: absolute;
top: 7px;
right: 7px;
display: flex;
gap: 5px;
opacity: 0;
transition: opacity 0.15s ease;
}

&:hover [data-slot="user-message-copy-wrapper"] {
&:hover [data-slot="user-message-actions"] {
opacity: 1;
}
}
Expand Down
66 changes: 65 additions & 1 deletion packages/ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,22 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
}

export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
const data = useData()
const dialog = useDialog()
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
const [expanded, setExpanded] = createSignal(false)
const [canExpand, setCanExpand] = createSignal(false)
const [confirmingUndo, setConfirmingUndo] = createSignal(false)
const [confirmingFork, setConfirmingFork] = createSignal(false)
let textRef: HTMLDivElement | undefined
let undoTimer: ReturnType<typeof setTimeout> | undefined
let forkTimer: ReturnType<typeof setTimeout> | undefined

onCleanup(() => {
if (undoTimer) clearTimeout(undoTimer)
if (forkTimer) clearTimeout(forkTimer)
})

const updateCanExpand = () => {
const el = textRef
Expand Down Expand Up @@ -417,7 +427,61 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
>
<Icon name="chevron-down" size="small" />
</button>
<div data-slot="user-message-copy-wrapper">
<div data-slot="user-message-actions">
<Show when={data.forkMessage}>
<Tooltip
value={confirmingFork() ? i18n.t("ui.message.fork.confirm") : i18n.t("ui.message.fork.description")}
placement="top"
gutter={8}
forceOpen={confirmingFork()}
>
<IconButton
icon="branch"
size="small"
variant={confirmingFork() ? "primary" : "secondary"}
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
if (confirmingFork()) {
if (forkTimer) clearTimeout(forkTimer)
setConfirmingFork(false)
data.forkMessage?.(props.message.sessionID, props.message.id)
return
}
setConfirmingFork(true)
forkTimer = setTimeout(() => setConfirmingFork(false), 3000)
}}
aria-label={confirmingFork() ? i18n.t("ui.message.fork.confirm") : i18n.t("ui.message.fork")}
/>
</Tooltip>
</Show>
<Show when={data.undoMessage}>
<Tooltip
value={confirmingUndo() ? i18n.t("ui.message.undo.confirm") : i18n.t("ui.message.undo")}
placement="top"
gutter={8}
forceOpen={confirmingUndo()}
>
<IconButton
icon="undo"
size="small"
variant={confirmingUndo() ? "primary" : "secondary"}
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
if (confirmingUndo()) {
if (undoTimer) clearTimeout(undoTimer)
setConfirmingUndo(false)
data.undoMessage?.(props.message.sessionID, props.message.id)
return
}
setConfirmingUndo(true)
undoTimer = setTimeout(() => setConfirmingUndo(false), 3000)
}}
aria-label={confirmingUndo() ? i18n.t("ui.message.undo.confirm") : i18n.t("ui.message.undo")}
/>
</Tooltip>
</Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
placement="top"
Expand Down
Loading
Loading