diff options
| author | Adam <[email protected]> | 2026-03-30 08:50:42 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-30 08:50:42 -0500 |
| commit | c2f78224ae59263eada831051a6ece1c65126b1a (patch) | |
| tree | 23468fdd7e2910266756944aa764f7fdcf696d25 /packages/app/src | |
| parent | 14f9e21d5c3f4e853dee8ca133693dd3b915b634 (diff) | |
| download | opencode-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.tsx | 167 | ||||
| -rw-r--r-- | packages/app/src/pages/session/file-tabs.tsx | 273 | ||||
| -rw-r--r-- | packages/app/src/pages/session/use-session-commands.tsx | 808 |
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(), + ]) } |
