clone

a hermes agent plugin for openspec: tools for spec workflows and a dashboard tab for browsing changes and specs

main
hermes-openspec / desktop / plugin.js
plugin.js 78.9K
1/**2 * OpenSpec Desktop runtime plugin — source registry + project surface.3 *4 * Registers /openspec route + sidebar nav entry. Provides source selector,5 * work board (ideas, changes by status), change/idea detail dialogs,6 * current specs browser, worktree diff views, and source management7 * (add/edit/remove) via the existing backend CRUD routes.8 *9 * Packaging: single uncompiled ESM file. Runtime loader scans10 * ~/.hermes/plugins/openspec/desktop/plugin.js.11 */1213import {14 Badge, Button, cn, Codicon, CopyButton, Dialog, DialogContent, EmptyState, ErrorState,15 Input, Loader, ROUTES_AREA, SearchField, Select,16 SelectContent, SelectItem, SelectTrigger, SelectValue,17 SegmentedControl, SIDEBAR_NAV_AREA, Tabs, TabsList, TabsTrigger, useQuery18} from '@hermes/plugin-sdk'19import React from 'react'20import { jsx, jsxs } from 'react/jsx-runtime'2122var SOURCE_TRIGGER_HOVER_CSS = '[data-openspec-source-trigger].desktop-input-chrome { background-color: var(--chrome-action-hover) !important; } [data-openspec-source-trigger].desktop-input-chrome:hover { background-color: var(--chrome-action-hover) !important; }'2324// ═══════════════════════════════════════════════════════════════════════════════25// Pure helpers — exported as __test for headless verification26// ═══════════════════════════════════════════════════════════════════════════════2728function sourcesPath() { return '/sources' }2930function changePath(sourceId, name) {31 return '/sources/' + encodeURIComponent(sourceId) + '/changes/' + encodeURIComponent(name)32}3334function ideaPath(sourceId, name) {35 return '/sources/' + encodeURIComponent(sourceId) + '/ideas/' + encodeURIComponent(name)36}3738function specBrowserPath(sourceId, opts) {39 var base = '/sources/' + encodeURIComponent(sourceId) + '/spec-browser'40 if (opts && opts.dirty) return base + '?dirty=true'41 return base42}4344function specPath(sourceId, path) {45 return '/sources/' + encodeURIComponent(sourceId) + '/specs?path=' + encodeURIComponent(path)46}4748function selectFirstValid(sources) {49 if (!sources || sources.length === 0) return null50 var valid = sources.filter(function(s) { return s.valid !== false })51 if (valid.length > 0) return valid[0]52 return sources[0]53}5455function normalizeStatus(status) {56 var s = String(status || '').toLowerCase()57 if (s === 'in-progress' || s === 'inprogress' || s === 'in_progress') return 'in-progress'58 if (s === 'ideas') return 'ideas'59 if (s === 'draft') return 'draft'60 if (s === 'todo') return 'todo'61 if (s === 'done') return 'done'62 if (s === 'archived') return 'archived'63 return 'todo'64}6566var STATUS_ORDER = ['ideas', 'draft', 'todo', 'in-progress', 'done', 'archived']6768function groupByStatus(items, showArchived) {69 var groups = {}70 var order = showArchived ? STATUS_ORDER : STATUS_ORDER.filter(function(s) { return s !== 'archived' })71 for (var i = 0; i < order.length; i++) groups[order[i]] = []72 for (var j = 0; j < (items || []).length; j++) {73 var item = items[j]74 var status = normalizeStatus(item.status || item.state || item.phase)75 if (!showArchived && status === 'archived') continue76 if (!groups[status]) groups[status] = []77 groups[status].push(item)78 }79 return groups80}8182function normalizeSequencePosition(sequence) {83 var position = sequence && typeof sequence === 'object' ? sequence.position : sequence84 return typeof position === 'number' && Number.isFinite(position) ? position : null85}8687function sortItems(items) {88 return (items || []).slice().sort(function(a, b) {89 var sa = normalizeSequencePosition(a.sequence)90 var sb = normalizeSequencePosition(b.sequence)91 if (sa == null) sa = 999992 if (sb == null) sb = 999993 if (sa !== sb) return sa - sb94 var ta = (a.title || a.name || '').toLowerCase()95 var tb = (b.title || b.name || '').toLowerCase()96 if (ta < tb) return -197 if (ta > tb) return 198 return 099 })100}101102function filterItems(items, query) {103 if (!query) return items || []104 var q = String(query).toLowerCase()105 return (items || []).filter(function(item) {106 var title = String(item.title || item.name || '').toLowerCase()107 var token = String(item.token || '').toLowerCase()108 var name = String(item.name || '').toLowerCase()109 return title.includes(q) || token.includes(q) || name.includes(q)110 })111}112113function toggleArchived(current) { return !current }114115function stripFrontmatter(content) {116 if (!content) return content117 return content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '')118}119120// ═══════════════════════════════════════════════════════════════════════════════121// Resolver-safe identity helpers — pure, testable, canonical-aligned122// ═══════════════════════════════════════════════════════════════════════════════123124function buildSourceReference(source, allSources) {125 if (!source) return ''126 var name = String(source.name || '').trim()127 if (name) {128 var count = 0129 for (var i = 0; i < (allSources || []).length; i++) {130 var other = allSources[i]131 if (other && String(other.name || '').trim().toLowerCase() === name.toLowerCase()) count++132 }133 if (count === 1) return name134 }135 var token = String(source.token || '').trim()136 if (token) return token137 var id = String(source.id || '').trim()138 if (id) return id139 return ''140}141142function buildItemReference(source, item, allSources) {143 var sourceRef = buildSourceReference(source, allSources)144 var token = item && item.token ? String(item.token).trim() : ''145 if (!sourceRef || !token) return ''146 return sourceRef + '/' + token147}148149function hasUniqueItemToken(item, allItems) {150 var token = item && item.token ? String(item.token).trim() : ''151 if (!token) return false152 var count = 0153 for (var i = 0; i < (allItems || []).length; i++) {154 var other = allItems[i]155 if (other && String(other.token || '').trim() === token) count++156 }157 return count === 1158}159160function createSessionStore() {161 return { selectedSourceId: null, showArchived: true }162}163164function resolveRetainedSource(sources, retainedId) {165 if (!sources || sources.length === 0) return null166 if (retainedId != null && retainedId !== '') {167 for (var i = 0; i < sources.length; i++) {168 if (sources[i].id === retainedId) return retainedId169 }170 }171 var fallback = selectFirstValid(sources)172 return fallback ? fallback.id : null173}174175function detailTitle(loaded, summary, item, kind) {176 if (loaded && loaded.title) return loaded.title177 if (summary && summary.title) return summary.title178 if (item && item.name) return item.name179 return kind === 'idea' ? 'Untitled idea' : 'Untitled change'180}181182function detailSecondaryName(loaded, summary) {183 if (loaded && loaded.name) return loaded.name184 if (summary && summary.name) return summary.name185 return ''186}187188function ideaSecondaryName(name) {189 if (!name) return ''190 return String(name).replace(/\.md$/i, '') + '.md'191}192193function handleActivateKey(e, action) {194 if (!e) return195 if (e.key === 'Enter' || e.key === ' ') {196 if (typeof e.preventDefault === 'function') e.preventDefault()197 if (action) action()198 }199}200201// ═══════════════════════════════════════════════════════════════════════════════202// Lane override helpers — pure, testable, canonical-aligned203// ═══════════════════════════════════════════════════════════════════════════════204205function computeCollapsedSet(autoCollapsed, manualOverrides, statusOrder) {206 var result = {}207 for (var i = 0; i < statusOrder.length; i++) {208 var s = statusOrder[i]209 if (manualOverrides[s] !== undefined) {210 result[s] = manualOverrides[s]211 } else {212 result[s] = !!autoCollapsed[s]213 }214 }215 return result216}217218function toggleOverride(prev, status, autoCollapsed) {219 var next = Object.assign({}, prev)220 var auto = !!autoCollapsed[status]221 if (next[status] !== undefined) {222 delete next[status]223 } else {224 next[status] = !auto225 }226 return next227}228229function computeLanePhase(grouped, statusOrder) {230 if (!grouped) return null231 var parts = []232 for (var i = 0; i < statusOrder.length; i++) {233 var s = statusOrder[i]234 var items = grouped[s] || []235 parts.push(s + ':' + (items.length === 0 ? 'empty' : 'full'))236 }237 return parts.join('|')238}239240function pruneStaleOverrides(prevOverrides, prevPhase, lanePhase) {241 if (prevPhase === null || lanePhase === null || lanePhase === prevPhase) {242 return prevOverrides243 }244 var before = {}245 var prevParts = prevPhase.split('|')246 for (var i = 0; i < prevParts.length; i++) {247 var kv = prevParts[i].split(':')248 before[kv[0]] = kv[1]249 }250 var next = Object.assign({}, prevOverrides)251 var changed = false252 var curParts = lanePhase.split('|')253 for (var j = 0; j < curParts.length; j++) {254 var ckv = curParts[j].split(':')255 var name = ckv[0]256 var phase = ckv[1]257 var was = before[name]258 if (was !== undefined && was !== phase && next[name] !== undefined) {259 delete next[name]260 changed = true261 }262 }263 return changed ? next : prevOverrides264}265266// ═══════════════════════════════════════════════════════════════════════════════267// Lane layout helpers268// ═══════════════════════════════════════════════════════════════════════════════269270var LANE_EXPANDED_WIDTH = 256271var LANE_COLLAPSED_WIDTH = 32272273// ═══════════════════════════════════════════════════════════════════════════════274// Canonical status tone mapping — aligned with kanban/types.ts COLUMN_META275// ═══════════════════════════════════════════════════════════════════════════════276277var STATUS_TONE = {278 'ideas': 'var(--ui-text-tertiary)',279 'draft': '#a78bfa',280 'todo': 'var(--ui-text-secondary)',281 'in-progress': '#34d399',282 'done': 'var(--ui-text-tertiary)',283 'archived': 'var(--ui-text-quaternary)',284}285286function statusTone(status) {287 return STATUS_TONE[status] || 'var(--ui-text-secondary)'288}289290function autoCollapseEmptyLanes(grouped, statusOrder) {291 var totalItems = 0292 for (var i = 0; i < statusOrder.length; i++) {293 var s = statusOrder[i]294 var items = grouped[s] || []295 totalItems += items.length296 }297 var collapsed = {}298 if (totalItems > 0) {299 for (var j = 0; j < statusOrder.length; j++) {300 var st = statusOrder[j]301 var col = grouped[st] || []302 if (col.length === 0) collapsed[st] = true303 }304 }305 return collapsed306}307308function laneWidth(status, collapsedSet) {309 return (collapsedSet && collapsedSet[status]) ? LANE_COLLAPSED_WIDTH : LANE_EXPANDED_WIDTH310}311312function railStyle() {313 return {314 width: LANE_COLLAPSED_WIDTH + 'px',315 minWidth: LANE_COLLAPSED_WIDTH + 'px',316 flexShrink: 0,317 height: '100%',318 cursor: 'pointer',319 display: 'flex',320 flexDirection: 'column',321 alignItems: 'center',322 gap: '4px',323 borderRadius: '8px',324 padding: '8px 0',325 overflow: 'hidden',326 }327}328329function expandedStyle() {330 return {331 width: LANE_EXPANDED_WIDTH + 'px',332 minWidth: LANE_EXPANDED_WIDTH + 'px',333 flexShrink: 0,334 height: '100%',335 display: 'flex',336 flexDirection: 'column',337 background: 'color-mix(in srgb, var(--ui-bg-quinary) 50%, transparent)',338 borderRadius: '8px',339 padding: '8px',340 overflow: 'hidden',341 }342}343344function parseTasks(content) {345 var total = 0, done = 0346 var sections = []347 var lines = (content || '').split('\n')348 var currentSection = null349 for (var i = 0; i < lines.length; i++) {350 var line = lines[i]351 var headingMatch = line.match(/^#{1,6}\s+(.+)/)352 if (headingMatch) {353 currentSection = { title: headingMatch[1], done: 0, total: 0, tasks: [] }354 sections.push(currentSection)355 }356 var taskMatch = line.match(/^[-*]\s+\[([ xX])\]\s*(.*)/)357 if (taskMatch) {358 var isDone = taskMatch[1] === 'x' || taskMatch[1] === 'X'359 total++360 if (isDone) done++361 var task = { text: taskMatch[2], done: isDone }362 if (!currentSection) {363 currentSection = { title: 'Tasks', done: 0, total: 0, tasks: [] }364 sections.push(currentSection)365 }366 currentSection.total++367 if (isDone) currentSection.done++368 currentSection.tasks.push(task)369 }370 }371 return { total: total, done: done, sections: sections }372}373374function renderMarkdown(content) {375 if (!content) return []376 var elements = []377 var lines = content.split('\n')378 var i = 0379 while (i < lines.length) {380 var line = lines[i]381382 // Fenced code block383 if (line.match(/^```/)) {384 var lang = line.slice(3).trim()385 var codeLines = []386 i++387 while (i < lines.length && !lines[i].match(/^```/)) { codeLines.push(lines[i]); i++ }388 i++ // skip closing fence389 elements.push({ type: 'fenced', lang: lang, content: codeLines.join('\n') })390 continue391 }392393 // Heading394 var headingMatch = line.match(/^(#{1,6})\s+(.*)/)395 if (headingMatch) {396 elements.push({ type: 'heading', level: headingMatch[1].length, text: headingMatch[2] })397 i++; continue398 }399400 // Unordered list401 if (line.match(/^[-*]\s+/)) {402 elements.push({ type: 'list-item', ordered: false, text: line.replace(/^[-*]\s+/, '') })403 i++; continue404 }405406 // Ordered list407 if (line.match(/^\d+\.\s+/)) {408 elements.push({ type: 'list-item', ordered: true, text: line.replace(/^\d+\.\s+/, '') })409 i++; continue410 }411412 // Empty line413 if (line.trim() === '') { i++; continue }414415 // Paragraph416 elements.push({ type: 'paragraph', text: line })417 i++418 }419 return elements420}421422function classifyState(query) {423 if (query.isLoading) return 'loading'424 if (query.error) return 'error'425 if (query.data) {426 if (Array.isArray(query.data) && query.data.length === 0) return 'empty'427 if (query.data.sources && query.data.sources.length === 0) return 'empty'428 if (query.data.files && query.data.files.length === 0) return 'empty'429 if (query.data.items && Array.isArray(query.data.items) && query.data.items.length === 0) return 'empty'430 return 'content'431 }432 return 'empty'433}434435function classifyDiffLine(line) {436 if (!line) return 'context'437 if (line.startsWith('+')) return 'added'438 if (line.startsWith('-')) return 'removed'439 if (line.startsWith('@@')) return 'hunk'440 return 'context'441}442443function hasDiffMode(data, mode) {444 if (!data) return false445 if (mode === 'semantic') return !!(data.semantic_diff && (data.semantic_diff.groups || data.semantic_diff.requirements))446 if (mode === 'split') return !!(data.before != null && data.after != null)447 if (mode === 'raw') return !!(data.diff || data.raw_diff)448 return false449}450451function effectiveDiffMode(desired, data) {452 if (hasDiffMode(data, desired)) return desired453 var fallback = ['semantic', 'split', 'raw']454 for (var i = 0; i < fallback.length; i++) {455 if (hasDiffMode(data, fallback[i])) return fallback[i]456 }457 return desired458}459460function trimPath(raw) {461 return typeof raw === 'string' ? raw.trim() : ''462}463464function extractApiError(err) {465 if (!err) return 'Unknown error'466 var candidate = typeof err === 'string' ? err : (err.message || err.detail)467 if (typeof candidate === 'string' && /\b409\b/.test(candidate) && candidate.indexOf('Source already registered') !== -1) {468 return 'Source already registered. Select it from the source list, or remove it before adding again.'469 }470 if (typeof err === 'string') return err471 if (err.message) return err.message472 if (err.detail) return err.detail473 return String(err)474}475476function sourceNeedsInitialization(result) {477 var source = result && result.source478 return !!(source && source.id && source.valid === false && source.error === 'No openspec/ directory found')479}480481function initializeAddedSource(api, result) {482 if (!sourceNeedsInitialization(result)) return Promise.resolve(result)483 return api.initSource(result.source.id).then(function(initResult) {484 return initResult && initResult.source && initResult.source.id ? initResult : result485 }).catch(function(e) {486 return Object.assign({}, result, { initError: extractApiError(e) })487 })488}489490// ═══════════════════════════════════════════════════════════════════════════════491// API adapter — source registry mutations + read-only, namespace-scoped492// ═══════════════════════════════════════════════════════════════════════════════493494function createApi(ctx) {495 return {496 sources: function() { return ctx.rest(sourcesPath()) },497 change: function(sourceId, name) { return ctx.rest(changePath(sourceId, name)) },498 idea: function(sourceId, name) { return ctx.rest(ideaPath(sourceId, name)) },499 specBrowser: function(sourceId, opts) { return ctx.rest(specBrowserPath(sourceId, opts)) },500 spec: function(sourceId, path) { return ctx.rest(specPath(sourceId, path)) },501 addSource: function(path, name) {502 return ctx.rest(sourcesPath(), { method: 'POST', body: { path: path, name: name || undefined } })503 },504 updateSource: function(sourceId, path, name) {505 return ctx.rest('/sources/' + encodeURIComponent(sourceId), { method: 'PUT', body: { path: path, name: name || undefined } })506 },507 initSource: function(sourceId) {508 return ctx.rest('/sources/' + encodeURIComponent(sourceId) + '/init', { method: 'POST' })509 },510 removeSource: function(sourceId) {511 return ctx.rest('/sources/' + encodeURIComponent(sourceId), { method: 'DELETE' })512 },513 }514}515516// ═══════════════════════════════════════════════════════════════════════════════517// Markdown renderer — safe subset, no innerHTML518// ═══════════════════════════════════════════════════════════════════════════════519520function isSafeUrl(url) {521 if (!url) return false522 // Strip leading whitespace and control characters (U+0000-U+001F, U+007F-U+009F)523 var s = String(url).replace(/^[\s\x00-\x1f\x7f-\x9f]+/, '')524 if (!s) return false525 // Reject protocol-relative URLs526 if (s.startsWith('//')) return false527 // Allow relative paths and fragment-only links528 if (s.startsWith('/') || s.startsWith('#') || s.startsWith('?')) return true529 // Explicit scheme allowlist: http, https, mailto (case-insensitive)530 var lower = s.toLowerCase()531 if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('mailto:')) return true532 // Everything else is rejected (javascript:, data:, file:, vbscript:, etc.)533 return false534}535536function renderInline(text) {537 if (!text) return text538 var parts = []539 var remaining = text540 var regex = /(\*\*(.+?)\*\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\))/g541 var lastIndex = 0542 var match543 while ((match = regex.exec(remaining)) !== null) {544 if (match.index > lastIndex) parts.push(remaining.slice(lastIndex, match.index))545 if (match[2]) parts.push(jsx('strong', { style: { fontWeight: 600 }, children: match[2] }))546 else if (match[3]) parts.push(jsx('code', { style: { background: 'var(--editor-background, #1e1e1e)', padding: '1px 4px', borderRadius: '3px', fontFamily: 'monospace', fontSize: '0.9em' }, children: match[3] }))547 else if (match[4]) {548 if (isSafeUrl(match[5])) {549 parts.push(jsx('a', { href: match[5], target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--link-foreground, #4fc3f7)', textDecoration: 'underline' }, children: match[4] }))550 } else {551 parts.push(jsx('span', { style: { color: 'var(--muted-foreground, #888)' }, children: match[4] + ' [unsafe link]' }))552 }553 }554 lastIndex = match.index + match[0].length555 }556 if (lastIndex < remaining.length) parts.push(remaining.slice(lastIndex))557 return parts558}559560function MarkdownElement(el) {561 if (typeof el === 'string') return el562 if (typeof el === 'number') return el563 if (!el || !el.type) return null564 switch (el.type) {565 case 'heading':566 return jsx('div', { style: { fontWeight: 'bold', fontSize: el.level <= 2 ? '1.1em' : '1em', margin: '8px 0 4px', color: 'var(--foreground, #e0e0e0)' }, children: renderInline(el.text) })567 case 'paragraph':568 return jsx('div', { style: { margin: '4px 0', color: 'var(--foreground, #e0e0e0)' }, children: renderInline(el.text) })569 case 'list-item':570 return jsxs('div', { style: { margin: '1px 0', paddingLeft: '16px', color: 'var(--foreground, #e0e0e0)' }, children: [571 el.ordered ? ' ' : '\u2022 ',572 renderInline(el.text)573 ] })574 case 'fenced':575 return jsx('pre', { style: { background: 'var(--editor-background, #1e1e1e)', padding: '8px', borderRadius: '4px', margin: '4px 0', overflow: 'auto', fontFamily: 'monospace', fontSize: '0.9em', color: 'var(--foreground, #e0e0e0)' }, children: el.content })576 default:577 return null578 }579}580581function MarkdownView({ content }) {582 var elements = renderMarkdown(content)583 if (!elements || elements.length === 0) return jsx(EmptyState, { title: 'No content', description: 'This document is empty.' })584 return jsxs('div', { 'data-selectable-text': 'true', style: { padding: '12px' }, children: elements.map(function(el, i) { return jsx(MarkdownElement, Object.assign({}, el, { key: i })) }) })585}586587// ═══════════════════════════════════════════════════════════════════════════════588// Query state wrapper589// ═══════════════════════════════════════════════════════════════════════════════590591// Reusable retry error state: ErrorState with a Button child wired to onRetry.592// Real SDK ErrorStateProps has no onRetry — canonical pattern nests Button child.593function RetryErrorState({ title, description, onRetry }) {594 return jsx(ErrorState, {595 title: title,596 description: description,597 children: onRetry ? jsx(Button, {598 variant: 'outline',599 size: 'sm',600 onClick: onRetry,601 children: 'Retry'602 }) : null603 })604}605606function QueryState({ query, onRetry, emptyTitle, emptyDescription, children }) {607 var state = classifyState(query)608 if (state === 'loading') return jsx(Loader, { type: 'lemniscate-bloom' })609 if (state === 'error') return jsx(RetryErrorState, {610 title: 'Request failed',611 description: (query.error && query.error.message) || 'Could not reach the backend.',612 onRetry: onRetry613 })614 if (state === 'empty') return jsx(EmptyState, { title: emptyTitle || 'Nothing found', description: emptyDescription || 'No data available.' })615 return children616}617618// ═══════════════════════════════════════════════════════════════════════════════619// Source dialog — add/edit source registry entries620// ═══════════════════════════════════════════════════════════════════════════════621622function SourceDialog({ mode, api, source, onClose, onSaved }) {623 var initialPath = source && source.path ? source.path : ''624 var initialName = source && source.name ? source.name : ''625 var _p = React.useState(initialPath)626 var _n = React.useState(initialName)627 var _busy = React.useState(false)628 var _err = React.useState('')629 var path = _p[0], setPath = _p[1]630 var name = _n[0], setName = _n[1]631 var busy = _busy[0], setBusy = _busy[1]632 var err = _err[0], setErr = _err[1]633634 var title = mode === 'edit' ? 'Edit source' : 'Add source'635636 function handleSave() {637 var trimmed = trimPath(path)638 if (!trimmed) { setErr('Path is required'); return }639 setBusy(true); setErr('')640 var promise = mode === 'edit' && source641 ? api.updateSource(source.id, trimmed, trimPath(name))642 : api.addSource(trimmed, trimPath(name))643 if (mode === 'add') {644 promise = promise.then(function(result) { return initializeAddedSource(api, result) })645 }646 promise.then(function(result) {647 setBusy(false)648 onSaved(result)649 }).catch(function(e) {650 setBusy(false)651 setErr(extractApiError(e))652 })653 }654655 return jsx(Dialog, { open: true, onOpenChange: onClose, children: jsxs(DialogContent, { style: { padding: '16px', maxWidth: '480px' }, children: [656 jsx('h2', { style: { fontSize: '16px', fontWeight: 600, margin: '0 0 12px', color: 'var(--foreground, #e0e0e0)' }, children: title }),657 jsxs('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }, children: [658 jsxs('div', { children: [659 jsx('label', { style: { fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground, #888)', marginBottom: '4px', display: 'block' }, children: 'Path *' }),660 jsx(Input, { value: path, onChange: function(e) { setPath(e.target.value) }, placeholder: '/path/to/repo', disabled: busy }),661 ] }),662 jsxs('div', { children: [663 jsx('label', { style: { fontSize: '12px', fontWeight: 500, color: 'var(--muted-foreground, #888)', marginBottom: '4px', display: 'block' }, children: 'Display name' }),664 jsx(Input, { value: name, onChange: function(e) { setName(e.target.value) }, placeholder: 'Optional display name', disabled: busy }),665 ] }),666 ] }),667 err ? jsx('div', { style: { padding: '8px 12px', background: 'var(--destructive-background, #3d1a1a)', borderRadius: '4px', fontSize: '12px', color: 'var(--destructive, #f44336)', marginBottom: '12px' }, children: err }) : null,668 jsxs('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px' }, children: [669 jsx(Button, { variant: 'outline', size: 'sm', onClick: onClose, disabled: busy, children: 'Cancel' }),670 jsx(Button, { size: 'sm', onClick: handleSave, disabled: busy, children: busy ? 'Saving...' : 'Save' }),671 ] }),672 ] }) })673}674675// ═══════════════════════════════════════════════════════════════════════════════676// Card helpers677// ═══════════════════════════════════════════════════════════════════════════════678679function CardTaskFraction({ item }) {680 var tasks = item.tasks || item.taskStats681 if (!tasks) return null682 var done = tasks.done != null ? tasks.done : (tasks.completed || 0)683 var total = tasks.total != null ? tasks.total : (tasks.count || 0)684 if (total === 0) return null685 return jsx(Badge, { variant: 'outline', children: '✅' + done + '/' + total })686}687688function CardArtifacts({ item }) {689 var badges = []690 if (item.hasProposal) badges.push({ label: 'proposal', color: '#60a5fa' })691 if (item.hasTasks) badges.push({ label: 'tasks', color: '#34d399' })692 if (item.hasDesign) badges.push({ label: 'design', color: '#a78bfa' })693 if (item.hasSpecs) badges.push({ label: 'specs', color: '#fbbf24' })694 if (badges.length === 0) return null695 return jsxs('div', { style: { display: 'flex', gap: '4px', flexWrap: 'wrap' },696 children: badges.map(function(b, i) {697 return jsx(Badge, { variant: 'secondary', style: { fontSize: '10px', color: b.color }, children: b.label }, i)698 })699 })700}701702function BoardCard({ item, source, allSources, allItems, onSelect }) {703 var token = item.token || item.name || ''704 var title = item.title || item.name || 'Untitled'705 var sequencePosition = normalizeSequencePosition(item.sequence)706 var itemRef = buildItemReference(source, item, allSources)707 var canCopy = !!itemRef && hasUniqueItemToken(item, allItems)708 var onClick = function() { onSelect(item) }709 var status = normalizeStatus(item.status || item.state || item.phase)710 return jsxs('div', {711 className: cn(712 'rounded-md border border-(--ui-stroke-tertiary) bg-(--ui-bg-elevated) px-3 pt-3 pb-0 cursor-pointer',713 'hover:border-(--ui-focus-border) hover:bg-primary/[0.06] hover:shadow-md transition-colors',714 'focus-visible:bg-(--ui-control-hover-background)'715 ),716 style: { borderLeftWidth: '2px', borderLeftColor: statusTone(status) },717 'data-selectable-text': 'true',718 tabIndex: 0,719 role: 'button',720 onClick: onClick,721 onKeyDown: function(e) { handleActivateKey(e, onClick) },722 children: [723 jsxs('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '8px' }, children: [724 jsx('div', { style: { fontWeight: 500, fontSize: '13px', color: 'var(--foreground, #e0e0e0)' }, children: title }),725 sequencePosition != null ? jsx(Badge, { variant: 'outline', style: { fontSize: '10px', flexShrink: 0 }, children: '#' + sequencePosition }) : null726 ] }),727 jsxs('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '4px' }, children: [728 jsx(CardArtifacts, { item: item }),729 jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 }, children: [730 jsx(CardTaskFraction, { item: item }),731 jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', marginLeft: 'auto', flexShrink: 0 }, children: [732 canCopy ? jsx(CopyButton, { appearance: 'icon', text: itemRef, label: 'Copy item reference', title: itemRef, stopPropagation: true }) : null,733 token ? jsx('span', { style: { fontFamily: 'monospace', fontSize: '10px', color: 'var(--muted-foreground, #888)' }, children: token }) : null,734 ] }),735 ] }),736 ] }),737 ]738 }, token || title)739}740741// ═══════════════════════════════════════════════════════════════════════════════742// Board columns743// ═══════════════════════════════════════════════════════════════════════════════744745var COLUMN_LABELS = {746 'ideas': 'Ideas',747 'draft': 'Draft',748 'todo': 'Todo',749 'in-progress': 'In Progress',750 'done': 'Done',751 'archived': 'Archived',752}753754function BoardColumn({ status, items, source, allSources, allItems, onSelect, collapsed, onExpand }) {755 var label = COLUMN_LABELS[status] || status756 var sorted = sortItems(items)757 var count = items.length758759 if (collapsed) {760 return jsx('div', {761 style: railStyle(),762 className: cn('focus-visible:bg-(--ui-control-hover-background) bg-[color-mix(in_srgb,var(--ui-bg-quinary)_50%,transparent)] hover:bg-(--ui-bg-quinary)'),763 tabIndex: 0,764 role: 'button',765 onClick: onExpand,766 onKeyDown: function(e) { handleActivateKey(e, onExpand) },767 title: label,768 'aria-label': label,769 children: [770 jsx('span', { style: { display: 'grid', height: '20px', placeItems: 'center', flexShrink: 0 }, children: [771 jsx('span', { style: { width: '6px', height: '6px', borderRadius: '50%', backgroundColor: statusTone(status) } }),772 ] }),773 jsx('span', { style: { fontSize: '11px', fontWeight: 600, color: 'var(--ui-text-tertiary)', letterSpacing: '0.05em', textTransform: 'uppercase', whiteSpace: 'nowrap', writingMode: 'vertical-rl' }, children: label }),774 count > 0 ? jsx('span', { style: { fontSize: '10px', color: 'var(--ui-text-quaternary)', fontVariantNumeric: 'tabular-nums' }, children: count }) : null,775 ]776 })777 }778779 return jsxs('div', { style: expandedStyle(), 'data-lane': status, children: [780 jsxs('button', {781 type: 'button',782 onClick: onExpand,783 title: 'Collapse ' + label,784 'aria-label': 'Collapse ' + label,785 className: cn(786 'flex w-full items-center justify-between px-1 pb-2 bg-transparent border-0 text-inherit cursor-pointer text-left',787 'focus-visible:bg-(--ui-control-hover-background)'788 ),789 style: { fontSize: 'inherit' },790 onKeyDown: function(e) { handleActivateKey(e, onExpand) },791 children: [792 jsxs('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' }, children: [793 jsx('span', { style: { width: '6px', height: '6px', borderRadius: '50%', backgroundColor: statusTone(status), flexShrink: 0 } }),794 jsx('span', { style: { fontWeight: 600, fontSize: '12px', textTransform: 'uppercase', color: 'var(--ui-text-tertiary)', letterSpacing: '0.05em' }, children: label }),795 count > 0 ? jsx('span', { style: { fontSize: '12px', color: 'var(--ui-text-quaternary)', fontVariantNumeric: 'tabular-nums' }, children: count }) : null,796 ] }),797 jsx('span', { style: { display: 'inline-flex', flexShrink: 0 }, children: jsx(Codicon, { name: 'chevron-left', size: '0.85rem' }) }),798 ]799 }),800 jsxs('div', { style: { flex: 1, overflowY: 'auto', overflowX: 'hidden', display: 'flex', flexDirection: 'column', gap: '8px', paddingRight: '4px' }, children: sorted.map(function(item) {801 return jsx(BoardCard, {802 item: item,803 source: source,804 allSources: allSources,805 allItems: allItems,806 onSelect: onSelect,807 }, item.token || item.name || item.title)808 }) }),809 items.length === 0 ? jsx('div', { style: { color: 'var(--muted-foreground, #666)', fontSize: '12px', padding: '12px 4px', fontStyle: 'italic' }, children: 'No items' }) : null,810 ] })811}812813// ═══════════════════════════════════════════════════════════════════════════════814// Detail drawer815// ═══════════════════════════════════════════════════════════════════════════════816817function DetailDrawer({ title, secondary, itemRef, canCopy, token, taskStats, onClose, children }) {818 return jsxs('div', {819 className: cn('absolute inset-y-0 right-0 z-20 flex w-[26rem] flex-col border-l border-(--ui-stroke-tertiary) bg-(--ui-bg-elevated) shadow-lg'),820 style: { minHeight: '0' },821 'data-detail-drawer': 'true',822 children: [823 jsxs('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: '1px solid var(--ui-border, #333)', flexShrink: 0 }, children: [824 jsxs('div', { style: { minWidth: 0 }, children: [825 jsx('h2', { style: { fontSize: '16px', fontWeight: 600, margin: 0, color: 'var(--foreground, #e0e0e0)' }, children: title }),826 secondary ? jsx('div', { style: { fontSize: '12px', color: 'var(--muted-foreground, #888)', fontFamily: 'monospace', marginTop: '2px' }, children: secondary }) : null,827 ] }),828 jsx('button', {829 type: 'button',830 onClick: onClose,831 title: 'Close detail',832 'aria-label': 'Close detail',833 style: { background: 'none', border: 'none', color: 'var(--muted-foreground, #888)', cursor: 'pointer', padding: '4px', flexShrink: 0 },834 children: jsx(Codicon, { name: 'close', size: '1rem' })835 }),836 ] }),837 jsxs('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', padding: '8px 16px', borderBottom: '1px solid var(--ui-border, #333)', flexShrink: 0 }, children: [838 canCopy ? jsx(CopyButton, { appearance: 'icon', text: itemRef, label: 'Copy item reference', title: itemRef }) : null,839 token ? jsx('span', { style: { fontSize: '12px', color: 'var(--muted-foreground, #888)', fontFamily: 'monospace' }, children: token }) : null,840 taskStats ? jsx(Badge, { variant: 'outline', children: taskStats.done + '/' + taskStats.total + ' tasks' }) : null,841 ] }),842 jsx('div', { 'data-selectable-text': 'true', style: { flex: 1, overflowY: 'auto', overflowX: 'hidden', minHeight: '0', padding: '8px 0' }, children: children }),843 ]844 })845}846847848// ═══════════════════════════════════════════════════════════════════════════════849// Change detail drawer850// ═══════════════════════════════════════════════════════════════════════════════851852function ChangeDetailDrawer({ api, sourceId, change, source, allSources, allItems, onClose }) {853 var query = useQuery({854 queryKey: ['openspec', 'change', sourceId, change.name || change.token],855 queryFn: function() { return api.change(sourceId, change.name || change.token) }856 })857858 var [activeTab, setActiveTab] = React.useState(null)859860 var itemRef = buildItemReference(source, change, allSources)861 var canCopy = !!itemRef && hasUniqueItemToken(change, allItems)862 var title = query.data && query.data.title ? query.data.title : (change.title || change.name || 'Untitled change')863 var secondary = query.data && query.data.name ? query.data.name : (change.name || '')864 var token = change.token || change.name || ''865866 if (query.isLoading) return jsx(DetailDrawer, { title: title, secondary: secondary, itemRef: itemRef, canCopy: canCopy, token: token, onClose: onClose, children: jsx(Loader, { type: 'lemniscate-bloom' }) })867 if (query.error) return jsx(DetailDrawer, { title: title, secondary: secondary, itemRef: itemRef, canCopy: canCopy, token: token, onClose: onClose, children: jsx(RetryErrorState, { title: 'Failed to load change', description: query.error.message, onRetry: function() { query.refetch() } }) })868869 var data = query.data || {}870 var tabs = []871 if (data.proposal) tabs.push({ key: 'proposal', label: 'Proposal', content: jsx(MarkdownView, { content: stripFrontmatter(data.proposal) }) })872 if (data.tasks) tabs.push({ key: 'tasks', label: 'Tasks', content: jsx(TaskView, { content: stripFrontmatter(data.tasks) }) })873 if (data.design) tabs.push({ key: 'design', label: 'Design', content: jsx(MarkdownView, { content: stripFrontmatter(data.design) }) })874 if (data.specs && data.specs.length > 0) tabs.push({ key: 'specs', label: 'Specs', content: jsx(SpecsDetailView, { specs: data.specs }) })875876 var currentTab = activeTab || (tabs.length > 0 ? tabs[0].key : null)877878 // Use detailTitle/detailSecondaryName fallback helpers for consistency.879 title = detailTitle(data, change, change, 'change')880 secondary = detailSecondaryName(data, change)881882 return jsx(DetailDrawer, {883 title: title,884 secondary: secondary,885 itemRef: itemRef,886 canCopy: canCopy,887 token: token,888 taskStats: data.taskStats,889 onClose: onClose,890 children: tabs.length > 0 ? jsxs(Tabs, { value: currentTab, onValueChange: setActiveTab, children: [891 jsx(TabsList, { children: tabs.map(function(t) { return jsx(TabsTrigger, { value: t.key, children: t.label }, t.key) }) }),892 tabs.map(function(t) { return currentTab === t.key ? jsx('div', { key: t.key, children: t.content }) : null }),893 ] }) : jsx(EmptyState, { title: 'No artifacts', description: 'This change has no proposal, tasks, design, or spec content.' })894 })895}896897// ═══════════════════════════════════════════════════════════════════════════════898// Task view899// ═══════════════════════════════════════════════════════════════════════════════900901function TaskView({ content }) {902 var parsed = parseTasks(content)903 return jsxs('div', { 'data-selectable-text': 'true', style: { padding: '12px' }, children: [904 jsxs('div', { style: { marginBottom: '12px', color: 'var(--muted-foreground, #888)', fontSize: '13px' }, children: [905 parsed.done + ' of ' + parsed.total + ' tasks completed',906 ] }),907 parsed.sections.map(function(section, i) {908 return jsxs('div', { style: { marginBottom: '12px' }, children: [909 jsx('div', { style: { fontWeight: 600, fontSize: '14px', marginBottom: '4px', color: 'var(--foreground, #e0e0e0)' }, children: section.title }),910 section.tasks.map(function(task, j) {911 return jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0', color: 'var(--foreground, #e0e0e0)' }, children: [912 jsx('span', { style: { color: task.done ? 'var(--primary, #4caf50)' : 'var(--muted-foreground, #888)' }, children: task.done ? '\u2611' : '\u2610' }),913 jsx('span', { style: { fontSize: '13px', textDecoration: task.done ? 'line-through' : 'none', opacity: task.done ? 0.6 : 1 }, children: task.text }),914 ] }, j)915 }),916 ] }, i)917 }),918 parsed.sections.length === 0 ? jsx(MarkdownView, { content: content }) : null,919 ] })920}921922// ═══════════════════════════════════════════════════════════════════════════════923// Idea detail drawer924// ═══════════════════════════════════════════════════════════════════════════════925926function IdeaDetailDrawer({ api, sourceId, idea, source, allSources, allItems, onClose }) {927 var query = useQuery({928 queryKey: ['openspec', 'idea', sourceId, idea.name || idea.token],929 queryFn: function() { return api.idea(sourceId, idea.name || idea.token) }930 })931932 var itemRef = buildItemReference(source, idea, allSources)933 var canCopy = !!itemRef && hasUniqueItemToken(idea, allItems)934 var title = query.data && query.data.title ? query.data.title : (idea.title || idea.name || 'Untitled idea')935 var secondary = ideaSecondaryName(query.data && query.data.name ? query.data.name : (idea.name || ''))936 var token = idea.token || idea.name || ''937938 if (query.isLoading) return jsx(DetailDrawer, { title: title, secondary: secondary, itemRef: itemRef, canCopy: canCopy, token: token, onClose: onClose, children: jsx(Loader, { type: 'lemniscate-bloom' }) })939 if (query.error) return jsx(DetailDrawer, { title: title, secondary: secondary, itemRef: itemRef, canCopy: canCopy, token: token, onClose: onClose, children: jsx(RetryErrorState, { title: 'Failed to load idea', description: query.error.message, onRetry: function() { query.refetch() } }) })940941 var data = query.data || {}942943 // Reconcile title/secondary with canonical fallback helpers.944 title = detailTitle(data, idea, idea, 'idea')945 secondary = ideaSecondaryName(detailSecondaryName(data, idea))946947 // Strip YAML frontmatter from content before rendering948 var content = data.content || ''949 var stripped = stripFrontmatter(content)950951 return jsx(DetailDrawer, { title: title, secondary: secondary, itemRef: itemRef, canCopy: canCopy, token: token, onClose: onClose, children: jsx(MarkdownView, { content: stripped }) })952}953954// ═══════════════════════════════════════════════════════════════════════════════955// Change spec detail — Proposed and Diff views956// ═══════════════════════════════════════════════════════════════════════════════957958function DiffModePanel({ data }) {959 var [diffMode, setDiffMode] = React.useState('semantic')960 var effMode = effectiveDiffMode(diffMode, data)961 return jsxs('div', { children: [962 hasDiffMode(data, 'semantic') || hasDiffMode(data, 'split') || hasDiffMode(data, 'raw') ? jsxs('div', { style: { display: 'flex', gap: '4px', marginBottom: '8px' }, children: [963 hasDiffMode(data, 'semantic') ? jsx(Button, { variant: effMode === 'semantic' ? 'default' : 'outline', size: 'sm', onClick: function() { setDiffMode('semantic') }, children: 'Semantic' }) : null,964 hasDiffMode(data, 'split') ? jsx(Button, { variant: effMode === 'split' ? 'default' : 'outline', size: 'sm', onClick: function() { setDiffMode('split') }, children: 'Side-by-side' }) : null,965 hasDiffMode(data, 'raw') ? jsx(Button, { variant: effMode === 'raw' ? 'default' : 'outline', size: 'sm', onClick: function() { setDiffMode('raw') }, children: 'Raw' }) : null,966 ] }) : null,967 effMode === 'semantic' && hasDiffMode(data, 'semantic') ? jsx(SemanticDiffView, { data: data.semantic_diff }) : null,968 effMode === 'split' && hasDiffMode(data, 'split') ? jsx(SplitDiffView, { before: data.before, after: data.after }) : null,969 effMode === 'raw' && hasDiffMode(data, 'raw') ? jsx(RawDiffView, { diff: data.diff || data.raw_diff }) : null,970 ] })971}972973function SpecsDetailView({ specs }) {974 var [selectedIdx, setSelectedIdx] = React.useState(0)975976 if (!specs || specs.length === 0) return jsx(EmptyState, { title: 'No specs', description: 'No spec changes in this change.' })977 var spec = specs[selectedIdx] || specs[0]978979 return jsxs('div', { 'data-selectable-text': 'true', style: { padding: '12px' }, children: [980 specs.length > 1 ? jsxs('div', { style: { marginBottom: '8px' }, children: [981 jsx('select', {982 value: selectedIdx,983 onChange: function(e) { setSelectedIdx(parseInt(e.target.value, 10)) },984 style: { padding: '4px 8px', borderRadius: '4px', border: '1px solid var(--ui-border, #333)', background: 'var(--input-background, #2a2a2a)', color: 'var(--foreground, #e0e0e0)' },985 children: specs.map(function(s, i) { return jsx('option', { value: i, children: s.path || s.title || ('Spec ' + (i + 1)) }, i) })986 })987 ] }) : null,988 spec.path ? jsx('div', { style: { fontSize: '12px', color: 'var(--muted-foreground, #888)', marginBottom: '8px', fontFamily: 'monospace' }, children: spec.path }) : null,989 spec.status ? jsx(Badge, { variant: 'outline', style: { marginBottom: '8px' }, children: spec.status }) : null,990 jsx(DiffModePanel, { data: spec }),991 spec.content ? jsx(MarkdownView, { content: stripFrontmatter(spec.content) }) : null,992 ] })993}994995// ═══════════════════════════════════════════════════════════════════════════════996// Diff views997// ═══════════════════════════════════════════════════════════════════════════════998999function normalizeSemanticGroups(data) {1000 if (!data) return []1001 // Legacy flat groups array (data.groups)1002 if (data.groups && Array.isArray(data.groups)) return data.groups1003 // Backend contract: {status, requirements: {added, modified, removed, unchanged}}1004 var reqs = data.requirements1005 if (!reqs || typeof reqs !== 'object' || Array.isArray(reqs)) return []1006 var items = []1007 var added = reqs.added || []1008 var modified = reqs.modified || []1009 var removed = reqs.removed || []1010 for (var i = 0; i < added.length; i++) {1011 var a = added[i]1012 items.push({ label: 'Added', name: a.name || '', description: a.description || '', color: 'var(--primary, #4caf50)' })1013 }1014 for (var j = 0; j < modified.length; j++) {1015 var m = modified[j]1016 var beforeDesc = m.before && typeof m.before === 'object' ? (m.before.description || '') : String(m.before || '')1017 var afterDesc = m.after && typeof m.after === 'object' ? (m.after.description || '') : String(m.after || '')1018 items.push({ label: 'Modified', name: m.name || '', description: '', before: beforeDesc, after: afterDesc, color: 'var(--warning, #ff9800)' })1019 }1020 for (var k = 0; k < removed.length; k++) {1021 var r = removed[k]1022 items.push({ label: 'Removed', name: r.name || '', description: r.description || '', color: 'var(--destructive, #f44336)' })1023 }1024 return items1025}10261027function SemanticDiffView({ data }) {1028 if (!data) return null1029 var items = normalizeSemanticGroups(data)1030 return jsxs('div', { style: { padding: '8px' }, children: [1031 jsx('div', { style: { fontWeight: 600, marginBottom: '8px', color: 'var(--foreground, #e0e0e0)' }, children: 'Semantic Changes' }),1032 items.map(function(item, i) {1033 return jsxs('div', { style: { marginBottom: '8px', padding: '8px', background: 'var(--card-background, #1a1a1a)', borderRadius: '4px', border: '1px solid var(--ui-border, #333)' }, children: [1034 jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '4px' }, children: [1035 jsx('span', { style: { fontSize: '10px', fontWeight: 600, padding: '1px 5px', borderRadius: '3px', background: item.color, color: '#fff' }, children: item.label }),1036 item.name ? jsx('span', { style: { fontWeight: 500, color: 'var(--foreground, #e0e0e0)' }, children: item.name }) : null,1037 ] }),1038 item.description ? jsx('div', { style: { fontSize: '12px', color: 'var(--muted-foreground, #888)', marginBottom: '2px' }, children: item.description }) : null,1039 item.before != null && item.before !== '' ? jsx('div', { style: { color: 'var(--destructive, #f44336)', fontSize: '12px', marginBottom: '2px' }, children: '- ' + item.before }) : null,1040 item.after != null && item.after !== '' ? jsx('div', { style: { color: 'var(--primary, #4caf50)', fontSize: '12px' }, children: '+ ' + item.after }) : null,1041 ] }, i)1042 }),1043 ] })1044}10451046function SplitDiffView({ before, after }) {1047 return jsxs('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', padding: '8px' }, children: [1048 jsxs('div', { children: [1049 jsx('div', { style: { fontWeight: 500, marginBottom: '4px', color: 'var(--muted-foreground, #888)', fontSize: '12px' }, children: 'Before' }),1050 jsx('pre', { style: { fontSize: '12px', fontFamily: 'monospace', whiteSpace: 'pre-wrap', padding: '8px', background: 'var(--editor-background, #1e1e1e)', borderRadius: '4px', color: 'var(--foreground, #e0e0e0)', margin: 0 }, children: before || '' }),1051 ] }),1052 jsxs('div', { children: [1053 jsx('div', { style: { fontWeight: 500, marginBottom: '4px', color: 'var(--muted-foreground, #888)', fontSize: '12px' }, children: 'After' }),1054 jsx('pre', { style: { fontSize: '12px', fontFamily: 'monospace', whiteSpace: 'pre-wrap', padding: '8px', background: 'var(--editor-background, #1e1e1e)', borderRadius: '4px', color: 'var(--foreground, #e0e0e0)', margin: 0 }, children: after || '' }),1055 ] }),1056 ] })1057}10581059function RawDiffView({ diff }) {1060 if (!diff) return null1061 var lines = Array.isArray(diff) ? diff : String(diff).split('\n')1062 return jsxs('pre', { style: { fontSize: '12px', fontFamily: 'monospace', whiteSpace: 'pre-wrap', padding: '8px', background: 'var(--editor-background, #1e1e1e)', borderRadius: '4px', margin: 0 }, children: lines.map(function(line, i) {1063 var cls = classifyDiffLine(line)1064 var style = { display: 'block', padding: '1px 4px' }1065 if (cls === 'added') style.color = 'var(--primary, #4caf50)'1066 else if (cls === 'removed') style.color = 'var(--destructive, #f44336)'1067 else if (cls === 'hunk') style.color = 'var(--muted-foreground, #888)'1068 else style.color = 'var(--foreground, #e0e0e0)'1069 return jsx('span', { style: style, children: line + '\n' }, i)1070 }) })1071}10721073// ═══════════════════════════════════════════════════════════════════════════════1074// Work view1075// ═══════════════════════════════════════════════════════════════════════════════10761077function WorkView({ api, source, sources, onSelectItem, showArchived, onToggleArchived }) {1078 var ideas = (source.openspec && source.openspec.ideas) || []1079 var changes = (source.openspec && source.openspec.changes) || []1080 var allItems = ideas.map(function(idea) {1081 return Object.assign({}, idea, { _type: 'idea' })1082 }).concat(changes.map(function(change) {1083 return Object.assign({}, change, { _type: 'change' })1084 }))10851086 var [filter, setFilter] = React.useState('')1087 var [manualOverrides, setManualOverrides] = React.useState({})1088 var prevLanePhase = React.useRef(null)10891090 var filtered = filterItems(allItems, filter)1091 var visibleStatuses = showArchived ? STATUS_ORDER : STATUS_ORDER.filter(function(s) { return s !== 'archived' })1092 var grouped = groupByStatus(filtered, showArchived)1093 var autoCollapsed = autoCollapseEmptyLanes(grouped, visibleStatuses)10941095 // Lane phase tracking — prune overrides when empty/full flips1096 var lanePhase = computeLanePhase(grouped, visibleStatuses)1097 React.useEffect(function() {1098 if (lanePhase === null || lanePhase === prevLanePhase.current) return1099 var prev = prevLanePhase.current1100 prevLanePhase.current = lanePhase1101 if (prev === null) return1102 setManualOverrides(function(prevOverrides) {1103 return pruneStaleOverrides(prevOverrides, prev, lanePhase)1104 })1105 }, [lanePhase])11061107 var collapsedSet = computeCollapsedSet(autoCollapsed, manualOverrides, visibleStatuses)11081109 function toggleCollapse(status) {1110 setManualOverrides(function(prev) {1111 return toggleOverride(prev, status, autoCollapsed)1112 })1113 }11141115 return jsxs('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', height: '100%', overflow: 'hidden', minWidth: 0, minHeight: 0 }, children: [1116 jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '0 4px', flexShrink: 0 }, children: [1117 jsx('span', { style: { display: 'inline-flex', alignItems: 'center', color: 'var(--muted-foreground, #888)', lineHeight: 1 }, title: 'Filter', 'aria-label': 'Filter', children: jsx(Codicon, { name: 'filter', size: '0.85rem' }) }),1118 jsx(Input, { placeholder: 'title, token, or name...', value: filter, onChange: function(e) { setFilter(e.target.value) }, style: { flex: 1 } }),1119 jsx(Button, { variant: 'outline', size: 'sm', type: 'button', 'aria-pressed': showArchived, style: { backgroundColor: showArchived ? 'var(--ui-control-active-background)' : undefined }, onClick: function() { onToggleArchived(!showArchived) }, children: [showArchived ? '\u2713' : null, ' ', 'Archived'] }),1120 ] }),1121 jsx('div', { style: { display: 'flex', gap: '8px', flex: 1, overflowX: 'auto', overflowY: 'hidden', minWidth: 0, minHeight: 0, paddingBottom: '8px' }, children: visibleStatuses.map(function(status) {1122 var items = grouped[status] || []1123 return jsx(BoardColumn, {1124 status: status,1125 items: items,1126 source: source,1127 allSources: sources,1128 allItems: allItems,1129 onSelect: onSelectItem,1130 collapsed: !!collapsedSet[status],1131 onExpand: function() { toggleCollapse(status) },1132 }, status)1133 }) }),1134 ] })1135}11361137// ═══════════════════════════════════════════════════════════════════════════════1138// Specs view1139// ═══════════════════════════════════════════════════════════════════════════════11401141function SpecsView({ api, sourceId }) {1142 var [worktree, setWorktree] = React.useState(false)11431144 var query = useQuery({1145 queryKey: ['openspec', 'spec-browser', sourceId, worktree],1146 queryFn: function() { return api.specBrowser(sourceId, { dirty: worktree }) },1147 })11481149 var [selectedFile, setSelectedFile] = React.useState(null)11501151 // Reset selected file when source changes — component remounts via key prop1152 React.useEffect(function() {1153 setSelectedFile(null)1154 }, [sourceId])11551156 // Gate spec query on file selection1157 var specQuery = useQuery({1158 queryKey: ['openspec', 'spec', sourceId, selectedFile],1159 queryFn: function() { return api.spec(sourceId, selectedFile) },1160 enabled: !!selectedFile && !worktree,1161 })11621163 var files = (query.data && query.data.files) || []1164 var branch = query.data && query.data.branch1165 var changedCount = query.data && query.data.changedCount != null ? query.data.changedCount : files.length11661167 return jsxs('div', { style: { display: 'flex', flexDirection: 'column', gap: '12px', height: '100%' }, children: [1168 jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '0 4px' }, children: [1169 jsx(Button, { variant: worktree ? 'outline' : 'default', size: 'sm', onClick: function() { setWorktree(false); setSelectedFile(null) }, children: 'Current' }),1170 jsx(Button, { variant: worktree ? 'default' : 'outline', size: 'sm', onClick: function() { setWorktree(true); setSelectedFile(null) }, children: 'Worktree' }),1171 branch ? jsx('span', { style: { fontSize: '12px', color: 'var(--muted-foreground, #888)' }, children: branch }) : null,1172 worktree ? jsx(Badge, { variant: 'outline', children: changedCount + ' changed' }) : null,1173 ] }),1174 query.isLoading ? jsx(Loader, { type: 'lemniscate-bloom' }) : null,1175 query.error ? jsx(RetryErrorState, { title: 'Failed to load specs', description: query.error.message, onRetry: function() { query.refetch() } }) : null,1176 !query.isLoading && !query.error && files.length === 0 ? jsx(EmptyState, {1177 title: worktree ? 'No spec changes between HEAD and worktree' : 'No specs found',1178 description: worktree ? 'The working tree matches HEAD for all spec files.' : 'No spec files are registered for this source.',1179 }) : null,1180 !query.isLoading && files.length > 0 ? jsxs('div', { style: { display: 'flex', gap: '12px', flex: 1, overflow: 'hidden' }, children: [1181 jsxs('div', { style: { width: '240px', flexShrink: 0, overflow: 'auto' }, children: [1182 files.map(function(file) {1183 var path = file.path || file.name || file1184 var isSelected = selectedFile === path1185 return jsxs('div', {1186 className: cn(1187 'px-3 py-2 cursor-pointer text-sm rounded-md',1188 isSelected ? 'bg-(--ui-selected-background, #2a2a2a) text-foreground' : 'text-muted-foreground hover:bg-(--ui-hover-background, #1a1a1a)'1189 ),1190 onClick: function() { setSelectedFile(path) },1191 children: [1192 jsx('div', { style: { fontFamily: 'monospace', fontSize: '12px' }, children: path }),1193 file.status ? jsx(Badge, { variant: 'outline', style: { fontSize: '10px', marginTop: '2px' }, children: file.status }) : null,1194 ]1195 }, path)1196 }),1197 ] }),1198 jsxs('div', { style: { flex: 1, overflow: 'auto' }, children: [1199 worktree && selectedFile ? jsx(WorktreeDetailView, { sourceId: sourceId, file: selectedFile, files: files }) : null,1200 !worktree && selectedFile && specQuery.isLoading ? jsx(Loader, { type: 'lemniscate-bloom' }) : null,1201 !worktree && selectedFile && specQuery.error ? jsx(RetryErrorState, { title: 'Failed to load spec', description: specQuery.error.message, onRetry: function() { specQuery.refetch() } }) : null,1202 !worktree && selectedFile && !specQuery.isLoading && !specQuery.error ? jsx(MarkdownView, { content: stripFrontmatter(specQuery.data && specQuery.data.content) }) : null,1203 !selectedFile ? jsx(EmptyState, { title: 'Select a file', description: 'Choose a spec file from the list to view its content.' }) : null,1204 ] }),1205 ] }) : null,1206 ] })1207}12081209function WorktreeDetailView({ sourceId, file, files }) {1210 var fileInfo = files.find(function(f) { return (f.path || f.name || f) === file }) || {}12111212 var data = fileInfo12131214 return jsxs('div', { style: { padding: '12px' }, children: [1215 jsxs('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }, children: [1216 jsx('h3', { style: { fontSize: '14px', fontWeight: 500, margin: 0, color: 'var(--foreground, #e0e0e0)' }, children: file }),1217 fileInfo.status ? jsx(Badge, { variant: 'outline', children: fileInfo.status }) : null,1218 ] }),1219 jsx(DiffModePanel, { data: data }),1220 !hasDiffMode(data, 'semantic') && !hasDiffMode(data, 'split') && !hasDiffMode(data, 'raw') && data.content ? jsx(MarkdownView, { content: stripFrontmatter(data.content) }) : null,1221 !hasDiffMode(data, 'semantic') && !hasDiffMode(data, 'split') && !hasDiffMode(data, 'raw') && !data.content ? jsx(EmptyState, { title: 'No diff data', description: 'No diff representation available for this file.' }) : null,1222 ] })1223}12241225// ═══════════════════════════════════════════════════════════════════════════════1226// Main page1227// ═══════════════════════════════════════════════════════════════════════════════12281229function OpenSpecPage({ api, sessionStore }) {1230 var sourcesQuery = useQuery({1231 queryKey: ['openspec', 'sources'],1232 queryFn: function() { return api.sources() },1233 })12341235 var [selectedSourceId, setSelectedSourceId] = React.useState(sessionStore ? sessionStore.selectedSourceId : null)1236 var [view, setView] = React.useState('work')1237 var [showArchived, setShowArchived] = React.useState(sessionStore ? sessionStore.showArchived : true)1238 var [selectedItem, setSelectedItem] = React.useState(null) // {item, sourceId, type}1239 var [sourceDialog, setSourceDialog] = React.useState(null) // {mode: 'add'|'edit'} or null1240 var [removeConfirm, setRemoveConfirm] = React.useState(null) // source object or null1241 var [removeBusy, setRemoveBusy] = React.useState(false)1242 var [removeError, setRemoveError] = React.useState('')1243 var [initBusy, setInitBusy] = React.useState(false)1244 var [initError, setInitError] = React.useState('')12451246 // Resolve selection after refreshed /sources data: restore the retained source1247 // while its ID remains present (including invalid sources), otherwise fall back1248 // to first-valid / first-returned; update the registration-scope store both ways.1249 // The effect is gated on data arrival: during the loading window1250 // sourcesQuery.data is still undefined, and resolving against the pre-data1251 // empty list would wipe a retained selection (both React state and the1252 // registration-scope store) before refreshed /sources data ever arrives. The1253 // retained ID survives the loading render, so resolution — and the null rule1254 // for genuinely empty results — only ever runs against the refreshed list.1255 var sources = (sourcesQuery.data && sourcesQuery.data.sources) || []1256 var previousResolvedInvalid = React.useRef(false)1257 React.useEffect(function() {1258 if (sourcesQuery.data === undefined) return1259 var next = resolveRetainedSource(sources, selectedSourceId)1260 var resolved = sources.find(function(s) { return s.id === next }) || null1261 var resolvedInvalid = resolved && resolved.valid === false1262 if (next === selectedSourceId) {1263 if (resolvedInvalid && !previousResolvedInvalid.current) {1264 setSelectedItem(null)1265 }1266 previousResolvedInvalid.current = !!resolvedInvalid1267 if (sessionStore && next !== null) sessionStore.selectedSourceId = next1268 return1269 }1270 setSelectedSourceId(next)1271 setSelectedItem(null)1272 previousResolvedInvalid.current = !!resolvedInvalid1273 if (sessionStore) sessionStore.selectedSourceId = next1274 }, [sources, sourcesQuery.data, selectedSourceId])12751276 // Close detail on explicit source change by user1277 React.useEffect(function() {1278 setSelectedItem(null)1279 }, [selectedSourceId])12801281 var selectedSource = sources.find(function(s) { return s.id === selectedSourceId }) || null1282 var sourceRef = buildSourceReference(selectedSource, sources)1283 var allItems = []1284 if (selectedSource && selectedSource.openspec) {1285 var loadedIdeas = selectedSource.openspec.ideas || []1286 var loadedChanges = selectedSource.openspec.changes || []1287 allItems = loadedIdeas.map(function(idea) {1288 return Object.assign({}, idea, { _type: 'idea' })1289 }).concat(loadedChanges.map(function(change) {1290 return Object.assign({}, change, { _type: 'change' })1291 }))1292 }12931294 function handleSelectSource(v) {1295 setSelectedSourceId(v)1296 if (sessionStore) sessionStore.selectedSourceId = v1297 }1298 function handleToggleArchived(v) {1299 setShowArchived(v)1300 if (sessionStore) sessionStore.showArchived = v1301 }13021303 if (sourcesQuery.isLoading) return jsx(Loader, { type: 'lemniscate-bloom' })1304 if (sourcesQuery.error) return jsx(RetryErrorState, { title: 'Failed to load sources', description: sourcesQuery.error.message, onRetry: function() { sourcesQuery.refetch() } })13051306 var onSelectItem = function(item) {1307 setSelectedItem({ item: item, sourceId: selectedSourceId, type: item._type || 'change' })1308 }13091310 function handleAddSource() { setSourceDialog({ mode: 'add' }) }1311 function handleEditSource() { if (selectedSource) setSourceDialog({ mode: 'edit' }) }1312 function handleInitSource() {1313 if (!selectedSource || initBusy) return1314 setInitBusy(true); setInitError('')1315 api.initSource(selectedSource.id).then(function() {1316 setInitBusy(false)1317 sourcesQuery.refetch()1318 }).catch(function(e) {1319 setInitBusy(false)1320 setInitError(extractApiError(e))1321 })1322 }1323 function handleRemoveSource() {1324 if (!selectedSource) return1325 setRemoveConfirm(selectedSource)1326 setRemoveError('')1327 }1328 function confirmRemove() {1329 if (!removeConfirm) return1330 setRemoveBusy(true); setRemoveError('')1331 api.removeSource(removeConfirm.id).then(function() {1332 setRemoveBusy(false); setRemoveConfirm(null)1333 setSelectedSourceId(null)1334 sourcesQuery.refetch()1335 }).catch(function(e) {1336 setRemoveBusy(false)1337 setRemoveError(extractApiError(e))1338 })1339 }1340 function onSourceSaved(result) {1341 setSourceDialog(null)1342 setInitError(result && result.initError ? result.initError : '')1343 var refresh = sourcesQuery.refetch()1344 if (result && result.source && result.source.id) {1345 var selectReturnedSource = function() {1346 setSelectedSourceId(result.source.id)1347 if (sessionStore) sessionStore.selectedSourceId = result.source.id1348 }1349 if (refresh && typeof refresh.then === 'function') {1350 refresh.then(selectReturnedSource, selectReturnedSource)1351 } else {1352 selectReturnedSource()1353 }1354 }1355 }13561357 return jsxs('div', { className: cn('relative flex flex-col h-full overflow-hidden'), children: [1358 jsx('style', { children: SOURCE_TRIGGER_HOVER_CSS }),13591360 // Header1361 jsxs('div', { style: { padding: '6px 16px', borderBottom: '1px solid var(--ui-border, #333)', display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' }, children: [1362 jsx('h1', { style: { fontSize: '13px', fontWeight: 600, margin: 0, color: 'var(--foreground, #e0e0e0)' }, children: 'OpenSpec' }),1363 selectedSource && selectedSource.path ? jsx('span', { style: { minWidth: 0, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'monospace', fontSize: '11px', color: 'var(--muted-foreground, #888)' }, title: selectedSource.path, children: selectedSource.path }) : null,1364 ] }),13651366 // Source controls and path line1367 jsxs('div', { style: { padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '11px', color: 'var(--muted-foreground, #888)', borderBottom: '1px solid var(--ui-border, #333)' }, children: [1368 selectedSource ? jsx(CopyButton, { appearance: 'icon', text: sourceRef, label: 'Copy source reference', title: sourceRef, disabled: !sourceRef }) : null,1369 jsx(Select, {1370 value: selectedSourceId || undefined,1371 onValueChange: handleSelectSource,1372 children: [1373 jsx(SelectTrigger, { className: 'bg-(--ui-bg-quinary) transition-colors hover:bg-primary/[0.06] hover:bg-primary/[0.06]!', 'data-openspec-source-trigger': 'true', style: { minWidth: '120px', flexShrink: 1 }, children: jsx(SelectValue, { placeholder: 'Select source' }) }),1374 jsx(SelectContent, { children: sources.map(function(s) {1375 return jsx(SelectItem, { value: s.id, children: (s.name || s.id) + (s.valid === false ? ' (invalid)' : '') }, s.id)1376 }) })1377 ]1378 }),1379 jsx(Button, { variant: 'outline', size: 'sm', style: { padding: '4px', minWidth: 'auto', flexShrink: 0 }, title: 'Refresh sources', 'aria-label': 'Refresh sources', onClick: function() { sourcesQuery.refetch() }, children: jsx(Codicon, { name: 'refresh', size: '0.85rem' }) }),1380 jsx(Button, { variant: 'ghost', size: 'sm', style: { padding: '2px 6px', fontSize: '11px', minWidth: 'auto', flexShrink: 0, whiteSpace: 'nowrap' }, title: 'Add source', 'aria-label': 'Add source', onClick: handleAddSource, children: ['+', ' Add'] }),1381 selectedSource ? jsx(Button, { variant: 'ghost', size: 'sm', style: { padding: '2px 6px', fontSize: '11px', minWidth: 'auto', flexShrink: 0, whiteSpace: 'nowrap' }, title: 'Edit source', 'aria-label': 'Edit source', onClick: handleEditSource, children: ['\u270E', ' Edit'] }) : null,1382 selectedSource ? jsx(Button, { variant: 'ghost', size: 'sm', style: { padding: '2px 6px', fontSize: '11px', minWidth: 'auto', flexShrink: 0, whiteSpace: 'nowrap' }, title: 'Delete source', 'aria-label': 'Delete source', onClick: handleRemoveSource, children: ['\u2715', ' Delete'] }) : null,1383 ] }),13841385 // View switch1386 jsxs('div', { style: { padding: '8px 16px', borderBottom: '1px solid var(--ui-border, #333)', display: 'flex', alignItems: 'center', gap: '12px' }, children: [1387 jsx(SegmentedControl, {1388 value: view,1389 onChange: setView,1390 options: [1391 { id: 'work', label: 'Work' },1392 { id: 'specs', label: 'Specs' },1393 ]1394 }),1395 selectedSource && selectedSource.openspec && selectedSource.openspec.counts ? jsxs('div', { style: { display: 'flex', gap: '4px', fontSize: '10px', color: 'var(--muted-foreground, #888)' }, children: [1396 selectedSource.openspec.counts.changes != null ? jsx(Badge, { variant: 'outline', children: selectedSource.openspec.counts.changes + ' changes' }) : null,1397 selectedSource.openspec.counts.ideas != null ? jsx(Badge, { variant: 'outline', children: selectedSource.openspec.counts.ideas + ' ideas' }) : null,1398 selectedSource.openspec.counts.specs != null ? jsx(Badge, { variant: 'outline', children: selectedSource.openspec.counts.specs + ' specs' }) : null,1399 ] }) : null,1400 ] }),14011402 // Invalid source warning1403 selectedSource && selectedSource.valid === false ? jsx('div', { style: { padding: '12px 16px', background: 'var(--warning-background, #fff3cd)', borderRadius: '4px', margin: '8px 16px', fontSize: '13px' }, children: jsxs('div', { children: [1404 jsxs('div', { children: [1405 jsx('strong', { children: 'Invalid source' }),1406 selectedSource.error ? ': ' + selectedSource.error : '',1407 selectedSource.path ? jsxs('div', { style: { fontFamily: 'monospace', fontSize: '11px', marginTop: '4px', opacity: 0.7 }, children: ['Path: ', selectedSource.path] }) : null,1408 ] }),1409 selectedSource.error === 'No openspec/ directory found' ? jsx(Button, { size: 'sm', onClick: handleInitSource, disabled: initBusy, style: { marginTop: '10px' }, children: initBusy ? 'Initializing...' : 'Initialize OpenSpec' }) : null,1410 initError ? jsx('div', { style: { marginTop: '8px', color: 'var(--destructive, #f44336)', fontSize: '12px' }, children: initError }) : null,1411 ] }) }) : null,14121413 // View body1414 jsx('div', { style: { flex: 1, overflow: 'hidden', padding: '12px 16px' }, children: selectedSource && selectedSource.valid !== false ? (1415 view === 'work'1416 ? jsx(WorkView, { key: selectedSource.id, api: api, source: selectedSource, sources: sources, showArchived: showArchived, onToggleArchived: handleToggleArchived, onSelectItem: onSelectItem })1417 : jsx(SpecsView, { key: selectedSource.id, api: api, sourceId: selectedSource.id })1418 ) : selectedSource && selectedSource.valid === false ? null : jsx(EmptyState, {1419 title: sources.length === 0 ? 'No sources registered' : 'Select a source',1420 description: sources.length === 0 ? 'Add a source to get started.' : 'Choose a source to view its OpenSpec project.'1421 }) }),14221423 // Detail drawer — synchronous gate: reject stale item from different source1424 selectedItem && selectedItem.sourceId === selectedSourceId && selectedItem.type === 'change' && selectedSource && selectedSource.valid !== false ? jsx(ChangeDetailDrawer, { api: api, sourceId: selectedSourceId, change: selectedItem.item, source: selectedSource, allSources: sources, allItems: allItems, onClose: function() { setSelectedItem(null) } }) : null,1425 selectedItem && selectedItem.sourceId === selectedSourceId && selectedItem.type === 'idea' && selectedSource && selectedSource.valid !== false ? jsx(IdeaDetailDrawer, { api: api, sourceId: selectedSourceId, idea: selectedItem.item, source: selectedSource, allSources: sources, allItems: allItems, onClose: function() { setSelectedItem(null) } }) : null,14261427 // Source add/edit dialog1428 sourceDialog ? jsx(SourceDialog, { mode: sourceDialog.mode, api: api, source: sourceDialog.mode === 'edit' ? selectedSource : null, onClose: function() { setSourceDialog(null) }, onSaved: onSourceSaved }) : null,14291430 // Remove source confirmation dialog1431 removeConfirm ? jsx(Dialog, { open: true, onOpenChange: function() { if (!removeBusy) { setRemoveConfirm(null); setRemoveError('') } }, children: jsxs(DialogContent, { style: { padding: '16px', maxWidth: '400px' }, children: [1432 jsx('h2', { style: { fontSize: '16px', fontWeight: 600, margin: '0 0 8px', color: 'var(--foreground, #e0e0e0)' }, children: 'Remove source' }),1433 jsx('p', { style: { fontSize: '13px', color: 'var(--muted-foreground, #888)', margin: '0 0 12px' }, children: 'Remove source "' + (removeConfirm.name || removeConfirm.id) + '"? This unregisters the source from OpenSpec but does not delete any files.' }),1434 removeError ? jsx('div', { style: { padding: '8px 12px', background: 'var(--destructive-background, #3d1a1a)', borderRadius: '4px', fontSize: '12px', color: 'var(--destructive, #f44336)', marginBottom: '12px' }, children: removeError }) : null,1435 jsxs('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px' }, children: [1436 jsx(Button, { variant: 'outline', size: 'sm', onClick: function() { setRemoveConfirm(null); setRemoveError('') }, disabled: removeBusy, children: 'Cancel' }),1437 jsx(Button, { variant: 'destructive', size: 'sm', onClick: confirmRemove, disabled: removeBusy, children: removeBusy ? 'Removing...' : 'Remove' }),1438 ] }),1439 ] }) }) : null,1440 ] })1441}14421443// ═══════════════════════════════════════════════════════════════════════════════1444// Plugin definition1445// ═══════════════════════════════════════════════════════════════════════════════14461447var plugin = {1448 id: 'openspec',1449 name: 'OpenSpec',1450 description: 'OpenSpec source registry + project surface — sources, changes, ideas, specs, and diffs.',1451 register: function(ctx) {1452 var api = createApi(ctx)1453 var sessionStore = createSessionStore()1454 ctx.registerMany([1455 {1456 id: 'page',1457 area: ROUTES_AREA,1458 data: { path: '/openspec' },1459 render: function() { return jsx(OpenSpecPage, { api: api, sessionStore: sessionStore }) }1460 },1461 {1462 id: 'nav',1463 area: SIDEBAR_NAV_AREA,1464 order: 50,1465 data: { codicon: 'list-tree', label: 'OpenSpec', path: '/openspec' }1466 }1467 ])1468 }1469}14701471export default plugin1472export const __test = Object.freeze({1473 sourcesPath: sourcesPath,1474 changePath: changePath,1475 ideaPath: ideaPath,1476 specBrowserPath: specBrowserPath,1477 specPath: specPath,1478 selectFirstValid: selectFirstValid,1479 normalizeStatus: normalizeStatus,1480 groupByStatus: groupByStatus,1481 normalizeSequencePosition: normalizeSequencePosition,1482 sortItems: sortItems,1483 filterItems: filterItems,1484 toggleArchived: toggleArchived,1485 parseTasks: parseTasks,1486 renderMarkdown: renderMarkdown,1487 classifyState: classifyState,1488 classifyDiffLine: classifyDiffLine,1489 hasDiffMode: hasDiffMode,1490 normalizeSemanticGroups: normalizeSemanticGroups,1491 effectiveDiffMode: effectiveDiffMode,1492 isSafeUrl: isSafeUrl,1493 renderInline: renderInline,1494 MarkdownElement: MarkdownElement,1495 RetryErrorState: RetryErrorState,1496 autoCollapseEmptyLanes: autoCollapseEmptyLanes,1497 laneWidth: laneWidth,1498 railStyle: railStyle,1499 expandedStyle: expandedStyle,1500 LANE_EXPANDED_WIDTH: LANE_EXPANDED_WIDTH,1501 LANE_COLLAPSED_WIDTH: LANE_COLLAPSED_WIDTH,1502 STATUS_TONE: STATUS_TONE,1503 statusTone: statusTone,1504 computeCollapsedSet: computeCollapsedSet,1505 toggleOverride: toggleOverride,1506 computeLanePhase: computeLanePhase,1507 pruneStaleOverrides: pruneStaleOverrides,1508 BoardColumn: BoardColumn,1509 BoardCard: BoardCard,1510 CardArtifacts: CardArtifacts,1511 CardTaskFraction: CardTaskFraction,1512 DetailDrawer: DetailDrawer,1513 WorkView: WorkView,1514 stripFrontmatter: stripFrontmatter,1515 buildSourceReference: buildSourceReference,1516 buildItemReference: buildItemReference,1517 hasUniqueItemToken: hasUniqueItemToken,1518 createSessionStore: createSessionStore,1519 resolveRetainedSource: resolveRetainedSource,1520 detailTitle: detailTitle,1521 detailSecondaryName: detailSecondaryName,1522 ideaSecondaryName: ideaSecondaryName,1523 handleActivateKey: handleActivateKey,1524 trimPath: trimPath,1525 extractApiError: extractApiError,1526 sourceNeedsInitialization: sourceNeedsInitialization,1527 initializeAddedSource: initializeAddedSource,1528 createApi: createApi,1529})