Compare commits
4 Commits
feat/linux
...
v0.14.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e022a158eb | ||
|
|
9d9a6a79ec | ||
|
|
82a7c95dba | ||
|
|
313a0e579e |
@@ -19,12 +19,60 @@ interface MonacoDiffViewerProps {
|
||||
insertContextLabel?: string
|
||||
}
|
||||
|
||||
function getLineCount(value: string): number {
|
||||
if (!value) return 1
|
||||
return value.split("\n").length
|
||||
}
|
||||
|
||||
function getDigitCount(value: number): number {
|
||||
return String(Math.max(1, value)).length
|
||||
}
|
||||
|
||||
function getUnifiedGutterSizing(options: { before: string; after: string }) {
|
||||
const beforeLineCount = getLineCount(options.before)
|
||||
const afterLineCount = getLineCount(options.after)
|
||||
const beforeDigitCount = getDigitCount(beforeLineCount)
|
||||
const afterDigitCount = getDigitCount(afterLineCount)
|
||||
const maxDigitCount = Math.max(beforeDigitCount, afterDigitCount)
|
||||
const extraDigits = Math.max(0, maxDigitCount - 2)
|
||||
const beforeNumberChars = Math.max(2, beforeDigitCount)
|
||||
const afterNumberChars = Math.max(2, afterDigitCount)
|
||||
const fourDigitPenalty = Math.max(0, maxDigitCount - 3)
|
||||
|
||||
return {
|
||||
diffEditorLineNumbersMinChars: Math.max(beforeNumberChars, afterNumberChars),
|
||||
originalLineNumbersMinChars: beforeNumberChars,
|
||||
modifiedLineNumbersMinChars: afterNumberChars,
|
||||
lineDecorationsWidth: 6 + extraDigits * 2 + fourDigitPenalty * 2,
|
||||
}
|
||||
}
|
||||
|
||||
function getSplitGutterSizing(options: { before: string; after: string }) {
|
||||
const beforeLineCount = getLineCount(options.before)
|
||||
const afterLineCount = getLineCount(options.after)
|
||||
const beforeDigitCount = getDigitCount(beforeLineCount)
|
||||
const afterDigitCount = getDigitCount(afterLineCount)
|
||||
const maxDigitCount = Math.max(beforeDigitCount, afterDigitCount)
|
||||
const extraDigits = Math.max(0, maxDigitCount - 2)
|
||||
const beforeNumberChars = Math.max(2, beforeDigitCount)
|
||||
const afterNumberChars = Math.max(2, afterDigitCount)
|
||||
const fourDigitPenalty = Math.max(0, maxDigitCount - 3)
|
||||
|
||||
return {
|
||||
diffEditorLineNumbersMinChars: Math.max(beforeNumberChars, afterNumberChars),
|
||||
originalLineNumbersMinChars: beforeNumberChars,
|
||||
modifiedLineNumbersMinChars: afterNumberChars,
|
||||
lineDecorationsWidth: 8 + extraDigits * 2 + fourDigitPenalty,
|
||||
}
|
||||
}
|
||||
|
||||
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
const { isDark } = useTheme()
|
||||
let host: HTMLDivElement | undefined
|
||||
|
||||
let diffEditor: any = null
|
||||
let monaco: any = null
|
||||
let splitLayoutFrame: number | null = null
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [hoveredLine, setHoveredLine] = createSignal<number | null>(null)
|
||||
const [selectedRange, setSelectedRange] = createSignal<{ startLine: number; endLine: number } | null>(null)
|
||||
@@ -55,6 +103,44 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
diffEditor = null
|
||||
}
|
||||
|
||||
const clearSplitLayoutVariables = () => {
|
||||
if (!host) return
|
||||
host.style.removeProperty("--split-original-line-number-width")
|
||||
host.style.removeProperty("--split-original-delete-sign-left")
|
||||
host.style.removeProperty("--split-original-gutter-width")
|
||||
}
|
||||
|
||||
const syncSplitLayoutVariables = (options: {
|
||||
viewMode: "split" | "unified"
|
||||
originalLineNumbersMinChars: number
|
||||
lineDecorationsWidth: number
|
||||
}) => {
|
||||
if (!host) return
|
||||
if (splitLayoutFrame !== null && typeof window !== "undefined") {
|
||||
window.cancelAnimationFrame(splitLayoutFrame)
|
||||
splitLayoutFrame = null
|
||||
}
|
||||
if (options.viewMode !== "split" || typeof window === "undefined") {
|
||||
clearSplitLayoutVariables()
|
||||
return
|
||||
}
|
||||
|
||||
splitLayoutFrame = window.requestAnimationFrame(() => {
|
||||
splitLayoutFrame = null
|
||||
if (!host) return
|
||||
const originalLineNumbers = host.querySelector<HTMLElement>(".editor.original .line-numbers")
|
||||
const measuredWidth = originalLineNumbers?.getBoundingClientRect().width ?? 0
|
||||
const lineNumberWidth =
|
||||
measuredWidth > 0 ? measuredWidth : Math.max(12, options.originalLineNumbersMinChars * 6)
|
||||
host.style.setProperty("--split-original-line-number-width", `${lineNumberWidth}px`)
|
||||
host.style.setProperty("--split-original-delete-sign-left", `${lineNumberWidth}px`)
|
||||
host.style.setProperty(
|
||||
"--split-original-gutter-width",
|
||||
`${lineNumberWidth + options.lineDecorationsWidth}px`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const getModifiedEditor = () => diffEditor?.getModifiedEditor?.() ?? null
|
||||
|
||||
const getActiveInsertRange = () => {
|
||||
@@ -120,7 +206,7 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
renderWhitespace: "selection",
|
||||
fontSize: 13,
|
||||
wordWrap: props.wordWrap === "on" ? "on" : "off",
|
||||
glyphMargin: true,
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
||||
lineNumbersMinChars: 4,
|
||||
@@ -139,6 +225,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
|
||||
onCleanup(() => {
|
||||
cancelled = true
|
||||
if (splitLayoutFrame !== null && typeof window !== "undefined") {
|
||||
window.cancelAnimationFrame(splitLayoutFrame)
|
||||
splitLayoutFrame = null
|
||||
}
|
||||
clearSplitLayoutVariables()
|
||||
setReady(false)
|
||||
disposeEditor()
|
||||
})
|
||||
@@ -149,6 +240,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
monaco.editor.setTheme(isDark() ? "vs-dark" : "vs")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!host) return
|
||||
host.dataset.viewMode = props.viewMode === "split" ? "split" : "unified"
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready() || !monaco || !diffEditor) return
|
||||
const modifiedEditor = diffEditor.getModifiedEditor?.()
|
||||
@@ -222,10 +318,23 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
const viewMode = props.viewMode === "unified" ? "unified" : "split"
|
||||
const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded"
|
||||
const wordWrap = props.wordWrap === "on" ? "on" : "off"
|
||||
|
||||
const { before, after } = resolvedContent()
|
||||
const sizing =
|
||||
viewMode === "unified"
|
||||
? getUnifiedGutterSizing({ before, after })
|
||||
: getSplitGutterSizing({ before, after })
|
||||
const {
|
||||
diffEditorLineNumbersMinChars,
|
||||
originalLineNumbersMinChars,
|
||||
modifiedLineNumbersMinChars,
|
||||
lineDecorationsWidth,
|
||||
} = sizing
|
||||
diffEditor.updateOptions({
|
||||
renderSideBySide: viewMode === "split",
|
||||
renderSideBySideInlineBreakpoint: 0,
|
||||
renderIndicators: true,
|
||||
lineNumbersMinChars: diffEditorLineNumbersMinChars,
|
||||
lineDecorationsWidth,
|
||||
hideUnchangedRegions:
|
||||
contextMode === "collapsed"
|
||||
? { enabled: true }
|
||||
@@ -234,16 +343,30 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
})
|
||||
|
||||
try {
|
||||
diffEditor.getOriginalEditor?.()?.updateOptions?.({ wordWrap })
|
||||
diffEditor.getOriginalEditor?.()?.updateOptions?.({
|
||||
wordWrap,
|
||||
lineNumbersMinChars: originalLineNumbersMinChars,
|
||||
lineDecorationsWidth,
|
||||
})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
diffEditor.getModifiedEditor?.()?.updateOptions?.({ wordWrap })
|
||||
diffEditor.getModifiedEditor?.()?.updateOptions?.({
|
||||
wordWrap,
|
||||
lineNumbersMinChars: modifiedLineNumbersMinChars,
|
||||
lineDecorationsWidth,
|
||||
})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
syncSplitLayoutVariables({
|
||||
viewMode,
|
||||
originalLineNumbersMinChars,
|
||||
lineDecorationsWidth,
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -384,6 +384,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
||||
onContextModeChange={props.onContextModeChange}
|
||||
onWordWrapModeChange={props.onWordWrapModeChange}
|
||||
/>
|
||||
|
||||
</>
|
||||
}
|
||||
list={{ panel: renderGroupedList, overlay: renderGroupedList }}
|
||||
|
||||
@@ -581,113 +581,6 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
autoCapitalize="off"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="prompt-nav-buttons">
|
||||
<div class="prompt-nav-column prompt-nav-column-left">
|
||||
<Show when={showVoiceInput()}>
|
||||
<button
|
||||
type="button"
|
||||
class={`prompt-voice-button prompt-nav-voice-button ${voiceInput.isRecording() ? "is-recording" : ""}`}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
beginVoicePress(event)
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
event.preventDefault()
|
||||
endVoicePress()
|
||||
}}
|
||||
onPointerCancel={() => endVoicePress()}
|
||||
onLostPointerCapture={() => endVoicePress()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.repeat) return
|
||||
if (event.key !== " " && event.key !== "Enter") return
|
||||
event.preventDefault()
|
||||
beginVoicePress(event)
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key !== " " && event.key !== "Enter") return
|
||||
event.preventDefault()
|
||||
endVoicePress()
|
||||
}}
|
||||
onBlur={() => endVoicePress()}
|
||||
disabled={!voiceInput.isRecording() && (props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput())}
|
||||
aria-label={voiceInput.buttonTitle()}
|
||||
title={voiceInput.buttonTitle()}
|
||||
>
|
||||
<Show
|
||||
when={voiceInput.isRecording()}
|
||||
fallback={
|
||||
<Show when={voiceInput.isTranscribing()} fallback={<Mic class="h-4 w-4" aria-hidden="true" />}>
|
||||
<Loader2 class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Mic class="h-4 w-4" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={showConversationToggle()}>
|
||||
<button
|
||||
type="button"
|
||||
class={`prompt-voice-button prompt-nav-voice-button prompt-conversation-button ${conversationModeEnabled() ? "is-active" : ""}`}
|
||||
onClick={() => toggleConversationMode(props.instanceId)}
|
||||
disabled={!conversationModeEnabled() && !canToggleConversationMode()}
|
||||
aria-pressed={conversationModeEnabled()}
|
||||
aria-label={conversationModeButtonTitle()}
|
||||
title={conversationModeButtonTitle()}
|
||||
>
|
||||
<Volume2 class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-clear-button"
|
||||
onClick={handleClearPrompt}
|
||||
disabled={!canClearPrompt()}
|
||||
aria-label={t("promptInput.clear.ariaLabel")}
|
||||
title={t("promptInput.clear.title")}
|
||||
>
|
||||
<X class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="prompt-nav-column prompt-nav-column-right">
|
||||
<ExpandButton
|
||||
expandState={expandState}
|
||||
onToggleExpand={handleExpandToggle}
|
||||
/>
|
||||
<Show when={hasHistory()}>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-history-button"
|
||||
onClick={() =>
|
||||
selectPreviousHistory({
|
||||
force: true,
|
||||
isPickerOpen: showPicker(),
|
||||
getTextarea: () => textareaRef,
|
||||
})
|
||||
}
|
||||
disabled={!canHistoryGoPrevious()}
|
||||
aria-label={t("promptInput.history.previousAriaLabel")}
|
||||
>
|
||||
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-history-button"
|
||||
onClick={() =>
|
||||
selectNextHistory({
|
||||
force: true,
|
||||
isPickerOpen: showPicker(),
|
||||
getTextarea: () => textareaRef,
|
||||
})
|
||||
}
|
||||
disabled={!canHistoryGoNext()}
|
||||
aria-label={t("promptInput.history.nextAriaLabel")}
|
||||
>
|
||||
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={shouldShowOverlay()}>
|
||||
<div class={`prompt-input-overlay keyboard-hints ${mode() === "shell" ? "shell-mode" : ""}`}>
|
||||
<Show
|
||||
@@ -742,6 +635,116 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
</div>
|
||||
|
||||
<div class="prompt-input-actions">
|
||||
<div class="prompt-nav-buttons">
|
||||
<div class="prompt-nav-column prompt-nav-column-left">
|
||||
<Show when={showVoiceInput()}>
|
||||
<button
|
||||
type="button"
|
||||
class={`prompt-voice-button prompt-nav-voice-button ${voiceInput.isRecording() ? "is-recording" : ""}`}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
beginVoicePress(event)
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
event.preventDefault()
|
||||
endVoicePress()
|
||||
}}
|
||||
onPointerCancel={() => endVoicePress()}
|
||||
onLostPointerCapture={() => endVoicePress()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.repeat) return
|
||||
if (event.key !== " " && event.key !== "Enter") return
|
||||
event.preventDefault()
|
||||
beginVoicePress(event)
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key !== " " && event.key !== "Enter") return
|
||||
event.preventDefault()
|
||||
endVoicePress()
|
||||
}}
|
||||
onBlur={() => endVoicePress()}
|
||||
disabled={!voiceInput.isRecording() && (props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput())}
|
||||
aria-label={voiceInput.buttonTitle()}
|
||||
title={voiceInput.buttonTitle()}
|
||||
>
|
||||
<Show
|
||||
when={voiceInput.isRecording()}
|
||||
fallback={
|
||||
<Show when={voiceInput.isTranscribing()} fallback={<Mic class="h-4 w-4" aria-hidden="true" />}>
|
||||
<Loader2 class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Mic class="h-4 w-4" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={showConversationToggle()}>
|
||||
<button
|
||||
type="button"
|
||||
class={`prompt-voice-button prompt-nav-voice-button prompt-conversation-button ${conversationModeEnabled() ? "is-active" : ""}`}
|
||||
onClick={() => toggleConversationMode(props.instanceId)}
|
||||
disabled={!conversationModeEnabled() && !canToggleConversationMode()}
|
||||
aria-pressed={conversationModeEnabled()}
|
||||
aria-label={conversationModeButtonTitle()}
|
||||
title={conversationModeButtonTitle()}
|
||||
>
|
||||
<Volume2 class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-clear-button"
|
||||
onClick={handleClearPrompt}
|
||||
disabled={!canClearPrompt()}
|
||||
aria-label={t("promptInput.clear.ariaLabel")}
|
||||
title={t("promptInput.clear.title")}
|
||||
>
|
||||
<X class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="prompt-nav-column prompt-nav-column-right">
|
||||
<ExpandButton
|
||||
expandState={expandState}
|
||||
onToggleExpand={handleExpandToggle}
|
||||
/>
|
||||
<Show when={hasHistory()}>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-history-button"
|
||||
onClick={() =>
|
||||
selectPreviousHistory({
|
||||
force: true,
|
||||
isPickerOpen: showPicker(),
|
||||
getTextarea: () => textareaRef,
|
||||
})
|
||||
}
|
||||
disabled={!canHistoryGoPrevious()}
|
||||
aria-label={t("promptInput.history.previousAriaLabel")}
|
||||
>
|
||||
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-history-button"
|
||||
onClick={() =>
|
||||
selectNextHistory({
|
||||
force: true,
|
||||
isPickerOpen: showPicker(),
|
||||
getTextarea: () => textareaRef,
|
||||
})
|
||||
}
|
||||
disabled={!canHistoryGoNext()}
|
||||
aria-label={t("promptInput.history.nextAriaLabel")}
|
||||
>
|
||||
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prompt-input-primary-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="stop-button"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Virtualizer, type VirtualizerHandle } from "virtua/solid"
|
||||
|
||||
const DEFAULT_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||
const DEFAULT_HOLD_TARGET_TOP_THRESHOLD_PX = 8
|
||||
const DEFAULT_HOLD_TARGET_TOP_OVERSHOOT_PX = 128
|
||||
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
||||
const SCROLL_INTENT_KEYS = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"])
|
||||
|
||||
@@ -375,11 +374,11 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
const relativeTop = targetRect.top - containerRect.top
|
||||
const exceedsViewport = targetRect.height > element.clientHeight
|
||||
|
||||
if (
|
||||
exceedsViewport &&
|
||||
relativeTop <= holdTargetTopThresholdPx() &&
|
||||
relativeTop >= holdTargetTopThresholdPx() - DEFAULT_HOLD_TARGET_TOP_OVERSHOOT_PX
|
||||
) {
|
||||
if (exceedsViewport && relativeTop < 0) {
|
||||
const alignDelta = relativeTop - holdTargetTopThresholdPx()
|
||||
if (Math.abs(alignDelta) > 1) {
|
||||
element.scrollTop = Math.max(0, element.scrollTop + alignDelta)
|
||||
}
|
||||
setHeldItemCount(itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ type WorktreeOption =
|
||||
| { kind: "action"; key: "__create__"; label: string }
|
||||
| { kind: "worktree"; key: string; slug: string; directory: string; raw: WorktreeDescriptor }
|
||||
|
||||
type DeleteErrorKind = "localChanges" | "inUse" | "notFound" | "permissionDenied" | "unknown"
|
||||
|
||||
type DeleteErrorDetails = {
|
||||
summary: string
|
||||
causeLabel: string
|
||||
nextStep: string
|
||||
}
|
||||
|
||||
function preventSelectPress(event: PointerEvent | MouseEvent) {
|
||||
// Prevent Select.Item from treating this as a selection.
|
||||
// We intentionally prevent default to stop Kobalte's internal press handling.
|
||||
@@ -64,6 +72,57 @@ function relativePath(fromDir: string, toDir: string): string {
|
||||
return relParts.join("/") || "."
|
||||
}
|
||||
|
||||
function extractDeleteErrorMessage(input: string): string {
|
||||
const trimmed = (input ?? "").trim()
|
||||
if (!trimmed) return ""
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as { error?: unknown }
|
||||
if (typeof parsed?.error === "string" && parsed.error.trim()) {
|
||||
return parsed.error.trim()
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the raw string when the backend returned plain text.
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function classifyDeleteError(message: string): DeleteErrorKind {
|
||||
const normalized = message.toLowerCase()
|
||||
|
||||
if (
|
||||
normalized.includes("modified or untracked files") ||
|
||||
normalized.includes("contains modified") ||
|
||||
normalized.includes("contains untracked") ||
|
||||
normalized.includes("use --force to delete it")
|
||||
) {
|
||||
return "localChanges"
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes("in use") ||
|
||||
normalized.includes("resource busy") ||
|
||||
normalized.includes("device or resource busy") ||
|
||||
normalized.includes("ebusy") ||
|
||||
normalized.includes("file is being used") ||
|
||||
normalized.includes("process cannot access the file") ||
|
||||
normalized.includes("directory not empty")
|
||||
) {
|
||||
return "inUse"
|
||||
}
|
||||
|
||||
if (normalized.includes("not found") || normalized.includes("no such file") || normalized.includes("cannot find")) {
|
||||
return "notFound"
|
||||
}
|
||||
|
||||
if (normalized.includes("permission denied") || normalized.includes("access is denied") || normalized.includes("eperm")) {
|
||||
return "permissionDenied"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
interface WorktreeSelectorProps {
|
||||
instanceId: string
|
||||
sessionId: string
|
||||
@@ -80,6 +139,7 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
const [deleteTarget, setDeleteTarget] = createSignal<WorktreeOption & { kind: "worktree" } | null>(null)
|
||||
const [forceDelete, setForceDelete] = createSignal(false)
|
||||
const [isDeleting, setIsDeleting] = createSignal(false)
|
||||
const [deleteError, setDeleteError] = createSignal<string | null>(null)
|
||||
|
||||
const session = createMemo(() => sessions().get(props.instanceId)?.get(props.sessionId))
|
||||
const isChildSession = createMemo(() => Boolean(session()?.parentId))
|
||||
@@ -114,10 +174,16 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
const openDeleteDialog = (opt: WorktreeOption & { kind: "worktree" }) => {
|
||||
if (opt.slug === "root") return
|
||||
setForceDelete(false)
|
||||
setDeleteError(null)
|
||||
setDeleteTarget(opt)
|
||||
setDeleteOpen(true)
|
||||
}
|
||||
|
||||
const closeDeleteDialog = () => {
|
||||
setDeleteOpen(false)
|
||||
setDeleteError(null)
|
||||
}
|
||||
|
||||
const repoRoot = createMemo(() => {
|
||||
const list = getWorktrees(props.instanceId)
|
||||
return list.find((wt) => wt.slug === "root")?.directory ?? ""
|
||||
@@ -139,6 +205,89 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeDeleteError = (input: string) => {
|
||||
let sanitized = (input ?? "").trim()
|
||||
if (!sanitized) {
|
||||
return t("instanceShell.worktree.delete.error.fallback")
|
||||
}
|
||||
|
||||
sanitized = sanitized.replace(/[A-Za-z]:[\\/][^\r\n"']+/g, "[path]")
|
||||
sanitized = sanitized.replace(/\\Users\\[^\\/\r\n]+/gi, "\\Users\\[user]")
|
||||
sanitized = sanitized.replace(/\/Users\/[^/\r\n]+/g, "/Users/[user]")
|
||||
sanitized = sanitized.replace(/\/home\/[^/\r\n]+/g, "/home/[user]")
|
||||
sanitized = sanitized.replace(/([A-Za-z]:[\\/])?Users[\\/][^\\/\r\n]+/gi, "$1Users/[user]")
|
||||
return sanitized
|
||||
}
|
||||
|
||||
const handleCopyDeleteError = async (mode: "raw" | "sanitized") => {
|
||||
const raw = deleteError()
|
||||
if (!raw) return
|
||||
const text = mode === "sanitized" ? sanitizeDeleteError(raw) : raw
|
||||
|
||||
try {
|
||||
const ok = await copyToClipboard(text)
|
||||
showToastNotification({
|
||||
message: ok
|
||||
? t(mode === "sanitized" ? "instanceShell.worktree.delete.error.copySanitizedSuccess" : "instanceShell.worktree.delete.error.copySuccess")
|
||||
: t("instanceShell.worktree.delete.error.copyFailure"),
|
||||
variant: ok ? "success" : "error",
|
||||
})
|
||||
} catch (error) {
|
||||
log.error("Failed to copy delete worktree error", error)
|
||||
showToastNotification({
|
||||
message: t("instanceShell.worktree.delete.error.copyFailure"),
|
||||
variant: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteErrorDetails = createMemo<DeleteErrorDetails | null>(() => {
|
||||
const raw = deleteError()
|
||||
if (!raw) return null
|
||||
|
||||
const parsed = extractDeleteErrorMessage(raw)
|
||||
const kind = classifyDeleteError(parsed)
|
||||
|
||||
switch (kind) {
|
||||
case "localChanges":
|
||||
return {
|
||||
summary: t("instanceShell.worktree.delete.error.summary.localChanges"),
|
||||
causeLabel: t("instanceShell.worktree.delete.error.cause.localChanges"),
|
||||
nextStep: t("instanceShell.worktree.delete.error.nextStep.localChanges"),
|
||||
}
|
||||
case "inUse":
|
||||
return {
|
||||
summary: t("instanceShell.worktree.delete.error.summary.inUse"),
|
||||
causeLabel: t("instanceShell.worktree.delete.error.cause.inUse"),
|
||||
nextStep: t("instanceShell.worktree.delete.error.nextStep.inUse"),
|
||||
}
|
||||
case "notFound":
|
||||
return {
|
||||
summary: t("instanceShell.worktree.delete.error.summary.notFound"),
|
||||
causeLabel: t("instanceShell.worktree.delete.error.cause.notFound"),
|
||||
nextStep: t("instanceShell.worktree.delete.error.nextStep.notFound"),
|
||||
}
|
||||
case "permissionDenied":
|
||||
return {
|
||||
summary: t("instanceShell.worktree.delete.error.summary.permissionDenied"),
|
||||
causeLabel: t("instanceShell.worktree.delete.error.cause.permissionDenied"),
|
||||
nextStep: t("instanceShell.worktree.delete.error.nextStep.permissionDenied"),
|
||||
}
|
||||
default:
|
||||
return {
|
||||
summary: t("instanceShell.worktree.delete.error.summary.unknown"),
|
||||
causeLabel: t("instanceShell.worktree.delete.error.cause.unknown"),
|
||||
nextStep: t("instanceShell.worktree.delete.error.nextStep.unknown"),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const displayDeleteError = createMemo(() => {
|
||||
const raw = deleteError()
|
||||
if (!raw) return null
|
||||
return extractDeleteErrorMessage(raw)
|
||||
})
|
||||
|
||||
const handleChange = async (value: WorktreeOption | null) => {
|
||||
if (worktreesUnavailable()) return
|
||||
if (!value) return
|
||||
@@ -343,22 +492,23 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={deleteOpen()} onOpenChange={(open) => !open && setDeleteOpen(false)}>
|
||||
<Dialog open={deleteOpen()} onOpenChange={(open) => !open && closeDeleteDialog()}>
|
||||
<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-md p-6 flex flex-col gap-5">
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-3 md:p-4">
|
||||
<Dialog.Content class="modal-surface w-[clamp(640px,45vw,960px)] max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)] overflow-y-auto p-4 flex flex-col gap-3">
|
||||
<div>
|
||||
<Dialog.Title class="text-xl font-semibold text-primary">Delete worktree</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-secondary mt-2">Removes the git worktree checkout directory for this branch.</Dialog.Description>
|
||||
<Dialog.Description class="text-sm text-secondary mt-1">Deletes this branch worktree and its local folder.</Dialog.Description>
|
||||
</div>
|
||||
|
||||
<Show when={deleteTarget()}>
|
||||
{(target) => (
|
||||
<div class="rounded-lg border border-base bg-surface-secondary p-4">
|
||||
<p class="text-xs font-medium text-muted uppercase tracking-wide mb-1">Worktree</p>
|
||||
<p class="text-sm font-mono text-primary break-all">{target().slug}</p>
|
||||
<p class="text-[11px] text-secondary mt-2 break-all font-mono">{target().directory}</p>
|
||||
<div class="rounded-lg border border-base bg-surface-secondary px-3 py-2">
|
||||
<p class="text-sm text-primary">
|
||||
Worktree <span class="font-semibold font-mono">"{target().slug}"</span>
|
||||
</p>
|
||||
<p class="text-[11px] text-secondary break-all font-mono leading-5">{target().directory}</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
@@ -377,7 +527,7 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary"
|
||||
onClick={() => setDeleteOpen(false)}
|
||||
onClick={closeDeleteDialog}
|
||||
disabled={isDeleting()}
|
||||
>
|
||||
Cancel
|
||||
@@ -389,12 +539,13 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
onClick={() => {
|
||||
const target = deleteTarget()
|
||||
if (!target) {
|
||||
setDeleteOpen(false)
|
||||
closeDeleteDialog()
|
||||
return
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
setIsDeleting(true)
|
||||
setDeleteError(null)
|
||||
await deleteWorktree(props.instanceId, target.slug, { force: forceDelete() })
|
||||
await reloadWorktrees(props.instanceId)
|
||||
await reloadWorktreeMap(props.instanceId)
|
||||
@@ -403,15 +554,12 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
await setWorktreeSlugForParentSession(props.instanceId, parentId(), "root")
|
||||
}
|
||||
|
||||
setDeleteOpen(false)
|
||||
closeDeleteDialog()
|
||||
showToastNotification({ message: `Deleted worktree ${target.slug}`, variant: "success" })
|
||||
})()
|
||||
.catch((error) => {
|
||||
log.warn("Failed to delete worktree", error)
|
||||
showToastNotification({
|
||||
message: error instanceof Error ? error.message : "Failed to delete worktree",
|
||||
variant: "error",
|
||||
})
|
||||
setDeleteError(error instanceof Error ? error.message : t("instanceShell.worktree.delete.error.fallback"))
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDeleting(false)
|
||||
@@ -421,6 +569,56 @@ export default function WorktreeSelector(props: WorktreeSelectorProps) {
|
||||
{isDeleting() ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={displayDeleteError()}>
|
||||
{(message) => (
|
||||
<div class="rounded-lg border border-danger bg-danger/10 p-3 flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-xs font-medium text-danger uppercase tracking-wide">
|
||||
{t("instanceShell.worktree.delete.error.title")}
|
||||
</p>
|
||||
<Show when={deleteErrorDetails()}>
|
||||
{(details) => (
|
||||
<>
|
||||
<p class="text-sm text-primary font-medium">{details().summary}</p>
|
||||
<p class="text-sm text-secondary">
|
||||
<span class="font-medium text-primary">{t("instanceShell.worktree.delete.error.causeLabel")}</span>{" "}
|
||||
{details().causeLabel}
|
||||
</p>
|
||||
<p class="text-sm text-secondary">
|
||||
<span class="font-medium text-primary">{t("instanceShell.worktree.delete.error.nextStepLabel")}</span>{" "}
|
||||
{details().nextStep}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<pre class="max-h-[40vh] overflow-auto whitespace-pre-wrap break-all rounded border border-danger/30 bg-surface-primary px-3 py-2 text-xs text-primary select-text leading-5">{message()}</pre>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary"
|
||||
onClick={() => {
|
||||
void handleCopyDeleteError("raw")
|
||||
}}
|
||||
>
|
||||
{t("instanceShell.worktree.delete.error.copyRaw")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary"
|
||||
onClick={() => {
|
||||
void handleCopyDeleteError("sanitized")
|
||||
}}
|
||||
>
|
||||
{t("instanceShell.worktree.delete.error.copySanitized")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
|
||||
@@ -158,6 +158,30 @@ export const instanceMessages = {
|
||||
"instanceShell.diff.enableWordWrap": "Enable word wrap",
|
||||
"instanceShell.diff.disableWordWrap": "Disable word wrap",
|
||||
"instanceShell.worktree.create": "+ Create worktree",
|
||||
"instanceShell.worktree.delete.error.title": "Delete failed",
|
||||
"instanceShell.worktree.delete.error.fallback": "Failed to delete worktree",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "Likely cause:",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "Suggested next step:",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "Git refused to delete this worktree because it has modified or untracked files.",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "CodeNomad could not delete this worktree because something is still using files in the directory.",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "CodeNomad could not delete this worktree because the directory or worktree record was not found.",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "CodeNomad could not delete this worktree because access to the directory was denied.",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad could not delete this worktree.",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "Local changes",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "Another process is using this worktree",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "The worktree directory or record is missing",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "Insufficient filesystem permissions",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "The backend returned an unclassified delete error",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "Enable Force delete if you want to discard local changes, or clean the worktree and try again.",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "Close terminals, editors, watchers, or background processes using this worktree and try again.",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "Refresh worktrees and try again. If it still fails, inspect the worktree path on disk.",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "Check filesystem permissions and close applications that may be locking this directory, then try again.",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "Review the raw error below for details, then retry after addressing the reported problem.",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "Copy error",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "Copy sanitized",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "Copied delete error",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "Copied sanitized delete error",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "Failed to copy delete error",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
||||
"instanceShell.plan.empty": "Nothing planned yet.",
|
||||
|
||||
@@ -166,6 +166,30 @@ export const instanceMessages = {
|
||||
"instanceShell.backgroundProcesses.actions.output": "Salida",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "Detener",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "Terminar",
|
||||
"instanceShell.worktree.delete.error.title": "Error al eliminar",
|
||||
"instanceShell.worktree.delete.error.fallback": "Error al eliminar el worktree",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "Causa probable:",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "Siguiente paso sugerido:",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "Git rechazo la eliminacion de este worktree porque contiene archivos modificados o sin seguimiento.",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "CodeNomad no pudo eliminar este worktree porque algo sigue usando archivos dentro del directorio.",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "CodeNomad no pudo eliminar este worktree porque no se encontro el directorio o el registro del worktree.",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "CodeNomad no pudo eliminar este worktree porque se denego el acceso al directorio.",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad no pudo eliminar este worktree.",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "Cambios locales",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "Otro proceso esta usando este worktree",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "Falta el directorio o el registro del worktree",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "Permisos insuficientes del sistema de archivos",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "El backend devolvio un error de eliminacion sin clasificar",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "Activa Forzar eliminacion si quieres descartar los cambios locales, o limpia el worktree e intentalo de nuevo.",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "Cierra terminales, editores, observadores o procesos en segundo plano que usen este worktree y vuelve a intentarlo.",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "Recarga los worktrees y vuelve a intentarlo. Si sigue fallando, inspecciona la ruta del worktree en disco.",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "Revisa los permisos del sistema de archivos y cierra aplicaciones que puedan estar bloqueando este directorio, luego vuelve a intentarlo.",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "Revisa el error sin procesar de abajo para ver los detalles y vuelve a intentarlo despues de corregir el problema indicado.",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "Copiar error",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "Copiar saneado",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "Error de eliminacion copiado",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "Error de eliminacion saneado copiado",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "No se pudo copiar el error de eliminacion",
|
||||
|
||||
"versionPill.appWithVersion": "App {version}",
|
||||
"versionPill.ui": "UI",
|
||||
|
||||
@@ -166,6 +166,30 @@ export const instanceMessages = {
|
||||
"instanceShell.backgroundProcesses.actions.output": "Sortie",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "Arrêter",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "Terminer",
|
||||
"instanceShell.worktree.delete.error.title": "Echec de suppression",
|
||||
"instanceShell.worktree.delete.error.fallback": "Impossible de supprimer le worktree",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "Cause probable :",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "Etape suivante suggeree :",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "Git a refuse de supprimer ce worktree car il contient des fichiers modifies ou non suivis.",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "CodeNomad n'a pas pu supprimer ce worktree car quelque chose utilise encore des fichiers dans ce dossier.",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "CodeNomad n'a pas pu supprimer ce worktree car le dossier ou l'enregistrement du worktree est introuvable.",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "CodeNomad n'a pas pu supprimer ce worktree car l'acces au dossier a ete refuse.",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad n'a pas pu supprimer ce worktree.",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "Modifications locales",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "Un autre processus utilise ce worktree",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "Le dossier ou l'enregistrement du worktree est manquant",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "Permissions du systeme de fichiers insuffisantes",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "Le backend a renvoye une erreur de suppression non classee",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "Activez la suppression forcee si vous voulez jeter les modifications locales, ou nettoyez le worktree puis reessayez.",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "Fermez les terminaux, editeurs, observateurs ou processus en arrière-plan qui utilisent ce worktree puis reessayez.",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "Rechargez les worktrees puis reessayez. Si cela echoue encore, inspectez le chemin du worktree sur le disque.",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "Verifiez les permissions du systeme de fichiers et fermez les applications qui peuvent verrouiller ce dossier, puis reessayez.",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "Consultez l'erreur brute ci-dessous pour les details, puis reessayez apres avoir corrige le probleme signale.",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "Copier l'erreur",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "Copier la version nettoyee",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "Erreur de suppression copiee",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "Erreur de suppression nettoyee copiee",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "Impossible de copier l'erreur de suppression",
|
||||
|
||||
"versionPill.appWithVersion": "Appli {version}",
|
||||
"versionPill.ui": "UI",
|
||||
|
||||
@@ -174,6 +174,30 @@ export const instanceMessages = {
|
||||
"instanceShell.backgroundProcesses.actions.output": "פלט",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "עצור",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "סיים",
|
||||
"instanceShell.worktree.delete.error.title": "המחיקה נכשלה",
|
||||
"instanceShell.worktree.delete.error.fallback": "מחיקת ה-worktree נכשלה",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "סיבה סבירה:",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "השלב הבא המומלץ:",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "Git סירב למחוק את ה-worktree הזה כי יש בו קבצים ששונו או קבצים לא במעקב.",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "CodeNomad לא הצליח למחוק את ה-worktree הזה כי משהו עדיין משתמש בקבצים שבתיקייה.",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "CodeNomad לא הצליח למחוק את ה-worktree הזה כי התיקייה או רשומת ה-worktree לא נמצאו.",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "CodeNomad לא הצליח למחוק את ה-worktree הזה כי הגישה לתיקייה נדחתה.",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad לא הצליח למחוק את ה-worktree הזה.",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "שינויים מקומיים",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "תהליך אחר משתמש ב-worktree הזה",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "תיקיית ה-worktree או הרשומה שלו חסרות",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "אין הרשאות מתאימות במערכת הקבצים",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "ה-backend החזיר שגיאת מחיקה שלא סווגה",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "הפעילו מחיקה בכפייה אם אתם רוצים לזרוק את השינויים המקומיים, או נקו את ה-worktree ונסו שוב.",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "סגרו טרמינלים, עורכים, watchers או תהליכי רקע שמשתמשים ב-worktree הזה ונסו שוב.",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "רעננו את רשימת ה-worktrees ונסו שוב. אם זה עדיין נכשל, בדקו את נתיב ה-worktree על הדיסק.",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "בדקו את הרשאות מערכת הקבצים וסגרו אפליקציות שעשויות לנעול את התיקייה הזאת, ואז נסו שוב.",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "עיינו בשגיאה הגולמית למטה לפרטים, ואז נסו שוב אחרי טיפול בבעיה שדווחה.",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "העתק שגיאה",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "העתק גרסה מסוננת",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "שגיאת המחיקה הועתקה",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "שגיאת המחיקה המסוננת הועתקה",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "העתקת שגיאת המחיקה נכשלה",
|
||||
|
||||
"versionPill.appWithVersion": "אפליקציה {version}",
|
||||
"versionPill.ui": "ממשק",
|
||||
|
||||
@@ -166,6 +166,30 @@ export const instanceMessages = {
|
||||
"instanceShell.backgroundProcesses.actions.output": "出力",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "停止",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "終了",
|
||||
"instanceShell.worktree.delete.error.title": "削除に失敗しました",
|
||||
"instanceShell.worktree.delete.error.fallback": "worktree の削除に失敗しました",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "考えられる原因:",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "推奨される次の手順:",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "この worktree に変更済みまたは未追跡のファイルがあるため、Git が削除を拒否しました。",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "このディレクトリ内のファイルがまだ使用中のため、CodeNomad はこの worktree を削除できませんでした。",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "ディレクトリまたは worktree レコードが見つからなかったため、CodeNomad はこの worktree を削除できませんでした。",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "ディレクトリへのアクセスが拒否されたため、CodeNomad はこの worktree を削除できませんでした。",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad はこの worktree を削除できませんでした。",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "ローカル変更",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "別のプロセスがこの worktree を使用中です",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "worktree のディレクトリまたは記録が見つかりません",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "ファイルシステム権限が不足しています",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "バックエンドが分類できない削除エラーを返しました",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "ローカル変更を破棄したい場合は Force delete を有効にするか、worktree を整理してから再試行してください。",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "この worktree を使用している端末、エディタ、watcher、バックグラウンドプロセスを閉じてから再試行してください。",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "worktree 一覧を更新して再試行してください。まだ失敗する場合は、ディスク上の worktree パスを確認してください。",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "ファイルシステム権限を確認し、このディレクトリをロックしている可能性のあるアプリを閉じてから再試行してください。",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "下の生エラーで詳細を確認し、報告された問題に対処してから再試行してください。",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "エラーをコピー",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "サニタイズ済みをコピー",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "削除エラーをコピーしました",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "サニタイズ済みの削除エラーをコピーしました",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "削除エラーをコピーできませんでした",
|
||||
|
||||
"versionPill.appWithVersion": "アプリ {version}",
|
||||
"versionPill.ui": "UI",
|
||||
|
||||
@@ -166,6 +166,30 @@ export const instanceMessages = {
|
||||
"instanceShell.backgroundProcesses.actions.output": "Вывод",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "Остановить",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "Завершить",
|
||||
"instanceShell.worktree.delete.error.title": "Удаление не удалось",
|
||||
"instanceShell.worktree.delete.error.fallback": "Не удалось удалить worktree",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "Вероятная причина:",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "Рекомендуемый следующий шаг:",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "Git отказался удалять этот worktree, потому что в нем есть измененные или неотслеживаемые файлы.",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "CodeNomad не смог удалить этот worktree, потому что что-то все еще использует файлы в каталоге.",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "CodeNomad не смог удалить этот worktree, потому что каталог или запись worktree не найдены.",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "CodeNomad не смог удалить этот worktree, потому что доступ к каталогу был запрещен.",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad не смог удалить этот worktree.",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "Локальные изменения",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "Другой процесс использует этот worktree",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "Каталог или запись worktree отсутствуют",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "Недостаточно прав файловой системы",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "Бэкенд вернул неклассифицированную ошибку удаления",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "Включите принудительное удаление, если хотите отбросить локальные изменения, либо очистите worktree и попробуйте снова.",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "Закройте терминалы, редакторы, watcher-процессы или фоновые процессы, использующие этот worktree, и попробуйте снова.",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "Обновите список worktree и попробуйте снова. Если ошибка сохранится, проверьте путь worktree на диске.",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "Проверьте права файловой системы и закройте приложения, которые могут удерживать этот каталог, затем попробуйте снова.",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "Посмотрите необработанную ошибку ниже, затем попробуйте снова после устранения указанной проблемы.",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "Копировать ошибку",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "Копировать обезличенную",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "Ошибка удаления скопирована",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "Обезличенная ошибка удаления скопирована",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "Не удалось скопировать ошибку удаления",
|
||||
|
||||
"versionPill.appWithVersion": "Приложение {version}",
|
||||
"versionPill.ui": "UI",
|
||||
|
||||
@@ -166,6 +166,30 @@ export const instanceMessages = {
|
||||
"instanceShell.backgroundProcesses.actions.output": "输出",
|
||||
"instanceShell.backgroundProcesses.actions.stop": "停止",
|
||||
"instanceShell.backgroundProcesses.actions.terminate": "终止",
|
||||
"instanceShell.worktree.delete.error.title": "删除失败",
|
||||
"instanceShell.worktree.delete.error.fallback": "删除 worktree 失败",
|
||||
"instanceShell.worktree.delete.error.causeLabel": "可能原因:",
|
||||
"instanceShell.worktree.delete.error.nextStepLabel": "建议的下一步:",
|
||||
"instanceShell.worktree.delete.error.summary.localChanges": "Git 拒绝删除这个 worktree,因为其中包含已修改或未跟踪的文件。",
|
||||
"instanceShell.worktree.delete.error.summary.inUse": "CodeNomad 无法删除这个 worktree,因为目录中的文件仍在被某些进程使用。",
|
||||
"instanceShell.worktree.delete.error.summary.notFound": "CodeNomad 无法删除这个 worktree,因为目录或 worktree 记录未找到。",
|
||||
"instanceShell.worktree.delete.error.summary.permissionDenied": "CodeNomad 无法删除这个 worktree,因为目录访问被拒绝。",
|
||||
"instanceShell.worktree.delete.error.summary.unknown": "CodeNomad 无法删除这个 worktree。",
|
||||
"instanceShell.worktree.delete.error.cause.localChanges": "本地更改",
|
||||
"instanceShell.worktree.delete.error.cause.inUse": "另一个进程正在使用这个 worktree",
|
||||
"instanceShell.worktree.delete.error.cause.notFound": "worktree 目录或记录缺失",
|
||||
"instanceShell.worktree.delete.error.cause.permissionDenied": "文件系统权限不足",
|
||||
"instanceShell.worktree.delete.error.cause.unknown": "后端返回了未分类的删除错误",
|
||||
"instanceShell.worktree.delete.error.nextStep.localChanges": "如果你想丢弃本地更改,请启用强制删除,或者先清理 worktree 后再重试。",
|
||||
"instanceShell.worktree.delete.error.nextStep.inUse": "关闭正在使用这个 worktree 的终端、编辑器、watcher 或后台进程,然后再试一次。",
|
||||
"instanceShell.worktree.delete.error.nextStep.notFound": "刷新 worktree 列表后再试一次。如果仍然失败,请检查磁盘上的 worktree 路径。",
|
||||
"instanceShell.worktree.delete.error.nextStep.permissionDenied": "检查文件系统权限,并关闭可能锁定此目录的应用程序,然后再试一次。",
|
||||
"instanceShell.worktree.delete.error.nextStep.unknown": "查看下方原始错误详情,并在处理提示的问题后再次重试。",
|
||||
"instanceShell.worktree.delete.error.copyRaw": "复制错误",
|
||||
"instanceShell.worktree.delete.error.copySanitized": "复制脱敏内容",
|
||||
"instanceShell.worktree.delete.error.copySuccess": "已复制删除错误",
|
||||
"instanceShell.worktree.delete.error.copySanitizedSuccess": "已复制脱敏后的删除错误",
|
||||
"instanceShell.worktree.delete.error.copyFailure": "复制删除错误失败",
|
||||
|
||||
"versionPill.appWithVersion": "应用 {version}",
|
||||
"versionPill.ui": "UI",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
.prompt-input-wrapper {
|
||||
@apply grid items-stretch;
|
||||
grid-template-columns: minmax(0, 1fr) 64px;
|
||||
grid-template-columns: minmax(0, 1fr) 72px 64px;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -19,6 +19,16 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.prompt-input-primary-actions {
|
||||
@apply flex flex-col items-center;
|
||||
align-self: stretch;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-inline-start: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.prompt-input-field-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -37,7 +47,7 @@
|
||||
.prompt-input {
|
||||
@apply w-full pt-2.5 border text-sm resize-none outline-none transition-colors;
|
||||
padding-inline-start: 0.75rem;
|
||||
padding-inline-end: 7.5rem;
|
||||
padding-inline-end: 0.75rem;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
@@ -85,16 +95,12 @@
|
||||
|
||||
/* Navigation buttons container (expand, prev, next). */
|
||||
.prompt-nav-buttons {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
inset-inline-end: 0.25rem;
|
||||
bottom: 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
gap: 0.125rem;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prompt-nav-column {
|
||||
@@ -287,7 +293,6 @@
|
||||
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-inverted);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.send-button.shell-mode {
|
||||
@@ -421,7 +426,7 @@
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.prompt-input-wrapper {
|
||||
grid-template-columns: minmax(0, 1fr) 40px;
|
||||
grid-template-columns: minmax(0, 1fr) 64px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +434,6 @@
|
||||
.prompt-input {
|
||||
min-height: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding-inline-end: 7.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -611,6 +611,40 @@
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="unified"] .line-numbers {
|
||||
text-align: left !important;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .line-numbers,
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.modified .line-numbers {
|
||||
text-align: left !important;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .glyph-margin {
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .line-numbers {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .cldr.delete-sign {
|
||||
left: var(--split-original-delete-sign-left, 14px) !important;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .margin,
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .margin-view-zones,
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .margin-view-overlays {
|
||||
width: var(--split-original-gutter-width, 24px) !important;
|
||||
}
|
||||
|
||||
.file-viewer-content--monaco .monaco-viewer[data-view-mode="split"] .editor.original .editor-scrollable {
|
||||
left: var(--split-original-gutter-width, 24px) !important;
|
||||
width: calc(100% - var(--split-original-gutter-width, 24px)) !important;
|
||||
}
|
||||
|
||||
.file-viewer-empty {
|
||||
@apply flex flex-col items-center justify-center h-full gap-3 text-center;
|
||||
color: var(--text-muted);
|
||||
|
||||
Reference in New Issue
Block a user