Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"test:e2e": "playwright test",
"test:ci": "pnpm typecheck && pnpm test && pnpm test:e2e",
"dist": "electron-vite build && electron-builder --mac",
"dist:local": "electron-vite build && electron-builder --mac --publish never",
"dist:linux": "electron-vite build && electron-builder --linux",
"dist:win": "electron-vite build && electron-builder --win",
"dist:publish": "electron-vite build && electron-builder --mac --publish always",
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer/src/components/layout/DockviewContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PlaceholderPane } from '../panes/PlaceholderPane'
import { WelcomePane } from '../panes/WelcomePane'
import { EditorPane } from '../panes/EditorPane'
import { EditorTab } from '../panes/EditorTab'
import { EditorTabActions } from '../panes/EditorTabActions'
import { AgentTab } from '../panes/AgentTab'
import { TerminalPane } from '../panes/TerminalPane'
import { MarkdownPreviewPane } from '../panes/MarkdownPreviewPane'
Expand Down Expand Up @@ -61,6 +62,7 @@ export function DockviewContainer({ onApiReady }: Props) {
onReady={handleReady}
components={components}
tabComponents={tabComponents}
rightHeaderActionsComponent={EditorTabActions}
/>
)
}
104 changes: 104 additions & 0 deletions packages/renderer/src/components/panes/EditorBreadcrumbBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useMemo } from 'react'

export type LspStatus = 'healthy' | 'starting' | 'down' | 'none'

interface Props {
filePath: string
workspaceRoot?: string | null
// TODO: wire to real LSP status feed once LSP is implemented.
lspStatus?: LspStatus
}

function relativeSegments(filePath: string, root?: string | null): string[] {
if (!filePath) return []
let rel = filePath
const normalizedRoot = root?.replace(/\/+$/, '')
if (
normalizedRoot
&& (filePath === normalizedRoot || filePath.startsWith(`${normalizedRoot}/`))
) {
rel = filePath.slice(normalizedRoot.length)
}
return rel.split('/').filter(Boolean)
Comment thread
Cheezeiii365 marked this conversation as resolved.
}

const LSP_LABELS: Record<LspStatus, string> = {
healthy: 'LSP',
starting: 'LSP starting',
down: 'LSP down',
none: 'No LSP',
}

export function EditorBreadcrumbBar({ filePath, workspaceRoot, lspStatus = 'none' }: Props) {
const segments = useMemo(
() => relativeSegments(filePath, workspaceRoot ?? null),
[filePath, workspaceRoot],
)

return (
<div className="editor-breadcrumb">
<div className="editor-breadcrumb__tools">
{/* Stub: outline / symbol list */}
<button type="button" className="editor-breadcrumb__icon-btn" aria-label="Outline" title="Outline" disabled>
<svg width="14" height="14" viewBox="0 0 14 14">
<circle cx="2" cy="3" r="1" fill="currentColor" />
<circle cx="2" cy="7" r="1" fill="currentColor" />
<circle cx="2" cy="11" r="1" fill="currentColor" />
<path d="M5 3h7M5 7h7M5 11h7" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
</svg>
</button>
{/* Stub: in-file search */}
<button type="button" className="editor-breadcrumb__icon-btn" aria-label="Search in file" title="Search in file" disabled>
<svg width="14" height="14" viewBox="0 0 14 14">
<circle cx="6" cy="6" r="3.5" stroke="currentColor" strokeWidth="1.2" fill="none" />
<path d="M9 9l3 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
</button>
{/* Stub: back / forward navigation */}
<button type="button" className="editor-breadcrumb__icon-btn" aria-label="Back" title="Back" disabled>
<svg width="14" height="14" viewBox="0 0 14 14">
<path d="M9 2L4 7l5 5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
</button>
<button type="button" className="editor-breadcrumb__icon-btn" aria-label="Forward" title="Forward" disabled>
<svg width="14" height="14" viewBox="0 0 14 14">
<path d="M5 2l5 5-5 5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
</button>
</div>

<nav className="editor-breadcrumb__path" aria-label="File path">
{segments.map((seg, i) => {
const isLast = i === segments.length - 1
return (
<span key={`${i}-${seg}`} className="editor-breadcrumb__segment-wrap">
<button
type="button"
className={
'editor-breadcrumb__segment' +
(isLast ? ' editor-breadcrumb__segment--last' : '')
}
disabled
>
{seg}
</button>
{!isLast && <span className="editor-breadcrumb__chevron">β€Ί</span>}
Comment thread
Cheezeiii365 marked this conversation as resolved.
</span>
)
})}
</nav>

<div className="editor-breadcrumb__right">
{lspStatus !== 'none' && (
<span
className={`editor-breadcrumb__lsp editor-breadcrumb__lsp--${lspStatus}`}
title={LSP_LABELS[lspStatus]}
>
<span className="editor-breadcrumb__lsp-dot" />
{LSP_LABELS[lspStatus]}
</span>
)}
</div>
</div>
)
}
7 changes: 5 additions & 2 deletions packages/renderer/src/components/panes/EditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import { useEditorStatus } from '../../hooks/useEditorStatus'
import { useTheme } from '../../hooks/useTheme'
import { showToast } from '../shared/Toast'
import { diffCompartment, toggleInlineDiff } from '../../lib/editor/editorInlineDiff'
import { EditorBreadcrumbBar } from './EditorBreadcrumbBar'
import '../../styles/inline-diff.css'
import '../../styles/editor-breadcrumb.css'

interface EditorPaneParams {
filePath: string
Expand Down Expand Up @@ -433,8 +435,8 @@ export function EditorPane({ params, api }: IDockviewPanelProps<EditorPaneParams
const name = filePath.split('/').pop() ?? filePath
const wid = (params.workspaceId ?? '') as string
const applyTitle = () => {
const s = getDocumentSession(params.workspaceId, filePath)
api.setTitle(s?.isDirty ? 'β€’ ' + name : name)
// Tab dirty state is rendered by EditorTab; no need to prefix the title.
api.setTitle(name)
}
applyTitle()
const unsub = onDocumentSessionChanged((wk, path) => {
Expand Down Expand Up @@ -478,6 +480,7 @@ export function EditorPane({ params, api }: IDockviewPanelProps<EditorPaneParams

return (
<div className="editor-pane">
<EditorBreadcrumbBar filePath={filePath} workspaceRoot={workspaceRoot} />
{loading && <div className="editor-pane__loading">Loading…</div>}
{diffActive && (
<div
Expand Down
48 changes: 42 additions & 6 deletions packages/renderer/src/components/panes/EditorTab.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
import { DockviewDefaultTab, type IDockviewPanelHeaderProps } from 'dockview-react'
import { isDocumentDirty } from '../../lib/editor/documentStore'
import { useEffect, useState } from 'react'
import type { IDockviewPanelHeaderProps } from 'dockview-react'
import { isDocumentDirty, onDocumentSessionChanged } from '../../lib/editor/documentStore'
import { FileTypeIcon } from '../FileTree/FileTypeIcon'

interface EditorTabParams {
filePath: string
workspaceId?: string
}

export function EditorTab(props: IDockviewPanelHeaderProps<EditorTabParams>) {
const handleClose = () => {
const filePath = props.params.filePath
const workspaceId = props.params.workspaceId
const { filePath, workspaceId } = props.params
const name = filePath?.split('/').pop() ?? filePath ?? 'untitled'

const [dirty, setDirty] = useState(() => isDocumentDirty(workspaceId, filePath))

useEffect(() => {
setDirty(isDocumentDirty(workspaceId, filePath))
return onDocumentSessionChanged((wk, path) => {
if (path === filePath && wk === (workspaceId ?? '')) {
setDirty(isDocumentDirty(workspaceId, filePath))
}
})
}, [filePath, workspaceId])

const handleClose = (e: React.MouseEvent) => {
e.stopPropagation()
if (filePath && isDocumentDirty(workspaceId, filePath)) {
if (!window.confirm('Discard unsaved changes?')) return
}
props.api.close()
}

return <DockviewDefaultTab {...props} closeActionOverride={handleClose} />
return (
<div className="editor-tab" title={filePath}>
<span className="editor-tab__icon">
<FileTypeIcon name={name} />
</span>
<span className="editor-tab__name">{name}</span>
<span className={`editor-tab__trailing${dirty ? ' editor-tab__trailing--dirty' : ''}`}>
{dirty && <span className="editor-tab__dirty" aria-label="Unsaved changes" />}
<button
type="button"
className="editor-tab__close"
onMouseDown={(e) => e.stopPropagation()}
onClick={handleClose}
aria-label="Close tab"
>
<svg width="10" height="10" viewBox="0 0 10 10">
<path d="M1 1l8 8M9 1l-8 8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
</button>
</span>
</div>
)
}
53 changes: 53 additions & 0 deletions packages/renderer/src/components/panes/EditorTabActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { IDockviewHeaderActionsProps } from 'dockview-react'
import { executeCommand } from '../../commands/CommandRegistry'

/**
* Right-side actions rendered in every Dockview group header.
* Visual stubs β€” wire to real commands as they land.
*/
export function EditorTabActions(_props: IDockviewHeaderActionsProps) {
return (
<div className="editor-tab-actions">
<button
type="button"
className="editor-tab-actions__btn"
title="New file"
aria-label="New file"
// TODO: wire to new-file command
>
<svg width="14" height="14" viewBox="0 0 14 14">
<path d="M7 2v10M2 7h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</button>
<button
type="button"
className="editor-tab-actions__btn"
title="Plugins"
aria-label="Plugins"
// TODO: wire to plugins panel
>
<svg width="14" height="14" viewBox="0 0 14 14">
<path
d="M5 1.5h2a1 1 0 011 1V4h1.5a1 1 0 011 1v1.5H12a1 1 0 011 1v2a1 1 0 01-1 1h-1.5V12a1 1 0 01-1 1H8v-1.5a1.25 1.25 0 10-2 0V13H4.5a1 1 0 01-1-1v-1.5H2a1 1 0 01-1-1V8.5h1.5a1.25 1.25 0 100-2.5H1V5a1 1 0 011-1h2V2.5a1 1 0 011-1z"
stroke="currentColor"
strokeWidth="1.1"
fill="none"
strokeLinejoin="round"
/>
</svg>
</button>
Comment on lines +11 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Disable the placeholder header actions until they are wired.

packages/renderer/src/components/layout/DockviewContainer.tsx Line 65 mounts this in every Dockview group header, but New file and Plugins are still plain clickable buttons with no handler. That ships two dead controls across the UI.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/panes/EditorTabActions.tsx` around lines 11
- 38, The "New file" and "Plugins" buttons in EditorTabActions.tsx are
interactive but have no handlers; disable them until wired by adding the
disabled attribute and aria-disabled="true" to both button elements (the ones
with title="New file" and title="Plugins" and
className="editor-tab-actions__btn"), optionally append a visually hidden
tooltip like "Not available" to title or change title to indicate disabled
state, and remove or keep the TODO comments; this prevents the dead controls
from being clickable when DockviewContainer mounts EditorTabActions.

<button
type="button"
className="editor-tab-actions__btn"
title="Split editor"
aria-label="Split editor"
onClick={() => executeCommand('editor.splitVertical')}
>
<svg width="14" height="14" viewBox="0 0 14 14">
<rect x="1.5" y="2" width="11" height="10" rx="1" stroke="currentColor" strokeWidth="1.1" fill="none" />
<path d="M7 2v10" stroke="currentColor" strokeWidth="1.1" />
</svg>
</button>
</div>
)
}
26 changes: 20 additions & 6 deletions packages/renderer/src/styles/dockview-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,34 @@
--dv-group-view-background-color: var(--bg-base);
}

/* Tighter tab bar */
/* Tab bar */
.dockview-theme-aide .tabs-container {
height: 28px !important;
height: 32px !important;
padding: 4px 6px 0 6px !important;
gap: 2px !important;
}

.dockview-theme-aide .tab {
font-size: 10px !important;
padding: 0 8px !important;
font-size: 11px !important;
padding: 0 10px !important;
min-width: 0 !important;
height: 28px !important;
border-radius: 8px 8px 0 0 !important;
margin-right: 2px !important;
transition: background-color 120ms ease;
}

.dockview-theme-aide .dv-tab:hover {
background-color: var(--bg-hover) !important;
}

/* Subtler active tab indicator */
/* Active tab merges with editor body via matching background + soft top border */
.dockview-theme-aide .dv-tab.dv-active-tab {
font-weight: 450;
font-weight: 500;
background-color: var(--bg-base) !important;
border-top: 1px solid var(--border-subtle, var(--border-base)) !important;
border-left: 1px solid var(--border-subtle, var(--border-base)) !important;
border-right: 1px solid var(--border-subtle, var(--border-base)) !important;
}

/* ─── Close button overrides ───────────────────── */
Expand Down
Loading
Loading