Compare commits

...

4 Commits

Author SHA1 Message Date
VooDisss
e022a158eb improve delete worktree failure diagnostics (#302)
## Summary
- move delete-worktree failures out of transient toast-only UX and keep
them inline in the delete modal
- add parsed diagnostics for common failure modes, including a short
summary, likely cause, and suggested next step
- make the raw error easier to review and share with raw and sanitized
copy actions

Closes #301.

## BEFORE:

<img width="1127" height="860" alt="image"
src="https://github.com/user-attachments/assets/dd09ba1e-be8c-450c-a1dd-f1cde2a48802"
/>

## AFTER: 

<img width="1384" height="835" alt="image"
src="https://github.com/user-attachments/assets/6b0d1459-21fa-4264-9e54-45540f584538"
/>

## Problem
Before this change, delete-worktree failures were difficult to work
with:

1. The failure message was effectively raw backend or git output.
2. Users had to infer the meaning of the error themselves.
3. The UI did not explain what likely went wrong or what to do next.
4. Sharing the error for debugging was awkward when it included
machine-local absolute paths.
5. The confirmation modal was not being used as the primary diagnostic
surface for a destructive action that frequently fails for
understandable reasons.

This was especially frustrating for common cases such as:
- modified or untracked files in the worktree
- a process still using the worktree directory
- permission errors on Windows
- missing worktree directories or stale worktree records

## What changed

### Modal failure UX
- keep delete failures inline inside
`packages/ui/src/components/worktree-selector.tsx`
- clear modal-local error state when opening or closing the dialog
- keep the success toast on successful deletion, but use the modal
itself for failure presentation

### Human-readable diagnostics
- parse JSON-shaped backend error payloads such as `{"error":"..."}`
before classification
- classify common delete failure patterns into:
  - `localChanges`
  - `inUse`
  - `notFound`
  - `permissionDenied`
  - `unknown`
- render three user-facing lines above the raw error:
  - summary
  - likely cause
  - suggested next step

### Copy flows
- add `Copy error` for the original failure text
- add `Copy sanitized` to redact common absolute path and username
patterns before copying

### Modal content and sizing
- present the target worktree in a simpler two-line summary block
- update the delete description text to plain English: `Deletes this
branch worktree and its local folder.`
- size the delete modal deliberately for desktop use while allowing
vertical expansion to the viewport limit before scrolling

### i18n coverage
- add the new delete diagnostic strings across all currently supported
locales touched by this area:
  - `en`
  - `es`
  - `fr`
  - `he`
  - `ja`
  - `ru`
  - `zh-Hans`

## Why this approach
- It keeps the backend contract unchanged and solves the UX problem
where it occurs.
- It preserves access to the raw failure text instead of hiding
implementation detail entirely.
- It gives users immediate guidance without forcing them to translate
git errors into next actions.
- It improves bug reporting without requiring a separate logging or
export workflow.

## Not included
- server-side preflight guards that block delete when the worktree is
still assigned or in use
- process-aware worktree locking detection
- automatic retry or force-delete-and-retry flows

Those are useful follow-ups, but this PR is intentionally scoped to
failure presentation and debuggability.

## Files changed
- `packages/ui/src/components/worktree-selector.tsx`
- `packages/ui/src/lib/i18n/messages/en/instance.ts`
- `packages/ui/src/lib/i18n/messages/es/instance.ts`
- `packages/ui/src/lib/i18n/messages/fr/instance.ts`
- `packages/ui/src/lib/i18n/messages/he/instance.ts`
- `packages/ui/src/lib/i18n/messages/ja/instance.ts`
- `packages/ui/src/lib/i18n/messages/ru/instance.ts`
- `packages/ui/src/lib/i18n/messages/zh-Hans/instance.ts`

## Validation
- `npm run typecheck --workspace @codenomad/ui`
- `npm run build --workspace @codenomad/ui`
- `npm run typecheck --workspace @neuralnomads/codenomad-electron-app`

## Notes for reviewers
- The error classifier is intentionally heuristic and string-based. It
is meant to improve the common cases without increasing backend
coupling.
- The sanitized copy flow is conservative and focused on path and
username redaction, not full structured log scrubbing.

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
2026-04-17 17:12:17 +01:00
VooDisss
9d9a6a79ec Git diff monaco redesign (#304)
## Summary

Fixes #303.

This PR redesigns the Git Changes Monaco diff gutter so unified and
split view both use a more intentional, space-efficient Monaco
presentation while preserving Monaco's performance on large diffs.

The final behavior includes:

- `Compact` and `Normal` gutter modes for Git Changes
- dynamic gutter sizing based on actual line-number digit counts
- independent original/modified number-column sizing where needed
- split-view fixes for both wasted left inset and line-number/sign
overlap
- persisted gutter-mode selection
- localized user-facing labels for the control

## Visual comparison

### Unified view before

<img width="465" height="353" alt="Unified view before"
src="https://github.com/user-attachments/assets/0c061f25-f20a-4127-a85d-aee1161611c7"
/>

### Unified view after

<img width="634" height="240" alt="Unified view after"
src="https://github.com/user-attachments/assets/f2dfd952-89ed-4fdd-83db-a05f19f023b2"
/>

### Split view before

<img width="596" height="335" alt="Split view before"
src="https://github.com/user-attachments/assets/09bfbe41-9438-4801-b181-49a9d19d5bb8"
/>

### Split view after

<img width="640" height="338" alt="Split view after"
src="https://github.com/user-attachments/assets/fc3618ef-474f-4217-bb21-5ffd53eb4e01"
/>

<!-- If you want to replace these screenshots later, keep the four
sections above and swap the image URLs. -->

## What changed

### Unified view

- added two Git Changes Monaco gutter presentations:
  - `Compact`
  - `Normal`
- kept compact as the tighter single-column-feel unified gutter
- kept normal as the wider Monaco-style unified gutter
- made unified gutter sizing respond to actual line-number digit counts
instead of fixed assumptions
- made normal mode size the visible number columns independently when
one side needs more width than the other

### Split view

- added dynamic split gutter sizing derived from actual before/after
line counts
- made split original and modified number columns size independently
- fixed the modified-pane overlap where larger line numbers could
collide with the `+` lane
- fixed the original-pane wasted left inset caused by Monaco reserving
an empty original-side glyph-margin lane

### Persistence and UI

- persisted the selected gutter mode in preferences so it survives
reloads
- moved the gutter-mode control out of the Git Changes toolbar and into
Appearance settings
- renamed the visible settings options to `Compact` and `Normal`

### i18n

- removed hardcoded user-facing gutter toggle strings
- added localized keys for the gutter control labels and titles used by
the Git Changes surface

## Implementation notes

- Monaco remains the active Git Changes renderer throughout
- gutter sizing logic is centralized in
`packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx`
- CSS is used only for narrow presentation adjustments such as the 4px
left inset and the split original-pane glyph-margin correction
- the persisted gutter-mode preference is the source of truth for the
selected presentation

## Review focus

- unified `Compact` mode should feel tight without clipping or overlap
- unified `Normal` mode should remain wider and readable
- 3-digit and 4-digit line numbers should not collide with the sign lane
- split original pane should no longer show wasted left inset before the
first visible number column
- split modified pane should not leave conspicuous dead space or collide
with the `+` lane as digit counts grow
- selected gutter mode should persist after reload

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
2026-04-17 17:04:10 +01:00
Shantur Rathore
82a7c95dba fix(ui): separate prompt composer action columns
Keep the textarea width independent from the prompt controls so wrapping matches the visible layout. Split secondary controls from the primary stop/send rail to preserve the original action column width and add a matching divider.
2026-04-17 16:12:48 +01:00
Shantur Rathore
313a0e579e fix(ui): hold streaming replies once top leaves view 2026-04-17 15:20:48 +01:00
14 changed files with 673 additions and 143 deletions

View File

@@ -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(() => {

View File

@@ -384,6 +384,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
onContextModeChange={props.onContextModeChange}
onWordWrapModeChange={props.onWordWrapModeChange}
/>
</>
}
list={{ panel: renderGroupedList, overlay: renderGroupedList }}

View File

@@ -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"

View File

@@ -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)
}
}

View File

@@ -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">&quot;{target().slug}&quot;</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>

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "ממשק",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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);