Restore cron sidebar UX: sync mode to editor and show orphan jobs.

DOM cron mode and React overlay could desync so the right pane stayed on the workspace chat; unassigned legacy jobs were also filtered out for normal users. Re-sync mode from the sidebar attribute and adopt/show reclaimable orphans again.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-08 16:39:21 +08:00
parent a0dc070372
commit feae559fcf
3 changed files with 68 additions and 10 deletions

View file

@ -697,7 +697,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
return ''
}
function JobList({ t, jobs, runs, selection, expanded, viewer, onSelectJob, onSelectRun, onNew, onRun, onToggle, onRemove, onToggleGroup }) {
function JobList({ t, jobs, runs, selection, expanded, viewer, error, onSelectJob, onSelectRun, onNew, onRun, onToggle, onRemove, onToggleGroup }) {
const skin = workspaceSkin()
const [query, setQuery] = useState('')
const [searchOn, setSearchOn] = useState(false)
@ -897,8 +897,9 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
addProjectIcon() || '+'),
),
),
error ? h('p', { className: 'dsh-ct-error' }, error) : null,
h('div', { className: skin.list },
jobs.length === 0 ? h('p', { className: skin.empty }, t('emptyJobs'))
jobs.length === 0 ? h('p', { className: skin.empty }, error || t('emptyJobs'))
: visibleJobs.length === 0 ? h('p', { className: skin.empty }, t('searchEmpty'))
: (canViewAll ? renderGrouped() : visibleJobs.map((job) => renderJobGroup(job))),
),
@ -1237,16 +1238,34 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
}, [])
useEffect(() => {
const readDomMode = () => {
const root = document.querySelector('[data-dsh-ct-mode]')
return root?.getAttribute('data-dsh-ct-mode') === 'on'
}
// Catch events missed before mount / after sidebar remount.
setCronMode(readDomMode())
load()
const id = setInterval(load, cronMode ? 1500 : 4000)
const onMode = (event) => setCronMode(!!event.detail)
const onVis = () => { if (document.visibilityState === 'visible') load() }
window.addEventListener('dsh-ct-mode', onMode)
document.addEventListener('visibilitychange', onVis)
const modeObs = new MutationObserver(() => {
const next = readDomMode()
setCronMode((prev) => (prev === next ? prev : next))
})
if (document.body) {
modeObs.observe(document.body, {
subtree: true,
attributes: true,
attributeFilter: ['data-dsh-ct-mode'],
})
}
return () => {
clearInterval(id)
window.removeEventListener('dsh-ct-mode', onMode)
document.removeEventListener('visibilitychange', onVis)
modeObs.disconnect()
}
}, [load, cronMode])
@ -1421,7 +1440,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
return h(React.Fragment, null,
h(JobList, {
t, jobs, runs, selection, viewer, expanded,
t, jobs, runs, selection, viewer, expanded, error,
onSelectJob: selectJob,
onSelectRun: selectRun,
onNew: selectNew,
@ -1650,7 +1669,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
entry.addEventListener('click', (event) => {
event.preventDefault()
cronOn = !cronOn
setCronMode(findSidebarRoot(found), cronOn, entry, t)
const live = findNewSessionButton() || found
setCronMode(findSidebarRoot(live), cronOn, entry, t)
})
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
icon.setAttribute('viewBox', '0 0 16 16')
@ -1701,6 +1721,12 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
hideNativeCronRows(sidebar)
if (sidebar) {
// Keep DOM mode attribute in sync after remounts so React overlay can follow.
const want = cronOn ? 'on' : 'off'
if (sidebar.getAttribute('data-dsh-ct-mode') !== want) {
sidebar.setAttribute('data-dsh-ct-mode', want)
window.dispatchEvent(new CustomEvent('dsh-ct-mode', { detail: cronOn }))
}
const width = sidebar.getBoundingClientRect().width
const next = `${Math.round(width)}px`
if (document.documentElement.style.getPropertyValue('--dsh-ct-sidebar') !== next) {

View file

@ -13,11 +13,24 @@ export function canViewAllJobs(identity) {
return !!identity?.permissions?.canViewAllSessions
}
function empNoFromUserWorkspacePath(cwd) {
const norm = String(cwd || '').replace(/\\/g, '/')
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
return match ? decodeURIComponent(match[1]) : null
}
export function jobVisibleToIdentity(job, identity) {
if (!identity?.empNo) return false
if (canViewAllJobs(identity)) return true
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
if (owner === UNASSIGNED_OWNER) return false
// Legacy / tool-created rows often lack ownerEmpNo. Keep them visible so the
// sidebar is not empty, then claimUnassignedForViewer can adopt them — unless
// the cwd clearly belongs to another user.
if (owner === UNASSIGNED_OWNER) {
const pathOwner = empNoFromUserWorkspacePath(job?.cwd)
if (pathOwner && pathOwner !== String(identity.empNo)) return false
return true
}
return owner === String(identity.empNo)
}
@ -127,14 +140,23 @@ export function claimUnassignedForViewer(state, identity, deps = {}) {
? job.origin.sessionId.trim()
: ''
let mine = false
// Prefer explicit session ownership / user-workspaces path — do not trust a loose isUserPath.
// Prefer session ownership / user-workspaces cwd. Adopt web/legacy orphans when
// no other owner is known so chat-created jobs reappear in the sidebar.
if (sessionId && typeof deps.getSessionOwner === 'function') {
mine = deps.getSessionOwner(sessionId) === identity.empNo
const sessionOwner = deps.getSessionOwner(sessionId)
if (sessionOwner && sessionOwner !== identity.empNo) return job
mine = !sessionOwner || sessionOwner === identity.empNo
}
if (!mine && job.cwd) {
const norm = String(job.cwd).replace(/\\/g, '/')
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
mine = !!(match && decodeURIComponent(match[1]) === identity.empNo)
if (match) {
if (decodeURIComponent(match[1]) === identity.empNo) mine = true
else return job
}
}
if (!mine && job.origin?.kind !== 'im') {
mine = true
}
if (!mine) return job
changed = true