Compare commits
2 Commits
v0.13.1-de
...
v0.13.1-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37b3f85e61 | ||
|
|
55a6479c0e |
@@ -2,7 +2,7 @@
|
||||
|
||||
import fs from "fs"
|
||||
import path, { join } from "path"
|
||||
import { execFileSync } from "child_process"
|
||||
import { spawnSync } from "child_process"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
||||
@@ -11,7 +11,8 @@ const workspaceRoot = join(appDir, "..", "..")
|
||||
const serverRoot = join(appDir, "..", "server")
|
||||
const resourcesRoot = join(appDir, "electron", "resources")
|
||||
const serverDest = join(resourcesRoot, "server")
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
|
||||
const npmExecPath = process.env.npm_execpath
|
||||
const npmNodeExecPath = process.env.npm_node_execpath
|
||||
|
||||
const serverSources = ["dist", "public", "node_modules", "package.json"]
|
||||
const serverDepsMarker = join(serverRoot, "node_modules", "fastify", "package.json")
|
||||
@@ -34,27 +35,34 @@ function ensureServerDependencies() {
|
||||
}
|
||||
|
||||
log("installing production server dependencies")
|
||||
execFileSync(
|
||||
npmCmd,
|
||||
[
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--ignore-scripts",
|
||||
"--workspaces=false",
|
||||
"--package-lock=false",
|
||||
"--install-strategy=shallow",
|
||||
"--fund=false",
|
||||
"--audit=false",
|
||||
],
|
||||
{
|
||||
cwd: serverRoot,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${join(workspaceRoot, "node_modules", ".bin")}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
const npmArgs = [
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--ignore-scripts",
|
||||
"--workspaces=false",
|
||||
"--package-lock=false",
|
||||
"--install-strategy=shallow",
|
||||
"--fund=false",
|
||||
"--audit=false",
|
||||
]
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PATH: `${join(workspaceRoot, "node_modules", ".bin")}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
npm_config_workspaces: "false",
|
||||
}
|
||||
|
||||
const npmCli = npmExecPath && npmNodeExecPath ? [npmNodeExecPath, [npmExecPath, ...npmArgs]] : null
|
||||
const result = npmCli
|
||||
? spawnSync(npmCli[0], npmCli[1], { cwd: serverRoot, stdio: "inherit", env })
|
||||
: spawnSync("npm", npmArgs, { cwd: serverRoot, stdio: "inherit", env, shell: process.platform === "win32" })
|
||||
|
||||
if (result.status !== 0) {
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
throw new Error(`npm install exited with code ${result.status ?? 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
function copyServerArtifacts() {
|
||||
|
||||
@@ -81,6 +81,14 @@ export class FileSystemBrowser {
|
||||
return { path: relativePath, absolutePath }
|
||||
}
|
||||
|
||||
writeFile(relativePath: string, contents: string): void {
|
||||
if (this.unrestricted) {
|
||||
throw new Error("writeFile is not available in unrestricted mode")
|
||||
}
|
||||
const resolved = this.toRestrictedAbsolute(relativePath)
|
||||
fs.writeFileSync(resolved, contents, "utf-8")
|
||||
}
|
||||
|
||||
readFile(relativePath: string): string {
|
||||
if (this.unrestricted) {
|
||||
throw new Error("readFile is not available in unrestricted mode")
|
||||
|
||||
@@ -19,6 +19,10 @@ const WorkspaceFileContentQuerySchema = z.object({
|
||||
path: z.string(),
|
||||
})
|
||||
|
||||
const WorkspaceFileContentBodySchema = z.object({
|
||||
contents: z.string(),
|
||||
})
|
||||
|
||||
const WorkspaceFileSearchQuerySchema = z.object({
|
||||
q: z.string().trim().min(1, "Query is required"),
|
||||
limit: z.coerce.number().int().positive().max(200).optional(),
|
||||
@@ -100,6 +104,20 @@ export function registerWorkspaceRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||
return handleWorkspaceError(error, reply)
|
||||
}
|
||||
})
|
||||
|
||||
app.put<{
|
||||
Params: { id: string }
|
||||
Querystring: { path?: string }
|
||||
}>("/api/workspaces/:id/files/content", async (request, reply) => {
|
||||
try {
|
||||
const query = WorkspaceFileContentQuerySchema.parse(request.query ?? {})
|
||||
const body = WorkspaceFileContentBodySchema.parse(request.body ?? {})
|
||||
deps.workspaceManager.writeFile(request.params.id, query.path, body.contents)
|
||||
reply.code(204)
|
||||
} catch (error) {
|
||||
return handleWorkspaceError(error, reply)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,12 @@ export class WorkspaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(workspaceId: string, relativePath: string, contents: string): void {
|
||||
const workspace = this.requireWorkspace(workspaceId)
|
||||
const browser = new FileSystemBrowser({ rootDir: workspace.path })
|
||||
browser.writeFile(relativePath, contents)
|
||||
}
|
||||
|
||||
async create(folder: string, name?: string): Promise<WorkspaceDescriptor> {
|
||||
|
||||
const id = `${Date.now().toString(36)}`
|
||||
|
||||
@@ -108,15 +108,15 @@ const AlertDialog: Component = () => {
|
||||
open
|
||||
modal
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
// Only handle dismiss if dialog is dismissible (default: true)
|
||||
if (!open && payload.dismissible !== false) {
|
||||
dismiss(false, payload)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<Dialog.Content class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||
<Dialog.Overlay class="modal-overlay z-[60]" />
|
||||
<Dialog.Content class="modal-surface fixed left-1/2 top-1/2 z-[1310] w-full max-w-sm -translate-x-1/2 -translate-y-1/2 p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
||||
@@ -140,10 +140,11 @@ const AlertDialog: Component = () => {
|
||||
|
||||
<Show when={isPrompt}>
|
||||
<div class="mt-4">
|
||||
<label class="text-sm font-medium text-secondary">
|
||||
<label for="prompt-input" class="text-sm font-medium text-secondary">
|
||||
{payload.inputLabel || t("alertDialog.prompt.inputLabel")}
|
||||
</label>
|
||||
<input
|
||||
id="prompt-input"
|
||||
ref={(el) => {
|
||||
promptInputRef = el
|
||||
}}
|
||||
@@ -184,11 +185,10 @@ const AlertDialog: Component = () => {
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
|
||||
@@ -9,6 +9,8 @@ interface MonacoFileViewerProps {
|
||||
scopeKey: string
|
||||
path: string
|
||||
content: string
|
||||
onSave?: (content: string) => void
|
||||
onContentChange?: (content: string) => void
|
||||
}
|
||||
|
||||
export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
||||
@@ -33,6 +35,11 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
||||
editor = null
|
||||
}
|
||||
|
||||
const saveContent = () => {
|
||||
if (!editor || !props.onSave) return
|
||||
props.onSave(editor.getValue())
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
@@ -44,7 +51,7 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
||||
editor = monaco.editor.create(host, {
|
||||
value: "",
|
||||
language: "plaintext",
|
||||
readOnly: true,
|
||||
readOnly: false,
|
||||
automaticLayout: true,
|
||||
lineNumbers: "on",
|
||||
minimap: { enabled: false },
|
||||
@@ -54,6 +61,14 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
||||
fontSize: 13,
|
||||
})
|
||||
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveContent)
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
if (props.onContentChange) {
|
||||
props.onContentChange(editor.getValue())
|
||||
}
|
||||
})
|
||||
|
||||
setReady(true)
|
||||
})()
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||
variant: "warning",
|
||||
confirmLabel: t("infoView.dispose.confirm.confirmLabel"),
|
||||
cancelLabel: t("infoView.dispose.confirm.cancelLabel"),
|
||||
dismissible: false,
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
@@ -420,6 +420,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
onClose={closeLeftDrawer}
|
||||
ModalProps={modalProps}
|
||||
sx={{
|
||||
zIndex: 60,
|
||||
"& .MuiDrawer-paper": {
|
||||
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
|
||||
boxSizing: "border-box",
|
||||
@@ -530,6 +531,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
onClose={closeRightDrawer}
|
||||
ModalProps={modalProps}
|
||||
sx={{
|
||||
zIndex: 60,
|
||||
"& .MuiDrawer-paper": {
|
||||
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
|
||||
boxSizing: "border-box",
|
||||
|
||||
@@ -24,6 +24,9 @@ import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } f
|
||||
|
||||
import { getDefaultWorktreeSlug, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "../../../../stores/worktrees"
|
||||
import { requestData } from "../../../../lib/opencode-api"
|
||||
import { serverApi } from "../../../../lib/api-client"
|
||||
import { showConfirmDialog } from "../../../../stores/alerts"
|
||||
import { showToastNotification } from "../../../../lib/notifications"
|
||||
import { buildUnifiedDiffFromSdkPatch, tryReverseApplyUnifiedDiff } from "../../../../lib/unified-diff-reverse"
|
||||
import { useGlobalPointerDrag } from "../useGlobalPointerDrag"
|
||||
import {
|
||||
@@ -102,6 +105,9 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
const [browserSelectedContent, setBrowserSelectedContent] = createSignal<string | null>(null)
|
||||
const [browserSelectedLoading, setBrowserSelectedLoading] = createSignal(false)
|
||||
const [browserSelectedError, setBrowserSelectedError] = createSignal<string | null>(null)
|
||||
const [browserSelectedDirty, setBrowserSelectedDirty] = createSignal(false)
|
||||
const [browserSelectedSaving, setBrowserSelectedSaving] = createSignal(false)
|
||||
const [browserSelectedOriginalContent, setBrowserSelectedOriginalContent] = createSignal<string | null>(null)
|
||||
|
||||
const [diffViewMode, setDiffViewMode] = createSignal<DiffViewMode>(
|
||||
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY, ["split", "unified"] as const) ?? "unified",
|
||||
@@ -539,6 +545,8 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
setBrowserSelectedLoading(true)
|
||||
setBrowserSelectedError(null)
|
||||
setBrowserSelectedContent(null)
|
||||
setBrowserSelectedDirty(false)
|
||||
setBrowserSelectedOriginalContent(null)
|
||||
|
||||
// Phone: treat file selection as a commit action and close the overlay.
|
||||
if (props.isPhoneLayout()) {
|
||||
@@ -559,6 +567,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
throw new Error("Unsupported file type")
|
||||
}
|
||||
setBrowserSelectedContent(text)
|
||||
setBrowserSelectedOriginalContent(text) // Track original content for conflict detection
|
||||
} catch (error) {
|
||||
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to read file")
|
||||
} finally {
|
||||
@@ -566,6 +575,95 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveBrowserFile = async (content: string): Promise<boolean> => {
|
||||
const path = browserSelectedPath()
|
||||
if (!path) return false
|
||||
|
||||
// Check for conflict: agent edited file while user was editing
|
||||
const originalContent = browserSelectedOriginalContent()
|
||||
if (originalContent !== null) {
|
||||
try {
|
||||
const currentDiskContent = await requestData<FileContent>(
|
||||
browserClient().file.read({ path }),
|
||||
"file.read",
|
||||
)
|
||||
const diskContent = (currentDiskContent as any)?.content
|
||||
|
||||
// If disk content differs from what we originally loaded (agent edit)
|
||||
// AND differs from user's current edits, we have a conflict
|
||||
if (diskContent !== originalContent && diskContent !== content) {
|
||||
const confirmed = await showConfirmDialog(
|
||||
props.t("instanceShell.rightPanel.actions.conflict.message", { path }),
|
||||
{
|
||||
variant: "warning",
|
||||
confirmLabel: props.t("instanceShell.rightPanel.actions.conflict.confirmLabel"),
|
||||
cancelLabel: props.t("instanceShell.rightPanel.actions.conflict.cancelLabel"),
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
if (!confirmed) {
|
||||
return false
|
||||
}
|
||||
// User chose to overwrite, proceed with save
|
||||
}
|
||||
} catch {
|
||||
// If we can't check for conflict, proceed with save
|
||||
}
|
||||
}
|
||||
|
||||
setBrowserSelectedSaving(true)
|
||||
try {
|
||||
await serverApi.writeWorkspaceFile(props.instanceId, path, content)
|
||||
setBrowserSelectedContent(content)
|
||||
setBrowserSelectedOriginalContent(content) // Update original to match saved
|
||||
setBrowserSelectedDirty(false)
|
||||
showToastNotification({
|
||||
message: props.t("instanceShell.rightPanel.toast.saveSuccess"),
|
||||
variant: "success",
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to save file")
|
||||
showToastNotification({
|
||||
message: props.t("instanceShell.rightPanel.toast.saveError"),
|
||||
variant: "error",
|
||||
})
|
||||
return false
|
||||
} finally {
|
||||
setBrowserSelectedSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBrowserFileChange = (content: string) => {
|
||||
setBrowserSelectedContent(content)
|
||||
setBrowserSelectedDirty(true)
|
||||
}
|
||||
|
||||
const handleOpenBrowserFileRequest = async (path: string) => {
|
||||
if (browserSelectedDirty()) {
|
||||
const confirmed = await showConfirmDialog(
|
||||
props.t("instanceShell.rightPanel.actions.saveConfirm.message", { path: browserSelectedPath() || "" }),
|
||||
{
|
||||
variant: "warning",
|
||||
confirmLabel: props.t("instanceShell.rightPanel.actions.saveConfirm.confirmLabel"),
|
||||
cancelLabel: props.t("instanceShell.rightPanel.actions.saveConfirm.cancelLabel"),
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
if (confirmed) {
|
||||
const saveSuccess = await saveBrowserFile(browserSelectedContent() || "")
|
||||
if (!saveSuccess) {
|
||||
// Save failed - stay on current file, error toast already shown
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// User chose not to save - clear dirty state and discard edits
|
||||
setBrowserSelectedDirty(false)
|
||||
}
|
||||
}
|
||||
await openBrowserFile(path)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (rightPanelTab() !== "files") return
|
||||
if (browserLoading()) return
|
||||
@@ -578,6 +676,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
setBrowserSelectedContent(null)
|
||||
setBrowserSelectedLoading(false)
|
||||
setBrowserSelectedError(null)
|
||||
setBrowserSelectedDirty(false)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -630,6 +729,22 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
}
|
||||
|
||||
const refreshFilesTab = async () => {
|
||||
// Prompt for confirmation if file has unsaved changes
|
||||
if (browserSelectedDirty()) {
|
||||
const confirmed = await showConfirmDialog(
|
||||
props.t("instanceShell.rightPanel.actions.refreshDirty.message"),
|
||||
{
|
||||
variant: "warning",
|
||||
confirmLabel: props.t("instanceShell.rightPanel.actions.refreshDirty.confirmLabel"),
|
||||
cancelLabel: props.t("instanceShell.rightPanel.actions.refreshDirty.cancelLabel"),
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
void loadBrowserEntries(browserPath())
|
||||
const selected = browserSelectedPath()
|
||||
if (selected) {
|
||||
@@ -651,6 +766,8 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
throw new Error("Unsupported file type")
|
||||
}
|
||||
setBrowserSelectedContent(text)
|
||||
setBrowserSelectedOriginalContent(text) // Update original content after refresh
|
||||
setBrowserSelectedDirty(false) // Clear dirty after refresh
|
||||
} catch (error) {
|
||||
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to read file")
|
||||
} finally {
|
||||
@@ -830,11 +947,15 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
browserSelectedContent={browserSelectedContent}
|
||||
browserSelectedLoading={browserSelectedLoading}
|
||||
browserSelectedError={browserSelectedError}
|
||||
browserSelectedDirty={browserSelectedDirty}
|
||||
browserSelectedSaving={browserSelectedSaving}
|
||||
parentPath={browserParentPath}
|
||||
scopeKey={browserScopeKey}
|
||||
onLoadEntries={(path: string) => void loadBrowserEntries(path)}
|
||||
onOpenFile={(path: string) => void openBrowserFile(path)}
|
||||
onRequestOpenFile={(path: string) => void handleOpenBrowserFileRequest(path)}
|
||||
onRefresh={() => void refreshFilesTab()}
|
||||
onSave={(content: string) => void saveBrowserFile(content)}
|
||||
onContentChange={(content: string) => handleBrowserFileChange(content)}
|
||||
listOpen={filesListOpen}
|
||||
onToggleList={toggleFilesList}
|
||||
splitWidth={filesSplitWidth}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { For, Show, Suspense, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
import { RefreshCw } from "lucide-solid"
|
||||
import { RefreshCw, Save } from "lucide-solid"
|
||||
|
||||
import SplitFilePanel from "../components/SplitFilePanel"
|
||||
|
||||
@@ -21,13 +21,17 @@ interface FilesTabProps {
|
||||
browserSelectedContent: Accessor<string | null>
|
||||
browserSelectedLoading: Accessor<boolean>
|
||||
browserSelectedError: Accessor<string | null>
|
||||
browserSelectedDirty: Accessor<boolean>
|
||||
browserSelectedSaving: Accessor<boolean>
|
||||
|
||||
parentPath: Accessor<string | null>
|
||||
scopeKey: Accessor<string>
|
||||
|
||||
onLoadEntries: (path: string) => void
|
||||
onOpenFile: (path: string) => void
|
||||
onRequestOpenFile: (path: string) => void
|
||||
onRefresh: () => void
|
||||
onSave: (content: string) => void
|
||||
onContentChange: (content: string) => void
|
||||
|
||||
listOpen: Accessor<boolean>
|
||||
onToggleList: () => void
|
||||
@@ -38,6 +42,13 @@ interface FilesTabProps {
|
||||
}
|
||||
|
||||
const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
const handleSave = () => {
|
||||
const content = props.browserSelectedContent()
|
||||
if (content !== undefined && content !== null) {
|
||||
props.onSave(content)
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = (): JSX.Element => {
|
||||
const entriesValue = props.browserEntries()
|
||||
const entries = entriesValue || []
|
||||
@@ -86,7 +97,13 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LazyMonacoFileViewer scopeKey={props.scopeKey()} path={payload().path} content={payload().content} />
|
||||
<LazyMonacoFileViewer
|
||||
scopeKey={props.scopeKey()}
|
||||
path={payload().path}
|
||||
content={payload().content}
|
||||
onSave={props.onSave}
|
||||
onContentChange={props.onContentChange}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</Show>
|
||||
@@ -135,7 +152,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
props.onLoadEntries(item.path)
|
||||
return
|
||||
}
|
||||
props.onOpenFile(item.path)
|
||||
props.onRequestOpenFile(item.path)
|
||||
}}
|
||||
title={item.path}
|
||||
>
|
||||
@@ -168,14 +185,25 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
</Show>
|
||||
<Show when={props.browserError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="files-header-icon-button"
|
||||
title={props.t("instanceShell.rightPanel.actions.save") || "Save (Ctrl+S)"}
|
||||
aria-label={props.t("instanceShell.rightPanel.actions.save") || "Save"}
|
||||
disabled={props.browserSelectedSaving() || !props.browserSelectedDirty()}
|
||||
style={{ "margin-inline-start": "auto" }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Show when={props.browserSelectedSaving()} fallback={<Save class="h-4 w-4" />}>
|
||||
<RefreshCw class="h-4 w-4 animate-spin" />
|
||||
</Show>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="files-header-icon-button"
|
||||
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||
disabled={props.browserLoading()}
|
||||
style={{ "margin-inline-start": "auto" }}
|
||||
onClick={() => props.onRefresh()}
|
||||
>
|
||||
<RefreshCw class={`h-4 w-4${props.browserLoading() ? " animate-spin" : ""}`} />
|
||||
@@ -198,4 +226,4 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
||||
return <>{renderContent()}</>
|
||||
}
|
||||
|
||||
export default FilesTab
|
||||
export default FilesTab
|
||||
@@ -98,6 +98,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
||||
variant: "warning",
|
||||
confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"),
|
||||
cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"),
|
||||
dismissible: false,
|
||||
})
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -157,6 +157,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
variant: "warning",
|
||||
confirmLabel: t("sessionList.delete.confirmLabel"),
|
||||
cancelLabel: t("sessionList.delete.cancelLabel"),
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
if (!confirmed) return
|
||||
@@ -285,6 +286,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
variant: "warning",
|
||||
confirmLabel: t("sessionList.bulkDelete.confirmLabel"),
|
||||
cancelLabel: t("sessionList.bulkDelete.cancelLabel"),
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export const RemoteAccessSettingsSection: Component = () => {
|
||||
variant: "warning",
|
||||
confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"),
|
||||
cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"),
|
||||
dismissible: false,
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
@@ -234,6 +234,16 @@ export const serverApi = {
|
||||
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
|
||||
)
|
||||
},
|
||||
writeWorkspaceFile(id: string, relativePath: string, contents: string): Promise<void> {
|
||||
const params = new URLSearchParams({ path: relativePath })
|
||||
return request(
|
||||
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ contents }),
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
fetchConfigOwner<T extends Record<string, any> = Record<string, any>>(owner: string): Promise<T> {
|
||||
return request<T>(`/api/storage/config/${encodeURIComponent(owner)}`)
|
||||
|
||||
@@ -95,6 +95,18 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.status": "Status",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
||||
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
||||
"instanceShell.rightPanel.actions.save": "Save (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "Do you want to save changes to \"{path}\" before switching?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Save",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Discard Changes",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "File was modified by the agent. Overwrite agent's changes?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Overwrite",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Cancel",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "File has unsaved changes. Refresh will discard your edits. Continue?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Refresh",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancel",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "File saved successfully",
|
||||
"instanceShell.rightPanel.toast.saveError": "Failed to save file",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
|
||||
@@ -94,6 +94,19 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.files": "Archivos",
|
||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||
"instanceShell.rightPanel.actions.refresh": "Actualizar",
|
||||
"instanceShell.rightPanel.actions.save": "Guardar (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "¿Deseas guardar los cambios en \"{path}\" antes de cambiar?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Guardar",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Descartar cambios",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "El archivo fue modificado por el agente. ¿Sobrescribir los cambios del agente?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Sobrescribir",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Cancelar",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "El archivo tiene cambios sin guardar. Actualizar discardará tus ediciones. ¿Continuar?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Actualizar",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancelar",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "Archivo guardado exitosamente",
|
||||
"instanceShell.rightPanel.toast.saveError": "Error al guardar el archivo",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
|
||||
@@ -94,6 +94,19 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.files": "Fichiers",
|
||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||
"instanceShell.rightPanel.actions.refresh": "Actualiser",
|
||||
"instanceShell.rightPanel.actions.save": "Enregistrer (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "Voulez-vous enregistrer les modifications de \"{path}\" avant de changer ?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Enregistrer",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Annuler les modifications",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "Le fichier a été modifié par l'agent. Écraser les modifications de l'agent ?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Écraser",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Annuler",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "Le fichier a des modifications non enregistrées. Actualiser supprimera vos modifications. Continuer ?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Actualiser",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Annuler",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "Fichier enregistré avec succès",
|
||||
"instanceShell.rightPanel.toast.saveError": "Échec de l'enregistrement du fichier",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
|
||||
@@ -95,6 +95,18 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.status": "סטטוס",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "לשוניות לוח ימני",
|
||||
"instanceShell.rightPanel.actions.refresh": "רענן",
|
||||
"instanceShell.rightPanel.actions.save": "שמור (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "האם ברצונך לשמור את השינויים לפני המעבר?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "שמור",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "בטל שינויים",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "הקובץ שונה על ידי הסוכן. לדרוס את שינויי הסוכן?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "דרוס",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "בטל",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "לקובץ יש שינויים שלא נשמרו. רענון יבטל את העריכות שלך. להמשיך?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "רענן",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "בטל",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "הקובץ נשמר בהצלחה",
|
||||
"instanceShell.rightPanel.toast.saveError": "כשלון בשמירת הקובץ",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
||||
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
||||
|
||||
@@ -94,6 +94,19 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.files": "ファイル",
|
||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||
"instanceShell.rightPanel.actions.refresh": "更新",
|
||||
"instanceShell.rightPanel.actions.save": "保存 (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "「{path}」への変更を切り替え前に保存しますか?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "保存",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "変更を破棄",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "ファイルはエージェントによって変更されました。上書きしますか?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "上書き",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "キャンセル",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "ファイルには未保存の変更があります。更新すると編集が破棄されます。続行しますか?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "更新",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "キャンセル",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "ファイルを保存しました",
|
||||
"instanceShell.rightPanel.toast.saveError": "ファイルの保存に失敗しました",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||
"instanceShell.rightPanel.sections.plan": "計画",
|
||||
|
||||
@@ -94,6 +94,19 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.files": "Файлы",
|
||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||
"instanceShell.rightPanel.actions.refresh": "Обновить",
|
||||
"instanceShell.rightPanel.actions.save": "Сохранить (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "Сохранить изменения в \"{path}\" перед переключением?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Сохранить",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Отменить изменения",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "Файл был изменён агентом. Перезаписать изменения агента?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Перезаписать",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Отмена",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "Файл имеет несохранённые изменения. Обновление отменит ваши правки. Продолжить?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Обновить",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Отмена",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "Файл успешно сохранён",
|
||||
"instanceShell.rightPanel.toast.saveError": "Не удалось сохранить файл",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||
"instanceShell.rightPanel.sections.plan": "План",
|
||||
|
||||
@@ -94,6 +94,19 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.files": "文件",
|
||||
"instanceShell.rightPanel.tabs.status": "状态",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||
"instanceShell.rightPanel.actions.refresh": "刷新",
|
||||
"instanceShell.rightPanel.actions.save": "保存 (Ctrl+S)",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.message": "切换前是否保存对 \"{path}\" 的更改?",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "保存",
|
||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "放弃更改",
|
||||
"instanceShell.rightPanel.actions.conflict.message": "文件已被代理修改。是否覆盖代理的更改?",
|
||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "覆盖",
|
||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "取消",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.message": "文件有未保存的更改。刷新将放弃您的编辑。继续?",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "刷新",
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "取消",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "文件保存成功",
|
||||
"instanceShell.rightPanel.toast.saveError": "保存文件失败",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||
"instanceShell.rightPanel.sections.plan": "计划",
|
||||
|
||||
@@ -10,6 +10,8 @@ export type AlertDialogState = {
|
||||
variant?: AlertVariant
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
/** When false, prevents dismissal via Escape key or backdrop click. Default: true */
|
||||
dismissible?: boolean
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
|
||||
|
||||
@@ -673,6 +673,7 @@ async function cleanupBlankSessions(instanceId: string, excludeSessionId?: strin
|
||||
detail: tGlobal("sessionState.cleanup.deepConfirm.detail"),
|
||||
confirmLabel: tGlobal("sessionState.cleanup.deepConfirm.confirmLabel"),
|
||||
cancelLabel: tGlobal("sessionState.cleanup.deepConfirm.cancelLabel"),
|
||||
dismissible: false,
|
||||
}
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
Reference in New Issue
Block a user