dsh-synapse 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +2 -2
- package/client.js +39 -1
- package/docs/architecture.md +2 -0
- package/index.js +35 -1
- package/package.json +1 -1
- package/styles.css +13 -15
package/app.js
CHANGED
|
@@ -1154,7 +1154,7 @@ function renderCardInspector(card) {
|
|
|
1154
1154
|
? `<button type="button" data-action="open-branch" data-thread="${thread.id}" data-card="${escapeHtml(card.id)}" data-seq="${card.answer.sourceSeq}"><svg aria-hidden="true" viewBox="0 0 16 16"><circle cx="4" cy="3.5" r="1.5"/><circle cx="12" cy="3.5" r="1.5"/><circle cx="12" cy="12.5" r="1.5"/><path d="M5.5 3.5h2A2.5 2.5 0 0 1 10 6v5"/></svg>创建分支</button>`
|
|
1155
1155
|
: ''
|
|
1156
1156
|
const openDshAction = `<button class="primary" type="button" data-action="open-dsh" data-thread="${thread.id}" data-seq="${Number.isInteger(card.answer?.sourceSeq) ? card.answer.sourceSeq : ''}"><svg aria-hidden="true" viewBox="0 0 16 16"><path d="M7 3.5H4.5A1.5 1.5 0 0 0 3 5v6.5A1.5 1.5 0 0 0 4.5 13H11a1.5 1.5 0 0 0 1.5-1.5V9"/><path d="M9.5 3.5h3v3M12.4 3.6 7.5 8.5"/></svg>在 DSH 中打开</button>`
|
|
1157
|
-
return `<aside class="card-inspector${state.inspectorOpening ? ' is-opening' : ''}" aria-label="卡片详情" data-inspector-card="${escapeHtml(card.id)}"><header class="card-inspector-head"><div><div class="card-inspector-meta"><span>第 ${card.turnIndex + 1} 轮</span>${card.error === null ? '' : '<span class="card-inspector-error-status">失败</span>'}${process.length > 0 ? `<span>工具 ${process.length}</span>` : ''}</div><h2>${escapeHtml(card.question)}</h2></div><button class="card-inspector-close" type="button" data-action="close-card-inspector" aria-label="关闭卡片详情" title="关闭"><svg aria-hidden="true" viewBox="0 0 16 16"><path d="m4.5 4.5 7 7m0-7-7 7"/></svg></button></header><div class="card-inspector-scroll">${error}${answer}${processRecordsHtml}</div><footer class="card-inspector-actions">${continueAction}${branch}${openDshAction}</footer></aside>`
|
|
1157
|
+
return `<aside class="card-inspector${state.inspectorOpening ? ' is-opening' : ''}" aria-label="卡片详情" data-inspector-card="${escapeHtml(card.id)}"><header class="card-inspector-head"><div><div class="card-inspector-meta"><span>第 ${card.turnIndex + 1} 轮</span>${card.error === null ? '' : '<span class="card-inspector-error-status">失败</span>'}${process.length > 0 ? `<span>工具 ${process.length}</span>` : ''}</div><h2 title="${escapeHtml(card.question)}">${escapeHtml(card.question)}</h2></div><button class="card-inspector-close" type="button" data-action="close-card-inspector" aria-label="关闭卡片详情" title="关闭"><svg aria-hidden="true" viewBox="0 0 16 16"><path d="m4.5 4.5 7 7m0-7-7 7"/></svg></button></header><div class="card-inspector-scroll">${error}${answer}${processRecordsHtml}</div><footer class="card-inspector-actions">${continueAction}${branch}${openDshAction}</footer></aside>`
|
|
1158
1158
|
}
|
|
1159
1159
|
|
|
1160
1160
|
function renderThread() {
|
|
@@ -1201,7 +1201,7 @@ function render() {
|
|
|
1201
1201
|
const canvasControls = state.mode === 'canvas' && (threads.length > 0 || state.draft?.kind === 'new') ? `<div class="canvas-controls"><button data-action="layout" title="整理节点" aria-label="整理节点"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><rect x="2.5" y="2.5" width="4.5" height="4.5" rx="1"/><rect x="9" y="2.5" width="4.5" height="4.5" rx="1"/><rect x="2.5" y="9" width="4.5" height="4.5" rx="1"/><rect x="9" y="9" width="4.5" height="4.5" rx="1"/></svg>整理</button><button data-action="focus-active" title="定位到当前会话" aria-label="定位到当前会话"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><circle cx="8" cy="8" r="3.2"/><path d="M8 1.5v2.6M8 11.9v2.6M1.5 8h2.6M11.9 8h2.6"/></svg>定位</button><button data-action="zoom-out" aria-label="缩小" title="缩小"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M3.5 8h9"/></svg></button><span>${Math.round(state.zoom * 100)}%</span><button data-action="zoom-in" aria-label="放大" title="放大"><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M8 3.5v9M3.5 8h9"/></svg></button></div>` : ''
|
|
1202
1202
|
const detailAvailable = currentThread() !== null
|
|
1203
1203
|
const canvasTabs = `<nav class="canvas-tabs" aria-label="会话地图视图"><button class="${state.mode === 'canvas' ? 'active' : ''}" data-action="show-canvas">地图</button><button class="${state.mode === 'thread' ? 'active' : ''}" data-action="show-thread" data-thread="${state.activeId ?? ''}" ${detailAvailable ? '' : 'disabled'}>详情</button></nav>`
|
|
1204
|
-
app.innerHTML = `<main class="synapse-shell ${state.sidebarCollapsed ? 'sidebar-collapsed' : ''}"><aside class="sidebar"><div class="sidebar-brand-row"><div class="brand" aria-label="Synapse"><svg class="brand-mark" aria-hidden="true" viewBox="0 0 32 32" fill="none"><path d="M9 10.5 16 7l7 3.5M9 10.5v8L16 22m0-15v15m7-11.5v8L16 22"/><circle cx="9" cy="10" r="2.5"/><circle cx="23" cy="10" r="2.5"/><circle cx="16" cy="23" r="2.5"/></svg><strong>Synapse</strong></div><button class="sidebar-toggle" type="button" data-action="toggle-sidebar" aria-label="${state.sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}" title="${state.sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="1.75" width="12.5" height="12.5" rx="2.25"/><path d="M6 2v12"/></svg></button></div><button class="new-workspace" type="button" data-action="create-session" ${state.draft !== null ? 'disabled' : ''}><svg class="new-session-icon" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 4.75v6.5M4.75 8h6.5"/></svg><span>新会话</span></button><label class="workspace-label"><span>工作区</span><span class="workspace-select"><svg aria-hidden="true" viewBox="0 0 16 16"><path d="M2.5 4.75h3l1.2 1.5h6.8v5.5a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z"/></svg><select data-action="select-workspace" aria-label="选择工作区" ${state.draft !== null ? 'disabled' : ''}>${choices.map(item => `<option value="${item.id}" title="${escapeHtml(item.path ?? item.title)}" ${item.id === selectedWorkspaceId ? 'selected' : ''}>${escapeHtml(item.title)}</option>`).join('')}</select></span></label><div class="sidebar-heading"><span>会话</span></div><nav class="thread-tree">${threads.map(thread => `<button class="tree-row ${thread.id === state.activeId ? 'active' : ''}" data-action="select-thread" data-thread="${thread.id}" style="--thread-color:#374151"><span class="tree-dot"></span><span>${escapeHtml(threadListTitle(thread))}</span>${thread.parentId === null ? '' : '<i>分支</i>'}</button>`).join('') || '<p class="tree-empty">暂未同步会话</p>'}</nav></aside><header class="topbar"
|
|
1204
|
+
app.innerHTML = `<main class="synapse-shell ${state.sidebarCollapsed ? 'sidebar-collapsed' : ''}"><aside class="sidebar"><div class="sidebar-brand-row"><div class="brand" aria-label="Synapse"><svg class="brand-mark" aria-hidden="true" viewBox="0 0 32 32" fill="none"><path d="M9 10.5 16 7l7 3.5M9 10.5v8L16 22m0-15v15m7-11.5v8L16 22"/><circle cx="9" cy="10" r="2.5"/><circle cx="23" cy="10" r="2.5"/><circle cx="16" cy="23" r="2.5"/></svg><strong>Synapse</strong></div><button class="sidebar-toggle" type="button" data-action="toggle-sidebar" aria-label="${state.sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}" title="${state.sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="1.75" width="12.5" height="12.5" rx="2.25"/><path d="M6 2v12"/></svg></button></div><button class="new-workspace" type="button" data-action="create-session" ${state.draft !== null ? 'disabled' : ''}><svg class="new-session-icon" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 4.75v6.5M4.75 8h6.5"/></svg><span>新会话</span></button><label class="workspace-label"><span>工作区</span><span class="workspace-select"><svg aria-hidden="true" viewBox="0 0 16 16"><path d="M2.5 4.75h3l1.2 1.5h6.8v5.5a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z"/></svg><select data-action="select-workspace" aria-label="选择工作区" ${state.draft !== null ? 'disabled' : ''}>${choices.map(item => `<option value="${item.id}" title="${escapeHtml(item.path ?? item.title)}" ${item.id === selectedWorkspaceId ? 'selected' : ''}>${escapeHtml(item.title)}</option>`).join('')}</select></span></label><div class="sidebar-heading"><span>会话</span></div><nav class="thread-tree">${threads.map(thread => `<button class="tree-row ${thread.id === state.activeId ? 'active' : ''}" data-action="select-thread" data-thread="${thread.id}" style="--thread-color:#374151"><span class="tree-dot"></span><span>${escapeHtml(threadListTitle(thread))}</span>${thread.parentId === null ? '' : '<i>分支</i>'}</button>`).join('') || '<p class="tree-empty">暂未同步会话</p>'}</nav></aside><header class="topbar">${canvasControls}</header><section class="main-stage">${state.error ? `<div class="status-message" role="alert"><span>${escapeHtml(state.error)}</span><button data-action="dismiss-error" aria-label="关闭" title="关闭">×</button></div>` : ''}${canvasTabs}${view}${selectionFollowupButton()}</section></main>`
|
|
1205
1205
|
installDragging()
|
|
1206
1206
|
cacheCardConnectors()
|
|
1207
1207
|
// The initial camera from renderCanvas is inset (viewport not laid out yet);
|
package/client.js
CHANGED
|
@@ -52,13 +52,50 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
reference.release()
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
// Mounting is idempotent. A hot-reloaded or re-applied instance used to
|
|
56
|
+
// leave the previous pill (and its overlay) on the page, which showed up
|
|
57
|
+
// as two switches at different offsets. Drop whatever a previous instance
|
|
58
|
+
// left behind, including a pill the reverted header-slot build moved into
|
|
59
|
+
// the Conversation header.
|
|
60
|
+
for (const stale of document.querySelectorAll('.dsh-synapse-host, .dsh-synapse-switch')) stale.remove()
|
|
55
61
|
const style = document.createElement('style')
|
|
56
|
-
|
|
62
|
+
// One coordinate, in both views. The switch is a page-level control, so it
|
|
63
|
+
// never takes part in the Conversation header layout: seating it in a slot
|
|
64
|
+
// made it jump between the header utilities and this position on every
|
|
65
|
+
// view change. Three details keep the single seat usable:
|
|
66
|
+
// - `z-index` above the map overlay, so the map can always be left;
|
|
67
|
+
// - `-webkit-app-region: no-drag`, because the pill sits below the
|
|
68
|
+
// Windows title-bar drag band and would otherwise swallow no clicks;
|
|
69
|
+
// - the `left` offset clears the header's left-hand content, which grows
|
|
70
|
+
// rightward (title, lineage and agent-preset chips). Half the sidebar
|
|
71
|
+
// width puts the pill over the centre of the content area instead of
|
|
72
|
+
// the window; `min()` pulls it back in on narrow windows.
|
|
73
|
+
style.textContent = '.dsh-synapse-switch{position:fixed;z-index:200;top:calc(var(--dsh-frame-chrome-top,var(--dsh-frame-top-clearance,0px)) + 12px);left:min(calc(50% + 130px),calc(100% - 96px));display:flex;gap:2px;transform:translateX(-50%);border:1px solid #d1d5db;border-radius:999px;background:rgba(255,255,255,.96);padding:3px;backdrop-filter:blur(10px);-webkit-app-region:no-drag;box-shadow:0 2px 8px rgba(17,24,39,.1)}.dsh-synapse-switch button{height:28px;border:0;border-radius:999px;background:transparent;padding:0 11px;color:#6b7280;font:600 12px Inter,system-ui,sans-serif;cursor:pointer;white-space:nowrap;-webkit-app-region:no-drag}.dsh-synapse-switch button:hover{background:#f3f4f6;color:#111827}.dsh-synapse-switch button.active{background:#111827;color:#fff}.dsh-synapse-switch button:focus-visible{outline:2px solid #111827;outline-offset:2px}.dsh-synapse-overlay{position:fixed;z-index:100;inset:var(--dsh-frame-chrome-top,var(--dsh-frame-top-clearance,0px)) 0 0 0;background:#f5f7fa}.dsh-synapse-overlay.is-opening{visibility:hidden}.dsh-synapse-overlay[hidden]{display:none}.dsh-synapse-overlay iframe{display:block;width:100%;height:100%;border:0}html[data-fullscreen] .dsh-synapse-overlay{top:0}html[data-fullscreen] .dsh-synapse-switch{top:12px}'
|
|
57
74
|
document.head.append(style)
|
|
58
75
|
const host = document.createElement('div')
|
|
59
76
|
host.className = 'dsh-synapse-host'
|
|
60
77
|
host.innerHTML = '<div class="dsh-synapse-switch" role="group" aria-label="视图切换"><button type="button" data-view="dialog" class="active" aria-pressed="true">对话</button><button type="button" data-view="map" aria-pressed="false">会话地图</button></div><section class="dsh-synapse-overlay" hidden><iframe title="会话地图" src="/synapse/"></iframe></section>'
|
|
61
78
|
document.body.append(host)
|
|
79
|
+
const switchEl = host.querySelector('.dsh-synapse-switch')
|
|
80
|
+
// Two live instances (a bundle loaded twice, or a sibling that outlived a
|
|
81
|
+
// redeploy) each mount a pill; no instance can see the other's apply. Let
|
|
82
|
+
// them converge instead of racing: every pill carries its mount stamp and
|
|
83
|
+
// every instance keeps the newest one, breaking ties on document order, so
|
|
84
|
+
// both sides agree on the same winner without a reload.
|
|
85
|
+
switchEl.dataset.mountedAt = String(Date.now())
|
|
86
|
+
const convergeSwitch = () => {
|
|
87
|
+
const pills = [...document.querySelectorAll('.dsh-synapse-switch')]
|
|
88
|
+
if (pills.length < 2) return
|
|
89
|
+
let keep = pills[0]
|
|
90
|
+
for (const pill of pills) {
|
|
91
|
+
const stamp = Number(pill.dataset?.mountedAt ?? 0)
|
|
92
|
+
const best = Number(keep.dataset?.mountedAt ?? 0)
|
|
93
|
+
if (stamp > best || (stamp === best && pills.indexOf(pill) > pills.indexOf(keep))) keep = pill
|
|
94
|
+
}
|
|
95
|
+
for (const pill of pills) if (pill !== keep) pill.remove()
|
|
96
|
+
}
|
|
97
|
+
convergeSwitch()
|
|
98
|
+
const convergeSwitchLater = window.setTimeout(convergeSwitch, 500)
|
|
62
99
|
const dialogButton = host.querySelector('[data-view="dialog"]')
|
|
63
100
|
const mapButton = host.querySelector('[data-view="map"]')
|
|
64
101
|
const overlay = host.querySelector('.dsh-synapse-overlay')
|
|
@@ -327,6 +364,7 @@ window.__ModuleLoader__.load({
|
|
|
327
364
|
liveSessions.clear()
|
|
328
365
|
for (const reference of ownReferences.values()) reference.release()
|
|
329
366
|
ownReferences.clear()
|
|
367
|
+
window.clearTimeout(convergeSwitchLater)
|
|
330
368
|
host.remove()
|
|
331
369
|
style.remove()
|
|
332
370
|
}, 'synapse: web workspace switch')
|
package/docs/architecture.md
CHANGED
|
@@ -76,6 +76,8 @@ With `autoProjection` enabled, committed DSH session events are grouped by worki
|
|
|
76
76
|
|
|
77
77
|
Each user question becomes a conversation card. The following assistant messages are folded into that turn, and the final assistant reply is shown as the answer. Forked sessions connect to the parent turn at the durable DSH seed boundary rather than at an arbitrary canvas coordinate.
|
|
78
78
|
|
|
79
|
+
Only a turn a person typed becomes a card. The harness also writes its own `user/message` rows — the runtime-context snapshot, the skill catalogue `<system-reminder>` — and those carry a structured `data.source.kind`; injected kinds (`skill-catalog`, plus the runtime snapshot's text marker) are not projected, so they neither occupy a card nor steal the answer from the real question. Existing projections are pruned the same way when the store loads.
|
|
80
|
+
|
|
79
81
|
Projected card text is capped at 8000 characters. Longer messages receive a truncation marker in the card, while their complete content remains available from the conversation detail view.
|
|
80
82
|
|
|
81
83
|
Projection writes are coalesced during event bursts, and live updates reuse cached Markdown and patch the active card instead of rebuilding the complete canvas. Card coordinates remain visual metadata only and never determine conversation lineage.
|
package/index.js
CHANGED
|
@@ -13,6 +13,16 @@ const MAX_NOTE_LENGTH = 4_000
|
|
|
13
13
|
const MAX_PROJECTION_LENGTH = 8_000
|
|
14
14
|
const PROJECTION_TRUNCATED_SUFFIX = '\n——…(详情查看全文)'
|
|
15
15
|
const TOPIC_COLORS = ['#0f766e', '#2563eb', '#be123c', '#7c3aed', '#b45309']
|
|
16
|
+
// The harness writes `user/message` rows of its own (runtime snapshots, skill
|
|
17
|
+
// catalogues). A canvas card must stand for a turn a person actually typed, so
|
|
18
|
+
// an injected row is recognised by its structured `source.kind` instead of by
|
|
19
|
+
// guessing at the prose (issue #31 / #23).
|
|
20
|
+
const INJECTED_USER_SOURCE_KINDS = new Set(['skill-catalog'])
|
|
21
|
+
// Stored projections keep no raw event to re-read, so the one-time cleanup
|
|
22
|
+
// recognises a legacy catalogue card by the markers only that injection emits:
|
|
23
|
+
// the `<system-reminder>` opener plus the `<available_skills>` index it wraps.
|
|
24
|
+
const SKILL_CATALOG_OPENER = '<system-reminder>'
|
|
25
|
+
const SKILL_CATALOG_MARKER = '<available_skills>'
|
|
16
26
|
const LOCK_STALE_MS = 60_000
|
|
17
27
|
// Deferred (event-projection) writes coalesce into one save per window, so a
|
|
18
28
|
// burst of session events costs a single full-state write instead of one per
|
|
@@ -536,7 +546,7 @@ function normalizeState(value) {
|
|
|
536
546
|
...workspace,
|
|
537
547
|
threads: Array.isArray(workspace.threads) ? workspace.threads.map(thread => {
|
|
538
548
|
if (Array.isArray(thread.messages)) {
|
|
539
|
-
const messages = thread.messages.filter(message => !
|
|
549
|
+
const messages = thread.messages.filter(message => !isInjectedUserMessage(message))
|
|
540
550
|
if (messages.length !== thread.messages.length) migrated = true
|
|
541
551
|
return { ...thread, messages }
|
|
542
552
|
}
|
|
@@ -649,6 +659,7 @@ function requiredText(value, maxLength, field) {
|
|
|
649
659
|
function projectableEvent(event) {
|
|
650
660
|
switch (event.type) {
|
|
651
661
|
case 'user/message': {
|
|
662
|
+
if (isInjectedUserSource(event.data?.source)) return null
|
|
652
663
|
const text = contentText(event.data.content)
|
|
653
664
|
return isRuntimeContextText(text) ? null : noteProjection('user', text)
|
|
654
665
|
}
|
|
@@ -694,6 +705,29 @@ function isRuntimeContextMessage(message) {
|
|
|
694
705
|
return message?.kind === 'user' && isRuntimeContextText(message.text)
|
|
695
706
|
}
|
|
696
707
|
|
|
708
|
+
/** A harness-authored turn, identified by its structured provenance. */
|
|
709
|
+
function isInjectedUserSource(source) {
|
|
710
|
+
return typeof source?.kind === 'string' && INJECTED_USER_SOURCE_KINDS.has(source.kind)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Recognises a stored skill catalogue. Only the harness emits both markers
|
|
715
|
+
* together, so a person quoting one of them is still kept as a real turn.
|
|
716
|
+
*/
|
|
717
|
+
function isSkillCatalogText(text) {
|
|
718
|
+
if (typeof text !== 'string') return false
|
|
719
|
+
const head = text.trimStart()
|
|
720
|
+
return head.startsWith(SKILL_CATALOG_OPENER) && head.includes(SKILL_CATALOG_MARKER)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Whether a stored message must stay out of the canvas graph: runtime
|
|
725
|
+
* snapshots and skill catalogues were never a turn the user took.
|
|
726
|
+
*/
|
|
727
|
+
function isInjectedUserMessage(message) {
|
|
728
|
+
return message?.kind === 'user' && (isRuntimeContextMessage(message) || isSkillCatalogText(message.text))
|
|
729
|
+
}
|
|
730
|
+
|
|
697
731
|
function contentText(content) {
|
|
698
732
|
if (!Array.isArray(content)) return ''
|
|
699
733
|
return content.flatMap(block => {
|
package/package.json
CHANGED
package/styles.css
CHANGED
|
@@ -10,7 +10,7 @@ button.primary:hover { border-color: #000; background: #000; color: #fff; }
|
|
|
10
10
|
.synapse-shell { --sidebar-width: 280px; --sidebar-half: 140px; height: 100vh; display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); grid-template-rows: 42px minmax(0, 1fr); overflow: hidden; }
|
|
11
11
|
.synapse-shell.sidebar-collapsed { --sidebar-width: 56px; --sidebar-half: 28px; }
|
|
12
12
|
.topbar { position: relative; grid-column: 2; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 16px; border: 0; background: #fff; }
|
|
13
|
-
.brand, .top-actions, .
|
|
13
|
+
.brand, .top-actions, .thread-card-head, .thread-meta, .detail-actions, .compare-title { display: flex; align-items: center; }
|
|
14
14
|
.sidebar-brand-row { box-sizing: border-box; display: flex; align-items: center; gap: 8px; height: 60px; flex: none; margin-bottom: 8px; padding: 8px 0 8px 4px; overflow: hidden; }
|
|
15
15
|
.brand { gap: 8px; min-width: 0; flex: 1; }
|
|
16
16
|
.brand::before { width: 31px; height: 26px; flex: none; content: ''; background: center / contain no-repeat url('/synapse/deepseek-mark.svg'); }
|
|
@@ -21,11 +21,6 @@ button.primary:hover { border-color: #000; background: #000; color: #fff; }
|
|
|
21
21
|
.sidebar-toggle:hover { border: 0; background: #eef0f2; color: #374151; }
|
|
22
22
|
.sidebar-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.4; }
|
|
23
23
|
.top-actions { gap: 10px; }
|
|
24
|
-
.view-switch { position: fixed; z-index: 80; top: 12px; left: 50%; gap: 2px; padding: 3px; transform: translateX(-50%); border: 1px solid #d1d5db; border-radius: 999px; background: rgba(255,255,255,.96); }
|
|
25
|
-
.view-switch button { min-height: 28px; border: 0; border-radius: 999px; background: transparent; padding: 0 11px; color: #64748b; font-size: 12px; font-weight: 650; }
|
|
26
|
-
.view-switch button:hover { background: #e5e7eb; color: #111827; }
|
|
27
|
-
.view-switch button.active { background: #111827; color: #fff; }
|
|
28
|
-
.view-switch button:focus-visible { outline: 2px solid #111827; outline-offset: 2px; }
|
|
29
24
|
.canvas-controls { position: fixed; z-index: 80; top: 14px; right: 16px; display: flex; align-items: center; gap: 5px; }
|
|
30
25
|
.canvas-controls button { min-width: 32px; padding: 0 8px; font-size: 12px; display: inline-flex; align-items: center; gap: 4px; }
|
|
31
26
|
.canvas-controls button svg { width: 14px; height: 14px; flex: none; }
|
|
@@ -135,14 +130,19 @@ button.primary:hover { border-color: #000; background: #000; color: #fff; }
|
|
|
135
130
|
.thread-card footer button:hover { background: #f3f4f6; color: #111827; border-radius: 6px; }
|
|
136
131
|
.thread-card footer .compare-button { margin-right: auto; color: #374151; }
|
|
137
132
|
.thread-card footer .compare-button.selected { background: #e5e7eb; color: #111827; }
|
|
138
|
-
.card-inspector { position: absolute; z-index: 30; top: 0; right: 0; bottom: 0; display: flex; width: min(460px, calc(100% - 48px)); min-width: 360px; flex-direction: column; border-left: 1px solid #dbe3ec; background: #fff; opacity: 1; transform: translateX(0); transition: transform 180ms cubic-bezier(.23,1,.32,1), opacity 150ms ease-out; }
|
|
133
|
+
.card-inspector { position: absolute; z-index: 30; top: 0; right: 0; bottom: 0; display: flex; width: min(460px, calc(100% - 48px)); min-width: 360px; flex-direction: column; overflow: hidden; border-left: 1px solid #dbe3ec; background: #fff; opacity: 1; transform: translateX(0); transition: transform 180ms cubic-bezier(.23,1,.32,1), opacity 150ms ease-out; }
|
|
139
134
|
.card-inspector.is-opening, .card-inspector.is-closing { opacity: 0; transform: translateX(28px); }
|
|
140
|
-
.card-inspector-head { display: grid; grid-template-columns: minmax(0, 1fr) 32px; gap: 12px; align-items: start; padding: 18px 20px 12px; background: #fff; }
|
|
135
|
+
.card-inspector-head { display: grid; flex: none; min-height: 0; grid-template-columns: minmax(0, 1fr) 32px; gap: 12px; align-items: start; padding: 18px 20px 12px; background: #fff; }
|
|
141
136
|
.card-inspector-meta { display: flex; align-items: center; gap: 7px; color: #8390a0; font-size: 11px; }
|
|
142
137
|
.card-inspector-meta span + span::before { content: '·'; margin-right: 7px; color: #bdc7d2; }
|
|
143
138
|
.card-inspector-meta span:last-child { color: inherit; font-weight: inherit; }
|
|
144
139
|
.card-inspector-meta .card-inspector-error-status { color: #b42318; font-weight: 650; }
|
|
145
|
-
|
|
140
|
+
/* The card question is a user prompt and can be arbitrarily long (pasted
|
|
141
|
+
logs, injected skill catalogues). Clamp it so the fixed-height drawer keeps
|
|
142
|
+
its answer scroll area and action bar on screen instead of letting the
|
|
143
|
+
header grow past the panel; the full text stays reachable via `title` and
|
|
144
|
+
in the thread detail. */
|
|
145
|
+
.card-inspector-head h2 { display: -webkit-box; margin: 8px 0 0; max-height: calc(1.5em * 3); overflow: hidden; color: #172033; font-size: 15px; font-weight: 680; line-height: 1.5; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
|
|
146
146
|
.card-inspector-close { display: grid; width: 32px; height: 32px; min-height: 32px; place-items: center; border: 0; border-radius: 50%; background: transparent; padding: 0; color: #718096; }
|
|
147
147
|
.card-inspector-close:hover { background: #eef2f6; color: #1f2937; }
|
|
148
148
|
.card-inspector-close:active, .card-inspector-actions button:active { transform: scale(.97); }
|
|
@@ -207,7 +207,9 @@ button.primary:hover { border-color: #000; background: #000; color: #fff; }
|
|
|
207
207
|
.detail-badge { display: inline-flex; align-items: center; gap: 5px; border-radius: 999px; background: #eaf0fa; padding: 3px 9px; color: #3b6fd4; font-size: 11px; font-weight: 750; letter-spacing: .05em; }
|
|
208
208
|
.detail-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
209
209
|
.detail-subtitle { overflow: hidden; color: #8a97a8; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
|
210
|
-
|
|
210
|
+
/* Same overlong-question guard for the detail view: the header is `flex: none`,
|
|
211
|
+
so an unclamped title pushes the message list and composer out of the view. */
|
|
212
|
+
.detail-head h1 { display: -webkit-box; margin: 10px 0 0; max-height: calc(1.35em * 3); overflow: hidden; color: #152033; font-size: 21px; font-weight: 650; line-height: 1.35; letter-spacing: 0; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
|
|
211
213
|
.detail-head-actions { flex: none; display: flex; gap: 8px; }
|
|
212
214
|
.detail-head-actions button { min-height: 32px; border: 1px solid #d9e1ea; border-radius: 7px; background: #fff; padding: 0 12px; color: #3d4a5c; font-size: 12px; font-weight: 600; }
|
|
213
215
|
.detail-head-actions button:hover { border-color: #b9c7d8; background: #f4f7fb; }
|
|
@@ -285,7 +287,7 @@ button.primary:hover { border-color: #000; background: #000; color: #fff; }
|
|
|
285
287
|
.compare-notes { display: grid; gap: 1px; padding: 0 16px 16px; }
|
|
286
288
|
.compare-notes p { margin: 0; padding: 11px 0; border-top: 1px solid #eef2f5; color: #475569; font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
|
287
289
|
@media (max-width: 760px) { .synapse-shell { --sidebar-width: 200px; --sidebar-half: 100px; } .synapse-shell.sidebar-collapsed { --sidebar-width: 56px; --sidebar-half: 28px; } .sidebar { padding: 6px 10px; } .detail-head { display: grid; gap: 12px; padding: 22px 18px 14px; } .detail-scroll { padding: 16px 18px 20px; } .message-composer { padding: 12px 18px 16px; } .message-user { max-width: 92%; } .compare-view { padding: 26px 18px; } .compare-columns { grid-template-columns: 1fr; } .topbar { padding: 0 10px; } .brand strong { display: none; } .card-inspector { top: auto; width: 100%; min-width: 0; max-height: 78%; border-top: 1px solid #dbe3ec; border-left: 0; } .card-inspector.is-opening, .card-inspector.is-closing { transform: translateY(24px); } }
|
|
288
|
-
@media (max-width: 560px) { .synapse-shell { min-width: 0; grid-template-columns: 1fr; } .sidebar { display: none; } .topbar, .main-stage { grid-column: 1; }
|
|
290
|
+
@media (max-width: 560px) { .synapse-shell { min-width: 0; grid-template-columns: 1fr; } .sidebar { display: none; } .topbar, .main-stage { grid-column: 1; } .top-actions > button { font-size: 11px; padding: 0 7px; } .thread-card { width: 280px; } }
|
|
289
291
|
@media (prefers-reduced-motion: reduce) { .card-inspector { transition: opacity 120ms ease; } .card-inspector.is-opening, .card-inspector.is-closing { transform: none; } }
|
|
290
292
|
|
|
291
293
|
/* ── Dark theme: follows DSH's body[data-ds-dark-theme] via synapse:theme ── */
|
|
@@ -368,10 +370,6 @@ button.primary:hover { border-color: #000; background: #000; color: #fff; }
|
|
|
368
370
|
[data-theme="dark"] .draft-branch-form textarea { border-color: #2a2d33; background: #15171b; color: #e6e8eb; }
|
|
369
371
|
[data-theme="dark"] .draft-actions button { background: #1b1c1f; color: #a7adb5; }
|
|
370
372
|
[data-theme="dark"] .draft-actions button.primary { background: #3478f6; color: #fff; }
|
|
371
|
-
[data-theme="dark"] .view-switch { border-color: #2a2d33; background: rgba(21,23,27,.96); }
|
|
372
|
-
[data-theme="dark"] .view-switch button { color: #8f959e; }
|
|
373
|
-
[data-theme="dark"] .view-switch button:hover { background: #26282d; color: #e6e8eb; }
|
|
374
|
-
[data-theme="dark"] .view-switch button.active { background: #e6e8eb; color: #111317; }
|
|
375
373
|
[data-theme="dark"] .canvas-controls span { color: #8f959e; }
|
|
376
374
|
[data-theme="dark"] .sidebar-toggle { color: #a7adb5; }
|
|
377
375
|
[data-theme="dark"] .sidebar-toggle:hover { background: #26282d; color: #e6e8eb; }
|