chore(app): cleanup (#20062)
This commit is contained in:
@@ -11,6 +11,47 @@ import { useSDK } from "@/context/sdk"
|
|||||||
|
|
||||||
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
|
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) => {
|
export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -41,6 +82,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
return language.t("session.question.progress", { current: n, total: total() })
|
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 last = createMemo(() => store.tab >= total() - 1)
|
||||||
|
|
||||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
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 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) => {
|
const pick = (answer: string, custom: boolean = false) => {
|
||||||
setStore("answers", store.tab, [answer])
|
setStore("answers", store.tab, [answer])
|
||||||
if (custom) setStore("custom", store.tab, answer)
|
if (custom) setStore("custom", store.tab, answer)
|
||||||
@@ -230,6 +281,24 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
customUpdate(input())
|
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 = () => {
|
const next = () => {
|
||||||
if (sending()) return
|
if (sending()) return
|
||||||
if (store.editing) commitCustom()
|
if (store.editing) commitCustom()
|
||||||
@@ -270,10 +339,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
type="button"
|
type="button"
|
||||||
data-slot="question-progress-segment"
|
data-slot="question-progress-segment"
|
||||||
data-active={i() === store.tab}
|
data-active={i() === store.tab}
|
||||||
data-answered={
|
data-answered={answered(i())}
|
||||||
(store.answers[i()]?.length ?? 0) > 0 ||
|
|
||||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
|
||||||
}
|
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={() => jump(i())}
|
onClick={() => jump(i())}
|
||||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||||
@@ -307,43 +373,23 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
</Show>
|
</Show>
|
||||||
<div data-slot="question-options">
|
<div data-slot="question-options">
|
||||||
<For each={options()}>
|
<For each={options()}>
|
||||||
{(opt, i) => {
|
{(opt, i) => (
|
||||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
<Option
|
||||||
return (
|
multi={multi()}
|
||||||
<button
|
picked={picked(opt.label)}
|
||||||
data-slot="question-option"
|
label={opt.label}
|
||||||
data-picked={picked()}
|
description={opt.description}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
|
||||||
aria-checked={picked()}
|
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={() => selectOption(i())}
|
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>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={store.editing}
|
when={store.editing}
|
||||||
fallback={
|
fallback={
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-custom="true"
|
data-custom="true"
|
||||||
data-picked={on()}
|
data-picked={on()}
|
||||||
@@ -352,24 +398,10 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={customOpen}
|
onClick={customOpen}
|
||||||
>
|
>
|
||||||
<span
|
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
||||||
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>
|
|
||||||
<span data-slot="question-option-main">
|
<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>
|
||||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
<span data-slot="option-description">{input() || customPlaceholder()}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -394,33 +426,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
commitCustom()
|
commitCustom()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
||||||
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>
|
|
||||||
<span data-slot="question-option-main">
|
<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
|
<textarea
|
||||||
ref={(el) =>
|
ref={focusCustom}
|
||||||
setTimeout(() => {
|
|
||||||
el.focus()
|
|
||||||
el.style.height = "0px"
|
|
||||||
el.style.height = `${el.scrollHeight}px`
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
data-slot="question-custom-input"
|
data-slot="question-custom-input"
|
||||||
placeholder={language.t("ui.question.custom.placeholder")}
|
placeholder={customPlaceholder()}
|
||||||
value={input()}
|
value={input()}
|
||||||
rows={1}
|
rows={1}
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
@@ -436,8 +448,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
}}
|
}}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
customUpdate(e.currentTarget.value)
|
customUpdate(e.currentTarget.value)
|
||||||
e.currentTarget.style.height = "0px"
|
resizeInput(e.currentTarget)
|
||||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -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 }) {
|
export function FileTabContent(props: { tab: string }) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
@@ -65,11 +191,6 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
}).activeFileTab
|
}).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
|
let find: FileSearchHandle | null = null
|
||||||
|
|
||||||
const search = {
|
const search = {
|
||||||
@@ -92,6 +213,10 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
|
if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null
|
||||||
return (getSessionHandoff(sessionKey())?.files[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) => {
|
const selectionPreview = (source: string, selection: FileSelection) => {
|
||||||
return previewSelectedLines(source, {
|
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: {
|
const addCommentToContext = (input: {
|
||||||
file: string
|
file: string
|
||||||
selection: SelectedLineRange
|
selection: SelectedLineRange
|
||||||
@@ -108,14 +239,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
origin?: "review" | "file"
|
origin?: "review" | "file"
|
||||||
}) => {
|
}) => {
|
||||||
const selection = selectionFromLines(input.selection)
|
const selection = selectionFromLines(input.selection)
|
||||||
const preview =
|
const preview = input.preview ?? buildPreview(input.file, selection)
|
||||||
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 saved = comments.add({
|
const saved = comments.add({
|
||||||
file: input.file,
|
file: input.file,
|
||||||
@@ -140,8 +264,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
comment: string
|
comment: string
|
||||||
}) => {
|
}) => {
|
||||||
comments.update(input.file, input.id, input.comment)
|
comments.update(input.file, input.id, input.comment)
|
||||||
const preview =
|
const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined
|
||||||
input.file === path() ? selectionPreview(contents(), selectionFromLines(input.selection)) : undefined
|
|
||||||
prompt.context.updateComment(input.file, input.id, {
|
prompt.context.updateComment(input.file, input.id, {
|
||||||
comment: input.comment,
|
comment: input.comment,
|
||||||
...(preview ? { preview } : {}),
|
...(preview ? { preview } : {}),
|
||||||
@@ -260,102 +383,6 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
requestAnimationFrame(() => comments.clearFocus())
|
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 cancelCommenting = () => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (p) file.setSelectedLines(p, null)
|
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)
|
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
|
||||||
prev = { loaded, ready, active }
|
prev = { loaded, ready, active }
|
||||||
if (!restore) return
|
if (!restore) return
|
||||||
queueRestore()
|
scrollSync.queueRestore()
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
for (const item of codeScroll) {
|
|
||||||
item.removeEventListener("scroll", handleCodeScroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
|
|
||||||
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderFile = (source: string) => (
|
const renderFile = (source: string) => (
|
||||||
@@ -402,7 +420,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
selectedLines={activeSelection()}
|
selectedLines={activeSelection()}
|
||||||
commentedLines={commentedLines()}
|
commentedLines={commentedLines()}
|
||||||
onRendered={() => {
|
onRendered={() => {
|
||||||
queueRestore()
|
scrollSync.queueRestore()
|
||||||
}}
|
}}
|
||||||
annotations={commentsUi.annotations()}
|
annotations={commentsUi.annotations()}
|
||||||
renderAnnotation={commentsUi.renderAnnotation}
|
renderAnnotation={commentsUi.renderAnnotation}
|
||||||
@@ -420,7 +438,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
mode: "auto",
|
mode: "auto",
|
||||||
path: path(),
|
path: path(),
|
||||||
current: state()?.content,
|
current: state()?.content,
|
||||||
onLoad: queueRestore,
|
onLoad: scrollSync.queueRestore,
|
||||||
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
||||||
if (args.kind !== "svg") return
|
if (args.kind !== "svg") return
|
||||||
showToast({
|
showToast({
|
||||||
@@ -435,14 +453,7 @@ export function FileTabContent(props: { tab: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
|
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
|
||||||
<ScrollView
|
<ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
|
||||||
class="h-full"
|
|
||||||
viewportRef={(el: HTMLDivElement) => {
|
|
||||||
scroll = el
|
|
||||||
restoreScroll()
|
|
||||||
}}
|
|
||||||
onScroll={handleScroll as any}
|
|
||||||
>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
||||||
<Match when={state()?.loading}>
|
<Match when={state()?.loading}>
|
||||||
|
|||||||
@@ -128,25 +128,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
|
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
|
||||||
return permission.isAutoAcceptingDirectory(sdk.directory)
|
return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||||
}
|
}
|
||||||
command.register("session", () => {
|
const write = async (value: string) => {
|
||||||
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
|
const body = typeof document === "undefined" ? undefined : document.body
|
||||||
if (body) {
|
if (body) {
|
||||||
const textarea = document.createElement("textarea")
|
const textarea = document.createElement("textarea")
|
||||||
@@ -159,20 +141,19 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
textarea.select()
|
textarea.select()
|
||||||
const copied = document.execCommand("copy")
|
const copied = document.execCommand("copy")
|
||||||
body.removeChild(textarea)
|
body.removeChild(textarea)
|
||||||
if (copied) return Promise.resolve(true)
|
if (copied) return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||||
if (!clipboard?.writeText) return Promise.resolve(false)
|
if (!clipboard?.writeText) return false
|
||||||
return clipboard.writeText(value).then(
|
return clipboard.writeText(value).then(
|
||||||
() => true,
|
() => true,
|
||||||
() => false,
|
() => false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const copy = async (url: string, existing: boolean) => {
|
const copyShare = async (url: string, existing: boolean) => {
|
||||||
const ok = await write(url)
|
if (!(await write(url))) {
|
||||||
if (!ok) {
|
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("toast.session.share.copyFailed.title"),
|
title: language.t("toast.session.share.copyFailed.title"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -181,22 +162,24 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
title: existing
|
title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"),
|
||||||
? language.t("session.share.copy.copied")
|
|
||||||
: language.t("toast.session.share.success.title"),
|
|
||||||
description: language.t("toast.session.share.success.description"),
|
description: language.t("toast.session.share.success.description"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const share = async () => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
const existing = info()?.share?.url
|
const existing = info()?.share?.url
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await copy(existing, true)
|
await copyShare(existing, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await sdk.client.session
|
const url = await sdk.client.session
|
||||||
.share({ sessionID: params.id })
|
.share({ sessionID })
|
||||||
.then((res) => res.data?.share?.url)
|
.then((res) => res.data?.share?.url)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -208,19 +191,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await copy(url, false)
|
await copyShare(url, false)
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
sessionCommand({
|
const unshare = async () => {
|
||||||
id: "session.unshare",
|
const sessionID = params.id
|
||||||
title: language.t("command.session.unshare"),
|
if (!sessionID) return
|
||||||
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
|
await sdk.client.session
|
||||||
.unshare({ sessionID: params.id })
|
.unshare({ sessionID })
|
||||||
.then(() =>
|
.then(() =>
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("toast.session.unshare.success.title"),
|
title: language.t("toast.session.unshare.success.title"),
|
||||||
@@ -235,50 +214,24 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
variant: "error",
|
variant: "error",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
return [
|
const openFile = () => {
|
||||||
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) => {
|
void import("@/components/dialog-select-file").then((x) => {
|
||||||
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />)
|
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />)
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
fileCommand({
|
const closeTab = () => {
|
||||||
id: "tab.close",
|
|
||||||
title: language.t("command.tab.close"),
|
|
||||||
keybind: "mod+w",
|
|
||||||
disabled: !closableTab(),
|
|
||||||
onSelect: () => {
|
|
||||||
const tab = closableTab()
|
const tab = closableTab()
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
tabs().close(tab)
|
tabs().close(tab)
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
contextCommand({
|
const addSelection = () => {
|
||||||
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()
|
const tab = activeFileTab()
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
|
|
||||||
const path = file.pathFromTab(tab)
|
const path = file.pathFromTab(tab)
|
||||||
if (!path) return
|
if (!path) return
|
||||||
|
|
||||||
@@ -292,8 +245,209 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSelectionToContext(path, selectionFromLines(range))
|
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.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: share,
|
||||||
}),
|
}),
|
||||||
|
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: unshare,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
viewCommand({
|
||||||
id: "terminal.toggle",
|
id: "terminal.toggle",
|
||||||
title: language.t("command.terminal.toggle"),
|
title: language.t("command.terminal.toggle"),
|
||||||
@@ -319,16 +473,19 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "ctrl+l",
|
keybind: "ctrl+l",
|
||||||
onSelect: focusInput,
|
onSelect: focusInput,
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const terminalCmds = () => [
|
||||||
terminalCommand({
|
terminalCommand({
|
||||||
id: "terminal.new",
|
id: "terminal.new",
|
||||||
title: language.t("command.terminal.new"),
|
title: language.t("command.terminal.new"),
|
||||||
description: language.t("command.terminal.new.description"),
|
description: language.t("command.terminal.new.description"),
|
||||||
keybind: "ctrl+alt+t",
|
keybind: "ctrl+alt+t",
|
||||||
onSelect: () => {
|
onSelect: openTerminal,
|
||||||
if (terminal.all().length > 0) terminal.new()
|
|
||||||
view().terminal.open()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const messageCmds = () => [
|
||||||
sessionCommand({
|
sessionCommand({
|
||||||
id: "message.previous",
|
id: "message.previous",
|
||||||
title: language.t("command.message.previous"),
|
title: language.t("command.message.previous"),
|
||||||
@@ -345,30 +502,38 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
disabled: !params.id,
|
disabled: !params.id,
|
||||||
onSelect: () => navigateMessageByOffset(1),
|
onSelect: () => navigateMessageByOffset(1),
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const modelCmds = () => [
|
||||||
modelCommand({
|
modelCommand({
|
||||||
id: "model.choose",
|
id: "model.choose",
|
||||||
title: language.t("command.model.choose"),
|
title: language.t("command.model.choose"),
|
||||||
description: language.t("command.model.choose.description"),
|
description: language.t("command.model.choose.description"),
|
||||||
keybind: "mod+'",
|
keybind: "mod+'",
|
||||||
slash: "model",
|
slash: "model",
|
||||||
onSelect: () => {
|
onSelect: chooseModel,
|
||||||
void import("@/components/dialog-select-model").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogSelectModel model={local.model} />)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
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({
|
mcpCommand({
|
||||||
id: "mcp.toggle",
|
id: "mcp.toggle",
|
||||||
title: language.t("command.mcp.toggle"),
|
title: language.t("command.mcp.toggle"),
|
||||||
description: language.t("command.mcp.toggle.description"),
|
description: language.t("command.mcp.toggle.description"),
|
||||||
keybind: "mod+;",
|
keybind: "mod+;",
|
||||||
slash: "mcp",
|
slash: "mcp",
|
||||||
onSelect: () => {
|
onSelect: chooseMcp,
|
||||||
void import("@/components/dialog-select-mcp").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogSelectMcp />)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const agentCmds = () => [
|
||||||
agentCommand({
|
agentCommand({
|
||||||
id: "agent.cycle",
|
id: "agent.cycle",
|
||||||
title: language.t("command.agent.cycle"),
|
title: language.t("command.agent.cycle"),
|
||||||
@@ -384,13 +549,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
modelCommand({
|
]
|
||||||
id: "model.variant.cycle",
|
|
||||||
title: language.t("command.model.variant.cycle"),
|
const permissionsCmds = () => [
|
||||||
description: language.t("command.model.variant.cycle.description"),
|
|
||||||
keybind: "shift+mod+d",
|
|
||||||
onSelect: () => local.model.variant.cycle(),
|
|
||||||
}),
|
|
||||||
permissionsCommand({
|
permissionsCommand({
|
||||||
id: "permissions.autoaccept",
|
id: "permissions.autoaccept",
|
||||||
title: isAutoAcceptActive()
|
title: isAutoAcceptActive()
|
||||||
@@ -398,110 +559,21 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
: language.t("command.permissions.autoaccept.enable"),
|
: language.t("command.permissions.autoaccept.enable"),
|
||||||
keybind: "mod+shift+a",
|
keybind: "mod+shift+a",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onSelect: () => {
|
onSelect: 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"),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
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,
|
|
||||||
]
|
]
|
||||||
})
|
|
||||||
|
command.register("session", () => [
|
||||||
|
...sessionCmds(),
|
||||||
|
...shareCmds(),
|
||||||
|
...fileCmds(),
|
||||||
|
...contextCmds(),
|
||||||
|
...viewCmds(),
|
||||||
|
...terminalCmds(),
|
||||||
|
...messageCmds(),
|
||||||
|
...modelCmds(),
|
||||||
|
...mcpCmds(),
|
||||||
|
...agentCmds(),
|
||||||
|
...permissionsCmds(),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user