summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-30 08:50:42 -0500
committerGitHub <[email protected]>2026-03-30 08:50:42 -0500
commitc2f78224ae59263eada831051a6ece1c65126b1a (patch)
tree23468fdd7e2910266756944aa764f7fdcf696d25 /packages/app/src
parent14f9e21d5c3f4e853dee8ca133693dd3b915b634 (diff)
downloadopencode-c2f78224ae59263eada831051a6ece1c65126b1a.tar.gz
opencode-c2f78224ae59263eada831051a6ece1c65126b1a.zip
chore(app): cleanup (#20062)
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/pages/session/composer/session-question-dock.tsx167
-rw-r--r--packages/app/src/pages/session/file-tabs.tsx273
-rw-r--r--packages/app/src/pages/session/use-session-commands.tsx808
3 files changed, 671 insertions, 577 deletions
diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx
index 7ba07b15d..ef1e52d26 100644
--- a/packages/app/src/pages/session/composer/session-question-dock.tsx
+++ b/packages/app/src/pages/session/composer/session-question-dock.tsx
@@ -11,6 +11,47 @@ import { useSDK } from "@/context/sdk"
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
+function Mark(props: { multi: boolean; picked: boolean; onClick?: (event: MouseEvent) => void }) {
+ return (
+ <span data-slot="question-option-check" aria-hidden="true" onClick={props.onClick}>
+ <span data-slot="question-option-box" data-type={props.multi ? "checkbox" : "radio"} data-picked={props.picked}>
+ <Show when={props.multi} fallback={<span data-slot="question-option-radio-dot" />}>
+ <Icon name="check-small" size="small" />
+ </Show>
+ </span>
+ </span>
+ )
+}
+
+function Option(props: {
+ multi: boolean
+ picked: boolean
+ label: string
+ description?: string
+ disabled: boolean
+ onClick: VoidFunction
+}) {
+ return (
+ <button
+ type="button"
+ data-slot="question-option"
+ data-picked={props.picked}
+ role={props.multi ? "checkbox" : "radio"}
+ aria-checked={props.picked}
+ disabled={props.disabled}
+ onClick={props.onClick}
+ >
+ <Mark multi={props.multi} picked={props.picked} />
+ <span data-slot="question-option-main">
+ <span data-slot="option-label">{props.label}</span>
+ <Show when={props.description}>
+ <span data-slot="option-description">{props.description}</span>
+ </Show>
+ </span>
+ </button>
+ )
+}
+
export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => {
const sdk = useSDK()
const language = useLanguage()
@@ -41,6 +82,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
return language.t("session.question.progress", { current: n, total: total() })
})
+ const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer")
+ const customPlaceholder = () => language.t("ui.question.custom.placeholder")
+
const last = createMemo(() => store.tab >= total() - 1)
const customUpdate = (value: string, selected: boolean = on()) => {
@@ -164,6 +208,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
+ const answered = (i: number) => {
+ if ((store.answers[i]?.length ?? 0) > 0) return true
+ return store.customOn[i] === true && (store.custom[i] ?? "").trim().length > 0
+ }
+
+ const picked = (answer: string) => store.answers[store.tab]?.includes(answer) ?? false
+
const pick = (answer: string, custom: boolean = false) => {
setStore("answers", store.tab, [answer])
if (custom) setStore("custom", store.tab, answer)
@@ -230,6 +281,24 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
customUpdate(input())
}
+ const resizeInput = (el: HTMLTextAreaElement) => {
+ el.style.height = "0px"
+ el.style.height = `${el.scrollHeight}px`
+ }
+
+ const focusCustom = (el: HTMLTextAreaElement) => {
+ setTimeout(() => {
+ el.focus()
+ resizeInput(el)
+ }, 0)
+ }
+
+ const toggleCustomMark = (event: MouseEvent) => {
+ event.preventDefault()
+ event.stopPropagation()
+ customToggle()
+ }
+
const next = () => {
if (sending()) return
if (store.editing) commitCustom()
@@ -270,10 +339,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
type="button"
data-slot="question-progress-segment"
data-active={i() === store.tab}
- data-answered={
- (store.answers[i()]?.length ?? 0) > 0 ||
- (store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
- }
+ data-answered={answered(i())}
disabled={sending()}
onClick={() => jump(i())}
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
@@ -307,43 +373,23 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
</Show>
<div data-slot="question-options">
<For each={options()}>
- {(opt, i) => {
- const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
- return (
- <button
- data-slot="question-option"
- data-picked={picked()}
- role={multi() ? "checkbox" : "radio"}
- aria-checked={picked()}
- disabled={sending()}
- onClick={() => selectOption(i())}
- >
- <span data-slot="question-option-check" aria-hidden="true">
- <span
- data-slot="question-option-box"
- data-type={multi() ? "checkbox" : "radio"}
- data-picked={picked()}
- >
- <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
- <Icon name="check-small" size="small" />
- </Show>
- </span>
- </span>
- <span data-slot="question-option-main">
- <span data-slot="option-label">{opt.label}</span>
- <Show when={opt.description}>
- <span data-slot="option-description">{opt.description}</span>
- </Show>
- </span>
- </button>
- )
- }}
+ {(opt, i) => (
+ <Option
+ multi={multi()}
+ picked={picked(opt.label)}
+ label={opt.label}
+ description={opt.description}
+ disabled={sending()}
+ onClick={() => selectOption(i())}
+ />
+ )}
</For>
<Show
when={store.editing}
fallback={
<button
+ type="button"
data-slot="question-option"
data-custom="true"
data-picked={on()}
@@ -352,24 +398,10 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
disabled={sending()}
onClick={customOpen}
>
- <span
- data-slot="question-option-check"
- aria-hidden="true"
- onClick={(e) => {
- e.preventDefault()
- e.stopPropagation()
- customToggle()
- }}
- >
- <span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
- <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
- <Icon name="check-small" size="small" />
- </Show>
- </span>
- </span>
+ <Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
<span data-slot="question-option-main">
- <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
- <span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
+ <span data-slot="option-label">{customLabel()}</span>
+ <span data-slot="option-description">{input() || customPlaceholder()}</span>
</span>
</button>
}
@@ -394,33 +426,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
commitCustom()
}}
>
- <span
- data-slot="question-option-check"
- aria-hidden="true"
- onClick={(e) => {
- e.preventDefault()
- e.stopPropagation()
- customToggle()
- }}
- >
- <span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
- <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
- <Icon name="check-small" size="small" />
- </Show>
- </span>
- </span>
+ <Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
<span data-slot="question-option-main">
- <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
+ <span data-slot="option-label">{customLabel()}</span>
<textarea
- ref={(el) =>
- setTimeout(() => {
- el.focus()
- el.style.height = "0px"
- el.style.height = `${el.scrollHeight}px`
- }, 0)
- }
+ ref={focusCustom}
data-slot="question-custom-input"
- placeholder={language.t("ui.question.custom.placeholder")}
+ placeholder={customPlaceholder()}
value={input()}
rows={1}
disabled={sending()}
@@ -436,8 +448,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
}}
onInput={(e) => {
customUpdate(e.currentTarget.value)
- e.currentTarget.style.height = "0px"
- e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
+ resizeInput(e.currentTarget)
}}
/>
</span>
diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx
index e3b57fdf2..8208b6c99 100644
--- a/packages/app/src/pages/session/file-tabs.tsx
+++ b/packages/app/src/pages/session/file-tabs.tsx
@@ -52,6 +52,132 @@ function FileCommentMenu(props: {
)
}
+type ScrollPos = { x: number; y: number }
+
+function createScrollSync(input: { tab: () => string; view: ReturnType<typeof useSessionLayout>["view"] }) {
+ let scroll: HTMLDivElement | undefined
+ let scrollFrame: number | undefined
+ let restoreFrame: number | undefined
+ let pending: ScrollPos | undefined
+ let code: HTMLElement[] = []
+
+ const getCode = () => {
+ const el = scroll
+ if (!el) return []
+
+ const host = el.querySelector("diffs-container")
+ if (!(host instanceof HTMLElement)) return []
+
+ const root = host.shadowRoot
+ if (!root) return []
+
+ return Array.from(root.querySelectorAll("[data-code]")).filter(
+ (node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0,
+ )
+ }
+
+ const save = (next: ScrollPos) => {
+ pending = next
+ if (scrollFrame !== undefined) return
+
+ scrollFrame = requestAnimationFrame(() => {
+ scrollFrame = undefined
+
+ const out = pending
+ pending = undefined
+ if (!out) return
+
+ input.view().setScroll(input.tab(), out)
+ })
+ }
+
+ const onCodeScroll = (event: Event) => {
+ const el = scroll
+ if (!el) return
+
+ const target = event.currentTarget
+ if (!(target instanceof HTMLElement)) return
+
+ save({
+ x: target.scrollLeft,
+ y: el.scrollTop,
+ })
+ }
+
+ const sync = () => {
+ const next = getCode()
+ if (next.length === code.length && next.every((el, i) => el === code[i])) return
+
+ for (const item of code) {
+ item.removeEventListener("scroll", onCodeScroll)
+ }
+
+ code = next
+
+ for (const item of code) {
+ item.addEventListener("scroll", onCodeScroll)
+ }
+ }
+
+ const restore = () => {
+ const el = scroll
+ if (!el) return
+
+ const pos = input.view().scroll(input.tab())
+ if (!pos) return
+
+ sync()
+
+ if (code.length > 0) {
+ for (const item of code) {
+ if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x
+ }
+ }
+
+ if (el.scrollTop !== pos.y) el.scrollTop = pos.y
+ if (code.length > 0) return
+ if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x
+ }
+
+ const queueRestore = () => {
+ if (restoreFrame !== undefined) return
+
+ restoreFrame = requestAnimationFrame(() => {
+ restoreFrame = undefined
+ restore()
+ })
+ }
+
+ const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
+ if (code.length === 0) sync()
+
+ save({
+ x: code[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
+ y: event.currentTarget.scrollTop,
+ })
+ }
+
+ const setViewport = (el: HTMLDivElement) => {
+ scroll = el
+ restore()
+ }
+
+ onCleanup(() => {
+ for (const item of code) {
+ item.removeEventListener("scroll", onCodeScroll)
+ }
+
+ if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
+ if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
+ })
+
+ return {
+ handleScroll,
+ queueRestore,
+ setViewport,
+ }
+}
+
export function FileTabContent(props: { tab: string }) {
const file = useFile()
const comments = useComments()
@@ -65,11 +191,6 @@ export function FileTabContent(props: { tab: string }) {
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
}).activeFileTab
- let scroll: HTMLDivElement | undefined
- let scrollFrame: number | undefined
- let restoreFrame: number | undefined
- let pending: { x: number; y: number } | undefined
- let codeScroll: HTMLElement[] = []
let find: FileSearchHandle | null = null
const search = {
@@ -92,6 +213,10 @@ export function FileTabContent(props: { tab: string }) {
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null
})
+ const scrollSync = createScrollSync({
+ tab: () => props.tab,
+ view,
+ })
const selectionPreview = (source: string, selection: FileSelection) => {
return previewSelectedLines(source, {
@@ -100,6 +225,12 @@ export function FileTabContent(props: { tab: string }) {
})
}
+ const buildPreview = (filePath: string, selection: FileSelection) => {
+ const source = filePath === path() ? contents() : file.get(filePath)?.content?.content
+ if (!source) return undefined
+ return selectionPreview(source, selection)
+ }
+
const addCommentToContext = (input: {
file: string
selection: SelectedLineRange
@@ -108,14 +239,7 @@ export function FileTabContent(props: { tab: string }) {
origin?: "review" | "file"
}) => {
const selection = selectionFromLines(input.selection)
- const preview =
- input.preview ??
- (() => {
- if (input.file === path()) return selectionPreview(contents(), selection)
- const source = file.get(input.file)?.content?.content
- if (!source) return undefined
- return selectionPreview(source, selection)
- })()
+ const preview = input.preview ?? buildPreview(input.file, selection)
const saved = comments.add({
file: input.file,
@@ -140,8 +264,7 @@ export function FileTabContent(props: { tab: string }) {
comment: string
}) => {
comments.update(input.file, input.id, input.comment)
- const preview =
- input.file === path() ? selectionPreview(contents(), selectionFromLines(input.selection)) : undefined
+ const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined
prompt.context.updateComment(input.file, input.id, {
comment: input.comment,
...(preview ? { preview } : {}),
@@ -260,102 +383,6 @@ export function FileTabContent(props: { tab: string }) {
requestAnimationFrame(() => comments.clearFocus())
})
- const getCodeScroll = () => {
- const el = scroll
- if (!el) return []
-
- const host = el.querySelector("diffs-container")
- if (!(host instanceof HTMLElement)) return []
-
- const root = host.shadowRoot
- if (!root) return []
-
- return Array.from(root.querySelectorAll("[data-code]")).filter(
- (node): node is HTMLElement => node instanceof HTMLElement && node.clientWidth > 0,
- )
- }
-
- const queueScrollUpdate = (next: { x: number; y: number }) => {
- pending = next
- if (scrollFrame !== undefined) return
-
- scrollFrame = requestAnimationFrame(() => {
- scrollFrame = undefined
-
- const out = pending
- pending = undefined
- if (!out) return
-
- view().setScroll(props.tab, out)
- })
- }
-
- const handleCodeScroll = (event: Event) => {
- const el = scroll
- if (!el) return
-
- const target = event.currentTarget
- if (!(target instanceof HTMLElement)) return
-
- queueScrollUpdate({
- x: target.scrollLeft,
- y: el.scrollTop,
- })
- }
-
- const syncCodeScroll = () => {
- const next = getCodeScroll()
- if (next.length === codeScroll.length && next.every((el, i) => el === codeScroll[i])) return
-
- for (const item of codeScroll) {
- item.removeEventListener("scroll", handleCodeScroll)
- }
-
- codeScroll = next
-
- for (const item of codeScroll) {
- item.addEventListener("scroll", handleCodeScroll)
- }
- }
-
- const restoreScroll = () => {
- const el = scroll
- if (!el) return
-
- const s = view().scroll(props.tab)
- if (!s) return
-
- syncCodeScroll()
-
- if (codeScroll.length > 0) {
- for (const item of codeScroll) {
- if (item.scrollLeft !== s.x) item.scrollLeft = s.x
- }
- }
-
- if (el.scrollTop !== s.y) el.scrollTop = s.y
- if (codeScroll.length > 0) return
- if (el.scrollLeft !== s.x) el.scrollLeft = s.x
- }
-
- const queueRestore = () => {
- if (restoreFrame !== undefined) return
-
- restoreFrame = requestAnimationFrame(() => {
- restoreFrame = undefined
- restoreScroll()
- })
- }
-
- const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
- if (codeScroll.length === 0) syncCodeScroll()
-
- queueScrollUpdate({
- x: codeScroll[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
- y: event.currentTarget.scrollTop,
- })
- }
-
const cancelCommenting = () => {
const p = path()
if (p) file.setSelectedLines(p, null)
@@ -375,16 +402,7 @@ export function FileTabContent(props: { tab: string }) {
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
prev = { loaded, ready, active }
if (!restore) return
- queueRestore()
- })
-
- onCleanup(() => {
- for (const item of codeScroll) {
- item.removeEventListener("scroll", handleCodeScroll)
- }
-
- if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
- if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
+ scrollSync.queueRestore()
})
const renderFile = (source: string) => (
@@ -402,7 +420,7 @@ export function FileTabContent(props: { tab: string }) {
selectedLines={activeSelection()}
commentedLines={commentedLines()}
onRendered={() => {
- queueRestore()
+ scrollSync.queueRestore()
}}
annotations={commentsUi.annotations()}
renderAnnotation={commentsUi.renderAnnotation}
@@ -420,7 +438,7 @@ export function FileTabContent(props: { tab: string }) {
mode: "auto",
path: path(),
current: state()?.content,
- onLoad: queueRestore,
+ onLoad: scrollSync.queueRestore,
onError: (args: { kind: "image" | "audio" | "svg" }) => {
if (args.kind !== "svg") return
showToast({
@@ -435,14 +453,7 @@ export function FileTabContent(props: { tab: string }) {
return (
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
- <ScrollView
- class="h-full"
- viewportRef={(el: HTMLDivElement) => {
- scroll = el
- restoreScroll()
- }}
- onScroll={handleScroll as any}
- >
+ <ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
<Switch>
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
<Match when={state()?.loading}>
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index 1a1c290f6..430fd46f8 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -128,380 +128,452 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
return permission.isAutoAcceptingDirectory(sdk.directory)
}
- command.register("session", () => {
- const share =
- sync.data.config.share === "disabled"
- ? []
- : [
- sessionCommand({
- id: "session.share",
- title: info()?.share?.url
- ? language.t("session.share.copy.copyLink")
- : language.t("command.session.share"),
- description: info()?.share?.url
- ? language.t("toast.session.share.success.description")
- : language.t("command.session.share.description"),
- slash: "share",
- disabled: !params.id,
- onSelect: async () => {
- if (!params.id) return
-
- const write = (value: string) => {
- const body = typeof document === "undefined" ? undefined : document.body
- if (body) {
- const textarea = document.createElement("textarea")
- textarea.value = value
- textarea.setAttribute("readonly", "")
- textarea.style.position = "fixed"
- textarea.style.opacity = "0"
- textarea.style.pointerEvents = "none"
- body.appendChild(textarea)
- textarea.select()
- const copied = document.execCommand("copy")
- body.removeChild(textarea)
- if (copied) return Promise.resolve(true)
- }
-
- const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
- if (!clipboard?.writeText) return Promise.resolve(false)
- return clipboard.writeText(value).then(
- () => true,
- () => false,
- )
- }
-
- const copy = async (url: string, existing: boolean) => {
- const ok = await write(url)
- if (!ok) {
- showToast({
- title: language.t("toast.session.share.copyFailed.title"),
- variant: "error",
- })
- return
- }
-
- showToast({
- title: existing
- ? language.t("session.share.copy.copied")
- : language.t("toast.session.share.success.title"),
- description: language.t("toast.session.share.success.description"),
- variant: "success",
- })
- }
-
- const existing = info()?.share?.url
- if (existing) {
- await copy(existing, true)
- return
- }
-
- const url = await sdk.client.session
- .share({ sessionID: params.id })
- .then((res) => res.data?.share?.url)
- .catch(() => undefined)
- if (!url) {
- showToast({
- title: language.t("toast.session.share.failed.title"),
- description: language.t("toast.session.share.failed.description"),
- variant: "error",
- })
- return
- }
-
- await copy(url, false)
- },
- }),
- sessionCommand({
- id: "session.unshare",
- title: language.t("command.session.unshare"),
- description: language.t("command.session.unshare.description"),
- slash: "unshare",
- disabled: !params.id || !info()?.share?.url,
- onSelect: async () => {
- if (!params.id) return
- await sdk.client.session
- .unshare({ sessionID: params.id })
- .then(() =>
- showToast({
- title: language.t("toast.session.unshare.success.title"),
- description: language.t("toast.session.unshare.success.description"),
- variant: "success",
- }),
- )
- .catch(() =>
- showToast({
- title: language.t("toast.session.unshare.failed.title"),
- description: language.t("toast.session.unshare.failed.description"),
- variant: "error",
- }),
- )
- },
- }),
- ]
+ const write = async (value: string) => {
+ const body = typeof document === "undefined" ? undefined : document.body
+ if (body) {
+ const textarea = document.createElement("textarea")
+ textarea.value = value
+ textarea.setAttribute("readonly", "")
+ textarea.style.position = "fixed"
+ textarea.style.opacity = "0"
+ textarea.style.pointerEvents = "none"
+ body.appendChild(textarea)
+ textarea.select()
+ const copied = document.execCommand("copy")
+ body.removeChild(textarea)
+ if (copied) return true
+ }
+ const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
+ if (!clipboard?.writeText) return false
+ return clipboard.writeText(value).then(
+ () => true,
+ () => false,
+ )
+ }
+
+ const copyShare = async (url: string, existing: boolean) => {
+ if (!(await write(url))) {
+ showToast({
+ title: language.t("toast.session.share.copyFailed.title"),
+ variant: "error",
+ })
+ return
+ }
+
+ showToast({
+ title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"),
+ description: language.t("toast.session.share.success.description"),
+ variant: "success",
+ })
+ }
+
+ const share = async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+
+ const existing = info()?.share?.url
+ if (existing) {
+ await copyShare(existing, true)
+ return
+ }
+
+ const url = await sdk.client.session
+ .share({ sessionID })
+ .then((res) => res.data?.share?.url)
+ .catch(() => undefined)
+ if (!url) {
+ showToast({
+ title: language.t("toast.session.share.failed.title"),
+ description: language.t("toast.session.share.failed.description"),
+ variant: "error",
+ })
+ return
+ }
+
+ await copyShare(url, false)
+ }
+
+ const unshare = async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+
+ await sdk.client.session
+ .unshare({ sessionID })
+ .then(() =>
+ showToast({
+ title: language.t("toast.session.unshare.success.title"),
+ description: language.t("toast.session.unshare.success.description"),
+ variant: "success",
+ }),
+ )
+ .catch(() =>
+ showToast({
+ title: language.t("toast.session.unshare.failed.title"),
+ description: language.t("toast.session.unshare.failed.description"),
+ variant: "error",
+ }),
+ )
+ }
+
+ const openFile = () => {
+ void import("@/components/dialog-select-file").then((x) => {
+ dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />)
+ })
+ }
+
+ const closeTab = () => {
+ const tab = closableTab()
+ if (!tab) return
+ tabs().close(tab)
+ }
+
+ const addSelection = () => {
+ const tab = activeFileTab()
+ if (!tab) return
+
+ const path = file.pathFromTab(tab)
+ if (!path) return
+
+ const range = file.selectedLines(path) as SelectedLineRange | null | undefined
+ if (!range) {
+ showToast({
+ title: language.t("toast.context.noLineSelection.title"),
+ description: language.t("toast.context.noLineSelection.description"),
+ })
+ return
+ }
+
+ addSelectionToContext(path, selectionFromLines(range))
+ }
+
+ const openTerminal = () => {
+ if (terminal.all().length > 0) terminal.new()
+ view().terminal.open()
+ }
+
+ const chooseModel = () => {
+ void import("@/components/dialog-select-model").then((x) => {
+ dialog.show(() => <x.DialogSelectModel model={local.model} />)
+ })
+ }
+
+ const chooseMcp = () => {
+ void import("@/components/dialog-select-mcp").then((x) => {
+ dialog.show(() => <x.DialogSelectMcp />)
+ })
+ }
+
+ const toggleAutoAccept = () => {
+ const sessionID = params.id
+ if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
+ else permission.toggleAutoAcceptDirectory(sdk.directory)
+
+ const active = sessionID
+ ? permission.isAutoAccepting(sessionID, sdk.directory)
+ : permission.isAutoAcceptingDirectory(sdk.directory)
+ showToast({
+ title: active
+ ? language.t("toast.permissions.autoaccept.on.title")
+ : language.t("toast.permissions.autoaccept.off.title"),
+ description: active
+ ? language.t("toast.permissions.autoaccept.on.description")
+ : language.t("toast.permissions.autoaccept.off.description"),
+ })
+ }
+
+ const undo = async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+
+ if (status().type !== "idle") {
+ await sdk.client.session.abort({ sessionID }).catch(() => {})
+ }
+
+ const revert = info()?.revert?.messageID
+ const message = findLast(userMessages(), (x) => !revert || x.id < revert)
+ if (!message) return
+
+ await sdk.client.session.revert({ sessionID, messageID: message.id })
+ const parts = sync.data.part[message.id]
+ if (parts) {
+ const restored = extractPromptFromParts(parts, { directory: sdk.directory })
+ prompt.set(restored)
+ }
+
+ const prev = findLast(userMessages(), (x) => x.id < message.id)
+ setActiveMessage(prev)
+ }
+
+ const redo = async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+
+ const revertMessageID = info()?.revert?.messageID
+ if (!revertMessageID) return
+
+ const next = userMessages().find((x) => x.id > revertMessageID)
+ if (!next) {
+ await sdk.client.session.unrevert({ sessionID })
+ prompt.reset()
+ const last = findLast(userMessages(), (x) => x.id >= revertMessageID)
+ setActiveMessage(last)
+ return
+ }
+
+ await sdk.client.session.revert({ sessionID, messageID: next.id })
+ const prev = findLast(userMessages(), (x) => x.id < next.id)
+ setActiveMessage(prev)
+ }
+
+ const compact = async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+
+ const model = local.model.current()
+ if (!model) {
+ showToast({
+ title: language.t("toast.model.none.title"),
+ description: language.t("toast.model.none.description"),
+ })
+ return
+ }
+
+ await sdk.client.session.summarize({
+ sessionID,
+ modelID: model.id,
+ providerID: model.provider.id,
+ })
+ }
+
+ const fork = () => {
+ void import("@/components/dialog-fork").then((x) => {
+ dialog.show(() => <x.DialogFork />)
+ })
+ }
+
+ const shareCmds = () => {
+ if (sync.data.config.share === "disabled") return []
return [
sessionCommand({
- id: "session.new",
- title: language.t("command.session.new"),
- keybind: "mod+shift+s",
- slash: "new",
- onSelect: () => navigate(`/${params.dir}/session`),
- }),
- fileCommand({
- id: "file.open",
- title: language.t("command.file.open"),
- description: language.t("palette.search.placeholder"),
- keybind: "mod+k,mod+p",
- slash: "open",
- onSelect: () => {
- void import("@/components/dialog-select-file").then((x) => {
- dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />)
- })
- },
- }),
- fileCommand({
- id: "tab.close",
- title: language.t("command.tab.close"),
- keybind: "mod+w",
- disabled: !closableTab(),
- onSelect: () => {
- const tab = closableTab()
- if (!tab) return
- tabs().close(tab)
- },
- }),
- contextCommand({
- id: "context.addSelection",
- title: language.t("command.context.addSelection"),
- description: language.t("command.context.addSelection.description"),
- keybind: "mod+shift+l",
- disabled: !canAddSelectionContext(),
- onSelect: () => {
- const tab = activeFileTab()
- if (!tab) return
- const path = file.pathFromTab(tab)
- if (!path) return
-
- const range = file.selectedLines(path) as SelectedLineRange | null | undefined
- if (!range) {
- showToast({
- title: language.t("toast.context.noLineSelection.title"),
- description: language.t("toast.context.noLineSelection.description"),
- })
- return
- }
-
- addSelectionToContext(path, selectionFromLines(range))
- },
- }),
- viewCommand({
- id: "terminal.toggle",
- title: language.t("command.terminal.toggle"),
- keybind: "ctrl+`",
- slash: "terminal",
- onSelect: () => view().terminal.toggle(),
- }),
- viewCommand({
- id: "review.toggle",
- title: language.t("command.review.toggle"),
- keybind: "mod+shift+r",
- onSelect: () => view().reviewPanel.toggle(),
- }),
- viewCommand({
- id: "fileTree.toggle",
- title: language.t("command.fileTree.toggle"),
- keybind: "mod+\\",
- onSelect: () => layout.fileTree.toggle(),
- }),
- viewCommand({
- id: "input.focus",
- title: language.t("command.input.focus"),
- keybind: "ctrl+l",
- onSelect: focusInput,
- }),
- terminalCommand({
- id: "terminal.new",
- title: language.t("command.terminal.new"),
- description: language.t("command.terminal.new.description"),
- keybind: "ctrl+alt+t",
- onSelect: () => {
- if (terminal.all().length > 0) terminal.new()
- view().terminal.open()
- },
- }),
- sessionCommand({
- id: "message.previous",
- title: language.t("command.message.previous"),
- description: language.t("command.message.previous.description"),
- keybind: "mod+alt+[",
+ id: "session.share",
+ title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
+ description: info()?.share?.url
+ ? language.t("toast.session.share.success.description")
+ : language.t("command.session.share.description"),
+ slash: "share",
disabled: !params.id,
- onSelect: () => navigateMessageByOffset(-1),
+ onSelect: share,
}),
sessionCommand({
- id: "message.next",
- title: language.t("command.message.next"),
- description: language.t("command.message.next.description"),
- keybind: "mod+alt+]",
- disabled: !params.id,
- onSelect: () => navigateMessageByOffset(1),
- }),
- modelCommand({
- id: "model.choose",
- title: language.t("command.model.choose"),
- description: language.t("command.model.choose.description"),
- keybind: "mod+'",
- slash: "model",
- onSelect: () => {
- void import("@/components/dialog-select-model").then((x) => {
- dialog.show(() => <x.DialogSelectModel model={local.model} />)
- })
- },
- }),
- mcpCommand({
- id: "mcp.toggle",
- title: language.t("command.mcp.toggle"),
- description: language.t("command.mcp.toggle.description"),
- keybind: "mod+;",
- slash: "mcp",
- onSelect: () => {
- void import("@/components/dialog-select-mcp").then((x) => {
- dialog.show(() => <x.DialogSelectMcp />)
- })
- },
- }),
- agentCommand({
- id: "agent.cycle",
- title: language.t("command.agent.cycle"),
- description: language.t("command.agent.cycle.description"),
- keybind: "mod+.",
- slash: "agent",
- onSelect: () => local.agent.move(1),
- }),
- agentCommand({
- id: "agent.cycle.reverse",
- title: language.t("command.agent.cycle.reverse"),
- description: language.t("command.agent.cycle.reverse.description"),
- keybind: "shift+mod+.",
- onSelect: () => local.agent.move(-1),
+ id: "session.unshare",
+ title: language.t("command.session.unshare"),
+ description: language.t("command.session.unshare.description"),
+ slash: "unshare",
+ disabled: !params.id || !info()?.share?.url,
+ onSelect: unshare,
}),
- modelCommand({
- id: "model.variant.cycle",
- title: language.t("command.model.variant.cycle"),
- description: language.t("command.model.variant.cycle.description"),
- keybind: "shift+mod+d",
- onSelect: () => local.model.variant.cycle(),
- }),
- permissionsCommand({
- id: "permissions.autoaccept",
- title: isAutoAcceptActive()
- ? language.t("command.permissions.autoaccept.disable")
- : language.t("command.permissions.autoaccept.enable"),
- keybind: "mod+shift+a",
- disabled: false,
- onSelect: () => {
- const sessionID = params.id
- if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
- else permission.toggleAutoAcceptDirectory(sdk.directory)
-
- const active = sessionID
- ? permission.isAutoAccepting(sessionID, sdk.directory)
- : permission.isAutoAcceptingDirectory(sdk.directory)
- showToast({
- title: active
- ? language.t("toast.permissions.autoaccept.on.title")
- : language.t("toast.permissions.autoaccept.off.title"),
- description: active
- ? language.t("toast.permissions.autoaccept.on.description")
- : language.t("toast.permissions.autoaccept.off.description"),
- })
- },
- }),
- sessionCommand({
- id: "session.undo",
- title: language.t("command.session.undo"),
- description: language.t("command.session.undo.description"),
- slash: "undo",
- disabled: !params.id || visibleUserMessages().length === 0,
- onSelect: async () => {
- const sessionID = params.id
- if (!sessionID) return
- if (status().type !== "idle") {
- await sdk.client.session.abort({ sessionID }).catch(() => {})
- }
- const revert = info()?.revert?.messageID
- const message = findLast(userMessages(), (x) => !revert || x.id < revert)
- if (!message) return
- await sdk.client.session.revert({ sessionID, messageID: message.id })
- const parts = sync.data.part[message.id]
- if (parts) {
- const restored = extractPromptFromParts(parts, { directory: sdk.directory })
- prompt.set(restored)
- }
- const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
- setActiveMessage(priorMessage)
- },
- }),
- sessionCommand({
- id: "session.redo",
- title: language.t("command.session.redo"),
- description: language.t("command.session.redo.description"),
- slash: "redo",
- disabled: !params.id || !info()?.revert?.messageID,
- onSelect: async () => {
- const sessionID = params.id
- if (!sessionID) return
- const revertMessageID = info()?.revert?.messageID
- if (!revertMessageID) return
- const nextMessage = userMessages().find((x) => x.id > revertMessageID)
- if (!nextMessage) {
- await sdk.client.session.unrevert({ sessionID })
- prompt.reset()
- const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
- setActiveMessage(lastMsg)
- return
- }
- await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
- const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
- setActiveMessage(priorMsg)
- },
- }),
- sessionCommand({
- id: "session.compact",
- title: language.t("command.session.compact"),
- description: language.t("command.session.compact.description"),
- slash: "compact",
- disabled: !params.id || visibleUserMessages().length === 0,
- onSelect: async () => {
- const sessionID = params.id
- if (!sessionID) return
- const model = local.model.current()
- if (!model) {
- showToast({
- title: language.t("toast.model.none.title"),
- description: language.t("toast.model.none.description"),
- })
- return
- }
- await sdk.client.session.summarize({
- sessionID,
- modelID: model.id,
- providerID: model.provider.id,
- })
- },
- }),
- sessionCommand({
- id: "session.fork",
- title: language.t("command.session.fork"),
- description: language.t("command.session.fork.description"),
- slash: "fork",
- disabled: !params.id || visibleUserMessages().length === 0,
- onSelect: () => {
- void import("@/components/dialog-fork").then((x) => {
- dialog.show(() => <x.DialogFork />)
- })
- },
- }),
- ...share,
]
- })
+ }
+
+ const sessionCmds = () => [
+ sessionCommand({
+ id: "session.new",
+ title: language.t("command.session.new"),
+ keybind: "mod+shift+s",
+ slash: "new",
+ onSelect: () => navigate(`/${params.dir}/session`),
+ }),
+ sessionCommand({
+ id: "session.undo",
+ title: language.t("command.session.undo"),
+ description: language.t("command.session.undo.description"),
+ slash: "undo",
+ disabled: !params.id || visibleUserMessages().length === 0,
+ onSelect: undo,
+ }),
+ sessionCommand({
+ id: "session.redo",
+ title: language.t("command.session.redo"),
+ description: language.t("command.session.redo.description"),
+ slash: "redo",
+ disabled: !params.id || !info()?.revert?.messageID,
+ onSelect: redo,
+ }),
+ sessionCommand({
+ id: "session.compact",
+ title: language.t("command.session.compact"),
+ description: language.t("command.session.compact.description"),
+ slash: "compact",
+ disabled: !params.id || visibleUserMessages().length === 0,
+ onSelect: compact,
+ }),
+ sessionCommand({
+ id: "session.fork",
+ title: language.t("command.session.fork"),
+ description: language.t("command.session.fork.description"),
+ slash: "fork",
+ disabled: !params.id || visibleUserMessages().length === 0,
+ onSelect: fork,
+ }),
+ ]
+
+ const fileCmds = () => [
+ fileCommand({
+ id: "file.open",
+ title: language.t("command.file.open"),
+ description: language.t("palette.search.placeholder"),
+ keybind: "mod+k,mod+p",
+ slash: "open",
+ onSelect: openFile,
+ }),
+ fileCommand({
+ id: "tab.close",
+ title: language.t("command.tab.close"),
+ keybind: "mod+w",
+ disabled: !closableTab(),
+ onSelect: closeTab,
+ }),
+ ]
+
+ const contextCmds = () => [
+ contextCommand({
+ id: "context.addSelection",
+ title: language.t("command.context.addSelection"),
+ description: language.t("command.context.addSelection.description"),
+ keybind: "mod+shift+l",
+ disabled: !canAddSelectionContext(),
+ onSelect: addSelection,
+ }),
+ ]
+
+ const viewCmds = () => [
+ viewCommand({
+ id: "terminal.toggle",
+ title: language.t("command.terminal.toggle"),
+ keybind: "ctrl+`",
+ slash: "terminal",
+ onSelect: () => view().terminal.toggle(),
+ }),
+ viewCommand({
+ id: "review.toggle",
+ title: language.t("command.review.toggle"),
+ keybind: "mod+shift+r",
+ onSelect: () => view().reviewPanel.toggle(),
+ }),
+ viewCommand({
+ id: "fileTree.toggle",
+ title: language.t("command.fileTree.toggle"),
+ keybind: "mod+\\",
+ onSelect: () => layout.fileTree.toggle(),
+ }),
+ viewCommand({
+ id: "input.focus",
+ title: language.t("command.input.focus"),
+ keybind: "ctrl+l",
+ onSelect: focusInput,
+ }),
+ ]
+
+ const terminalCmds = () => [
+ terminalCommand({
+ id: "terminal.new",
+ title: language.t("command.terminal.new"),
+ description: language.t("command.terminal.new.description"),
+ keybind: "ctrl+alt+t",
+ onSelect: openTerminal,
+ }),
+ ]
+
+ const messageCmds = () => [
+ sessionCommand({
+ id: "message.previous",
+ title: language.t("command.message.previous"),
+ description: language.t("command.message.previous.description"),
+ keybind: "mod+alt+[",
+ disabled: !params.id,
+ onSelect: () => navigateMessageByOffset(-1),
+ }),
+ sessionCommand({
+ id: "message.next",
+ title: language.t("command.message.next"),
+ description: language.t("command.message.next.description"),
+ keybind: "mod+alt+]",
+ disabled: !params.id,
+ onSelect: () => navigateMessageByOffset(1),
+ }),
+ ]
+
+ const modelCmds = () => [
+ modelCommand({
+ id: "model.choose",
+ title: language.t("command.model.choose"),
+ description: language.t("command.model.choose.description"),
+ keybind: "mod+'",
+ slash: "model",
+ onSelect: chooseModel,
+ }),
+ modelCommand({
+ id: "model.variant.cycle",
+ title: language.t("command.model.variant.cycle"),
+ description: language.t("command.model.variant.cycle.description"),
+ keybind: "shift+mod+d",
+ onSelect: () => local.model.variant.cycle(),
+ }),
+ ]
+
+ const mcpCmds = () => [
+ mcpCommand({
+ id: "mcp.toggle",
+ title: language.t("command.mcp.toggle"),
+ description: language.t("command.mcp.toggle.description"),
+ keybind: "mod+;",
+ slash: "mcp",
+ onSelect: chooseMcp,
+ }),
+ ]
+
+ const agentCmds = () => [
+ agentCommand({
+ id: "agent.cycle",
+ title: language.t("command.agent.cycle"),
+ description: language.t("command.agent.cycle.description"),
+ keybind: "mod+.",
+ slash: "agent",
+ onSelect: () => local.agent.move(1),
+ }),
+ agentCommand({
+ id: "agent.cycle.reverse",
+ title: language.t("command.agent.cycle.reverse"),
+ description: language.t("command.agent.cycle.reverse.description"),
+ keybind: "shift+mod+.",
+ onSelect: () => local.agent.move(-1),
+ }),
+ ]
+
+ const permissionsCmds = () => [
+ permissionsCommand({
+ id: "permissions.autoaccept",
+ title: isAutoAcceptActive()
+ ? language.t("command.permissions.autoaccept.disable")
+ : language.t("command.permissions.autoaccept.enable"),
+ keybind: "mod+shift+a",
+ disabled: false,
+ onSelect: toggleAutoAccept,
+ }),
+ ]
+
+ command.register("session", () => [
+ ...sessionCmds(),
+ ...shareCmds(),
+ ...fileCmds(),
+ ...contextCmds(),
+ ...viewCmds(),
+ ...terminalCmds(),
+ ...messageCmds(),
+ ...modelCmds(),
+ ...mcpCmds(),
+ ...agentCmds(),
+ ...permissionsCmds(),
+ ])
}