|
@@ -1,4 +1,3 @@
|
|
|
-import { createMemo } from "solid-js"
|
|
|
|
|
import { useNavigate } from "@solidjs/router"
|
|
import { useNavigate } from "@solidjs/router"
|
|
|
import { useCommand, type CommandOption } from "@/context/command"
|
|
import { useCommand, type CommandOption } from "@/context/command"
|
|
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
@@ -18,6 +17,7 @@ import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
|
|
import { DialogFork } from "@/components/dialog-fork"
|
|
import { DialogFork } from "@/components/dialog-fork"
|
|
|
import { showToast } from "@opencode-ai/ui/toast"
|
|
import { showToast } from "@opencode-ai/ui/toast"
|
|
|
import { findLast } from "@opencode-ai/util/array"
|
|
import { findLast } from "@opencode-ai/util/array"
|
|
|
|
|
+import { createSessionTabs } from "@/pages/session/helpers"
|
|
|
import { extractPromptFromParts } from "@/utils/prompt"
|
|
import { extractPromptFromParts } from "@/utils/prompt"
|
|
|
import { UserMessage } from "@opencode-ai/sdk/v2"
|
|
import { UserMessage } from "@opencode-ai/sdk/v2"
|
|
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
|
@@ -26,6 +26,7 @@ export type SessionCommandContext = {
|
|
|
navigateMessageByOffset: (offset: number) => void
|
|
navigateMessageByOffset: (offset: number) => void
|
|
|
setActiveMessage: (message: UserMessage | undefined) => void
|
|
setActiveMessage: (message: UserMessage | undefined) => void
|
|
|
focusInput: () => void
|
|
focusInput: () => void
|
|
|
|
|
+ review?: () => boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const withCategory = (category: string) => {
|
|
const withCategory = (category: string) => {
|
|
@@ -50,17 +51,43 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|
|
const navigate = useNavigate()
|
|
const navigate = useNavigate()
|
|
|
const { params, tabs, view } = useSessionLayout()
|
|
const { params, tabs, view } = useSessionLayout()
|
|
|
|
|
|
|
|
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
|
|
|
|
|
|
+ const info = () => {
|
|
|
|
|
+ const id = params.id
|
|
|
|
|
+ if (!id) return
|
|
|
|
|
+ return sync.session.get(id)
|
|
|
|
|
+ }
|
|
|
|
|
+ const hasReview = () => {
|
|
|
|
|
+ const id = params.id
|
|
|
|
|
+ if (!id) return false
|
|
|
|
|
+ return Math.max(info()?.summary?.files ?? 0, (sync.data.session_diff[id] ?? []).length) > 0
|
|
|
|
|
+ }
|
|
|
|
|
+ const normalizeTab = (tab: string) => {
|
|
|
|
|
+ if (!tab.startsWith("file://")) return tab
|
|
|
|
|
+ return file.tab(tab)
|
|
|
|
|
+ }
|
|
|
|
|
+ const tabState = createSessionTabs({
|
|
|
|
|
+ tabs,
|
|
|
|
|
+ pathFromTab: file.pathFromTab,
|
|
|
|
|
+ normalizeTab,
|
|
|
|
|
+ review: actions.review,
|
|
|
|
|
+ hasReview,
|
|
|
|
|
+ })
|
|
|
|
|
+ const activeFileTab = tabState.activeFileTab
|
|
|
|
|
+ const closableTab = tabState.closableTab
|
|
|
|
|
|
|
|
const idle = { type: "idle" as const }
|
|
const idle = { type: "idle" as const }
|
|
|
- const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
|
|
|
|
- const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
|
|
|
|
- const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[])
|
|
|
|
|
- const visibleUserMessages = createMemo(() => {
|
|
|
|
|
|
|
+ const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
|
|
|
|
+ const messages = () => {
|
|
|
|
|
+ const id = params.id
|
|
|
|
|
+ if (!id) return []
|
|
|
|
|
+ return sync.data.message[id] ?? []
|
|
|
|
|
+ }
|
|
|
|
|
+ const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
|
|
|
|
|
+ const visibleUserMessages = () => {
|
|
|
const revert = info()?.revert?.messageID
|
|
const revert = info()?.revert?.messageID
|
|
|
if (!revert) return userMessages()
|
|
if (!revert) return userMessages()
|
|
|
return userMessages().filter((m) => m.id < revert)
|
|
return userMessages().filter((m) => m.id < revert)
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const showAllFiles = () => {
|
|
const showAllFiles = () => {
|
|
|
if (layout.fileTree.tab() !== "changes") return
|
|
if (layout.fileTree.tab() !== "changes") return
|
|
@@ -79,9 +106,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const canAddSelectionContext = () => {
|
|
const canAddSelectionContext = () => {
|
|
|
- const active = tabs().active()
|
|
|
|
|
- if (!active) return false
|
|
|
|
|
- const path = file.pathFromTab(active)
|
|
|
|
|
|
|
+ const tab = activeFileTab()
|
|
|
|
|
+ if (!tab) return false
|
|
|
|
|
+ const path = file.pathFromTab(tab)
|
|
|
if (!path) return false
|
|
if (!path) return false
|
|
|
return file.selectedLines(path) != null
|
|
return file.selectedLines(path) != null
|
|
|
}
|
|
}
|
|
@@ -100,404 +127,369 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|
|
const agentCommand = withCategory(language.t("command.category.agent"))
|
|
const agentCommand = withCategory(language.t("command.category.agent"))
|
|
|
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
|
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
|
|
|
|
|
|
|
- const sessionCommands = createMemo(() => [
|
|
|
|
|
- sessionCommand({
|
|
|
|
|
- id: "session.new",
|
|
|
|
|
- title: language.t("command.session.new"),
|
|
|
|
|
- keybind: "mod+shift+s",
|
|
|
|
|
- slash: "new",
|
|
|
|
|
- onSelect: () => navigate(`/${params.dir}/session`),
|
|
|
|
|
- }),
|
|
|
|
|
- ])
|
|
|
|
|
-
|
|
|
|
|
- const fileCommands = createMemo(() => [
|
|
|
|
|
- fileCommand({
|
|
|
|
|
- id: "file.open",
|
|
|
|
|
- title: language.t("command.file.open"),
|
|
|
|
|
- description: language.t("palette.search.placeholder"),
|
|
|
|
|
- keybind: "mod+p",
|
|
|
|
|
- slash: "open",
|
|
|
|
|
- onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
|
|
|
|
- }),
|
|
|
|
|
- fileCommand({
|
|
|
|
|
- id: "tab.close",
|
|
|
|
|
- title: language.t("command.tab.close"),
|
|
|
|
|
- keybind: "mod+w",
|
|
|
|
|
- disabled: !tabs().active(),
|
|
|
|
|
- onSelect: () => {
|
|
|
|
|
- const active = tabs().active()
|
|
|
|
|
- if (!active) return
|
|
|
|
|
- tabs().close(active)
|
|
|
|
|
- },
|
|
|
|
|
- }),
|
|
|
|
|
- ])
|
|
|
|
|
-
|
|
|
|
|
- const contextCommands = createMemo(() => [
|
|
|
|
|
- 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 active = tabs().active()
|
|
|
|
|
- if (!active) return
|
|
|
|
|
- const path = file.pathFromTab(active)
|
|
|
|
|
- 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 viewCommands = createMemo(() => [
|
|
|
|
|
- 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()
|
|
|
|
|
- },
|
|
|
|
|
- }),
|
|
|
|
|
- ])
|
|
|
|
|
-
|
|
|
|
|
- const messageCommands = createMemo(() => [
|
|
|
|
|
- sessionCommand({
|
|
|
|
|
- id: "message.previous",
|
|
|
|
|
- title: language.t("command.message.previous"),
|
|
|
|
|
- description: language.t("command.message.previous.description"),
|
|
|
|
|
- keybind: "mod+arrowup",
|
|
|
|
|
- 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+arrowdown",
|
|
|
|
|
- disabled: !params.id,
|
|
|
|
|
- onSelect: () => navigateMessageByOffset(1),
|
|
|
|
|
- }),
|
|
|
|
|
- ])
|
|
|
|
|
-
|
|
|
|
|
- const agentCommands = createMemo(() => [
|
|
|
|
|
- modelCommand({
|
|
|
|
|
- id: "model.choose",
|
|
|
|
|
- title: language.t("command.model.choose"),
|
|
|
|
|
- description: language.t("command.model.choose.description"),
|
|
|
|
|
- keybind: "mod+'",
|
|
|
|
|
- slash: "model",
|
|
|
|
|
- onSelect: () => dialog.show(() => <DialogSelectModel />),
|
|
|
|
|
- }),
|
|
|
|
|
- mcpCommand({
|
|
|
|
|
- id: "mcp.toggle",
|
|
|
|
|
- title: language.t("command.mcp.toggle"),
|
|
|
|
|
- description: language.t("command.mcp.toggle.description"),
|
|
|
|
|
- keybind: "mod+;",
|
|
|
|
|
- slash: "mcp",
|
|
|
|
|
- onSelect: () => dialog.show(() => <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),
|
|
|
|
|
- }),
|
|
|
|
|
- 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 isAutoAcceptActive = () => {
|
|
const isAutoAcceptActive = () => {
|
|
|
const sessionID = params.id
|
|
const sessionID = params.id
|
|
|
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 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 permissionCommands = createMemo(() => [
|
|
|
|
|
- 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"),
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
- }),
|
|
|
|
|
- ])
|
|
|
|
|
|
|
+ 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 sessionActionCommands = createMemo(() => [
|
|
|
|
|
- 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: () => dialog.show(() => <DialogFork />),
|
|
|
|
|
- }),
|
|
|
|
|
- ])
|
|
|
|
|
|
|
+ const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
|
|
|
|
+ if (!clipboard?.writeText) return Promise.resolve(false)
|
|
|
|
|
+ return clipboard.writeText(value).then(
|
|
|
|
|
+ () => true,
|
|
|
|
|
+ () => false,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const shareCommands = createMemo(() => {
|
|
|
|
|
- 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: async () => {
|
|
|
|
|
- if (!params.id) return
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- 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)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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 clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
|
|
|
|
- if (!clipboard?.writeText) return Promise.resolve(false)
|
|
|
|
|
- return clipboard.writeText(value).then(
|
|
|
|
|
- () => true,
|
|
|
|
|
- () => false,
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const existing = info()?.share?.url
|
|
|
|
|
+ if (existing) {
|
|
|
|
|
+ await copy(existing, true)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- 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
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ }),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ 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+p",
|
|
|
|
|
+ slash: "open",
|
|
|
|
|
+ onSelect: () => dialog.show(() => <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({
|
|
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",
|
|
|
|
|
|
|
+ title: language.t("toast.context.noLineSelection.title"),
|
|
|
|
|
+ description: language.t("toast.context.noLineSelection.description"),
|
|
|
})
|
|
})
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const existing = info()?.share?.url
|
|
|
|
|
- if (existing) {
|
|
|
|
|
- await copy(existing, true)
|
|
|
|
|
|
|
+ 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+arrowup",
|
|
|
|
|
+ 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+arrowdown",
|
|
|
|
|
+ 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: () => dialog.show(() => <DialogSelectModel />),
|
|
|
|
|
+ }),
|
|
|
|
|
+ mcpCommand({
|
|
|
|
|
+ id: "mcp.toggle",
|
|
|
|
|
+ title: language.t("command.mcp.toggle"),
|
|
|
|
|
+ description: language.t("command.mcp.toggle.description"),
|
|
|
|
|
+ keybind: "mod+;",
|
|
|
|
|
+ slash: "mcp",
|
|
|
|
|
+ onSelect: () => dialog.show(() => <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),
|
|
|
|
|
+ }),
|
|
|
|
|
+ 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
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- const url = await sdk.client.session
|
|
|
|
|
- .share({ sessionID: params.id })
|
|
|
|
|
- .then((res) => res.data?.share?.url)
|
|
|
|
|
- .catch(() => undefined)
|
|
|
|
|
- if (!url) {
|
|
|
|
|
|
|
+ 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({
|
|
showToast({
|
|
|
- title: language.t("toast.session.share.failed.title"),
|
|
|
|
|
- description: language.t("toast.session.share.failed.description"),
|
|
|
|
|
- variant: "error",
|
|
|
|
|
|
|
+ title: language.t("toast.model.none.title"),
|
|
|
|
|
+ description: language.t("toast.model.none.description"),
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- await copy(url, false)
|
|
|
|
|
|
|
+ await sdk.client.session.summarize({
|
|
|
|
|
+ sessionID,
|
|
|
|
|
+ modelID: model.id,
|
|
|
|
|
+ providerID: model.provider.id,
|
|
|
|
|
+ })
|
|
|
},
|
|
},
|
|
|
}),
|
|
}),
|
|
|
sessionCommand({
|
|
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",
|
|
|
|
|
- }),
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ 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: () => dialog.show(() => <DialogFork />),
|
|
|
}),
|
|
}),
|
|
|
|
|
+ ...share,
|
|
|
]
|
|
]
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
- command.register("session", () =>
|
|
|
|
|
- [
|
|
|
|
|
- sessionCommands(),
|
|
|
|
|
- fileCommands(),
|
|
|
|
|
- contextCommands(),
|
|
|
|
|
- viewCommands(),
|
|
|
|
|
- messageCommands(),
|
|
|
|
|
- agentCommands(),
|
|
|
|
|
- permissionCommands(),
|
|
|
|
|
- sessionActionCommands(),
|
|
|
|
|
- shareCommands(),
|
|
|
|
|
- ].flatMap((x) => x),
|
|
|
|
|
- )
|
|
|
|
|
}
|
|
}
|