mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
feat(session): Obsidian-style live preview notes (#556)
* feat(session): add live-preview notes scratchpad Add a Notes panel between chat and right nav with a CodeMirror-based markdown editor that hides heading and emphasis markers unless the cursor is on that line/segment. * fix(notes): render live-preview decorations Switch markdown live-preview styling to a StateField-backed decorations pipeline and use a zero-width widget for marker hiding. Update evidence screenshots to show headings/emphasis rendering.
This commit is contained in:
@@ -27,6 +27,11 @@
|
||||
"bump:set": "node scripts/bump-version.mjs --set"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-markdown": "^6.3.3",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.0",
|
||||
"@opencode-ai/sdk": "^1.1.31",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@solid-primitives/event-bus": "^1.1.2",
|
||||
|
||||
341
packages/app/src/app/components/live-markdown-editor.tsx
Normal file
341
packages/app/src/app/components/live-markdown-editor.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
import { createEffect, onCleanup, onMount } from "solid-js";
|
||||
|
||||
import { EditorState, StateField } from "@codemirror/state";
|
||||
import {
|
||||
Decoration,
|
||||
type DecorationSet,
|
||||
EditorView,
|
||||
WidgetType,
|
||||
keymap,
|
||||
placeholder as cmPlaceholder,
|
||||
} from "@codemirror/view";
|
||||
import { history, defaultKeymap, historyKeymap, indentWithTab } from "@codemirror/commands";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
ariaLabel?: string;
|
||||
autofocus?: boolean;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
type EmphasisRange = {
|
||||
kind: "em" | "strong";
|
||||
openFrom: number;
|
||||
openTo: number;
|
||||
closeFrom: number;
|
||||
closeTo: number;
|
||||
contentFrom: number;
|
||||
contentTo: number;
|
||||
};
|
||||
|
||||
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
|
||||
|
||||
const findEmphasisRanges = (line: string): EmphasisRange[] => {
|
||||
// Minimal, line-local emphasis parsing.
|
||||
// - Strong: **text**
|
||||
// - Emphasis: *text*
|
||||
// Avoid matching markers that wrap whitespace.
|
||||
|
||||
const ranges: EmphasisRange[] = [];
|
||||
|
||||
const used = new Array(line.length).fill(false);
|
||||
const markUsed = (from: number, to: number) => {
|
||||
for (let i = clamp(from, 0, line.length); i < clamp(to, 0, line.length); i += 1) used[i] = true;
|
||||
};
|
||||
const isUsed = (from: number, to: number) => {
|
||||
for (let i = clamp(from, 0, line.length); i < clamp(to, 0, line.length); i += 1) {
|
||||
if (used[i]) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Strong first.
|
||||
{
|
||||
const re = /\*\*(?!\s)([^*\n]+?)(?<!\s)\*\*/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(line))) {
|
||||
const full = m[0] ?? "";
|
||||
const inner = m[1] ?? "";
|
||||
const from = m.index;
|
||||
const to = from + full.length;
|
||||
if (!full || !inner) continue;
|
||||
if (isUsed(from, to)) continue;
|
||||
|
||||
ranges.push({
|
||||
kind: "strong",
|
||||
openFrom: from,
|
||||
openTo: from + 2,
|
||||
contentFrom: from + 2,
|
||||
contentTo: to - 2,
|
||||
closeFrom: to - 2,
|
||||
closeTo: to,
|
||||
});
|
||||
markUsed(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
// Emphasis.
|
||||
{
|
||||
const re = /\*(?!\s)([^*\n]+?)(?<!\s)\*/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(line))) {
|
||||
const full = m[0] ?? "";
|
||||
const inner = m[1] ?? "";
|
||||
const from = m.index;
|
||||
const to = from + full.length;
|
||||
if (!full || !inner) continue;
|
||||
if (isUsed(from, to)) continue;
|
||||
|
||||
ranges.push({
|
||||
kind: "em",
|
||||
openFrom: from,
|
||||
openTo: from + 1,
|
||||
contentFrom: from + 1,
|
||||
contentTo: to - 1,
|
||||
closeFrom: to - 1,
|
||||
closeTo: to,
|
||||
});
|
||||
markUsed(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
};
|
||||
|
||||
class HiddenMarkerWidget extends WidgetType {
|
||||
toDOM() {
|
||||
// Zero-width widget that collapses the marker range.
|
||||
const el = document.createElement("span");
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
el.style.display = "inline-block";
|
||||
el.style.width = "0";
|
||||
el.style.overflow = "hidden";
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
const obsidianishLivePreview = () => {
|
||||
const headingLine = (level: number) =>
|
||||
Decoration.line({ attributes: { class: `cm-ow-heading cm-ow-heading-${level}` } });
|
||||
const hide = Decoration.replace({ widget: new HiddenMarkerWidget() });
|
||||
const emMark = Decoration.mark({ class: "cm-ow-em" });
|
||||
const strongMark = Decoration.mark({ class: "cm-ow-strong" });
|
||||
|
||||
const compute = (state: EditorState): DecorationSet => {
|
||||
const ranges: any[] = [];
|
||||
const add = (from: number, to: number, deco: any) => {
|
||||
ranges.push(deco.range(from, to));
|
||||
};
|
||||
|
||||
const doc = state.doc;
|
||||
const selections = state.selection.ranges;
|
||||
const activeLines = new Set<number>();
|
||||
for (const r of selections) {
|
||||
const fromLine = doc.lineAt(r.from).number;
|
||||
const toLine = doc.lineAt(r.to).number;
|
||||
for (let n = fromLine; n <= toLine; n += 1) activeLines.add(n);
|
||||
if (r.empty) activeLines.add(doc.lineAt(r.head).number);
|
||||
}
|
||||
|
||||
const cursorPos = state.selection.main.head;
|
||||
for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber += 1) {
|
||||
const line = doc.line(lineNumber);
|
||||
const lineText = line.text;
|
||||
const lineActive = activeLines.has(line.number);
|
||||
|
||||
// Headings: hide leading '#' when line is not active.
|
||||
const headingMatch = /^(#{1,6})\s+/.exec(lineText);
|
||||
if (headingMatch) {
|
||||
const level = headingMatch[1]?.length ?? 1;
|
||||
add(line.from, line.from, headingLine(level));
|
||||
|
||||
if (!lineActive) {
|
||||
const markerLen = (headingMatch[0] ?? "").length;
|
||||
add(line.from, line.from + markerLen, hide);
|
||||
}
|
||||
}
|
||||
|
||||
// Emphasis / strong emphasis: style inner text; hide markers unless cursor is inside.
|
||||
for (const r of findEmphasisRanges(lineText)) {
|
||||
const absOpenFrom = line.from + r.openFrom;
|
||||
const absOpenTo = line.from + r.openTo;
|
||||
const absCloseFrom = line.from + r.closeFrom;
|
||||
const absCloseTo = line.from + r.closeTo;
|
||||
const absContentFrom = line.from + r.contentFrom;
|
||||
const absContentTo = line.from + r.contentTo;
|
||||
if (absContentTo <= absContentFrom) continue;
|
||||
|
||||
const cursorInside = cursorPos >= absOpenFrom && cursorPos <= absCloseTo;
|
||||
const mark = r.kind === "strong" ? strongMark : emMark;
|
||||
|
||||
if (!cursorInside) {
|
||||
add(absOpenFrom, absOpenTo, hide);
|
||||
}
|
||||
|
||||
add(absContentFrom, absContentTo, mark);
|
||||
|
||||
if (!cursorInside) {
|
||||
add(absCloseFrom, absCloseTo, hide);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Decoration.set(ranges, true);
|
||||
};
|
||||
|
||||
const field = StateField.define<DecorationSet>({
|
||||
create(state) {
|
||||
return compute(state);
|
||||
},
|
||||
update(value, tr) {
|
||||
if (tr.docChanged || tr.selection) return compute(tr.state);
|
||||
return value;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
"&": {
|
||||
fontSize: "14px",
|
||||
},
|
||||
".cm-scroller": {
|
||||
fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial",
|
||||
},
|
||||
".cm-content": {
|
||||
padding: "12px 14px",
|
||||
caretColor: "var(--dls-text-primary)",
|
||||
},
|
||||
".cm-line": {
|
||||
padding: "0 2px",
|
||||
},
|
||||
".cm-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
".cm-selectionBackground": {
|
||||
backgroundColor: "rgba(var(--dls-accent-rgb) / 0.18)",
|
||||
},
|
||||
".cm-focused .cm-selectionBackground": {
|
||||
backgroundColor: "rgba(var(--dls-accent-rgb) / 0.22)",
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderLeftColor: "var(--dls-text-primary)",
|
||||
},
|
||||
".cm-placeholder": {
|
||||
color: "var(--dls-text-secondary)",
|
||||
},
|
||||
".cm-ow-em": {
|
||||
fontStyle: "italic",
|
||||
},
|
||||
".cm-ow-strong": {
|
||||
fontWeight: "650",
|
||||
},
|
||||
".cm-ow-heading": {
|
||||
letterSpacing: "-0.01em",
|
||||
},
|
||||
".cm-line.cm-ow-heading-1": {
|
||||
fontSize: "28px",
|
||||
fontWeight: "750",
|
||||
lineHeight: "1.15",
|
||||
paddingTop: "6px",
|
||||
paddingBottom: "6px",
|
||||
},
|
||||
".cm-line.cm-ow-heading-2": {
|
||||
fontSize: "22px",
|
||||
fontWeight: "720",
|
||||
lineHeight: "1.2",
|
||||
paddingTop: "6px",
|
||||
paddingBottom: "6px",
|
||||
},
|
||||
".cm-line.cm-ow-heading-3": {
|
||||
fontSize: "18px",
|
||||
fontWeight: "700",
|
||||
lineHeight: "1.25",
|
||||
paddingTop: "5px",
|
||||
paddingBottom: "5px",
|
||||
},
|
||||
".cm-line.cm-ow-heading-4": {
|
||||
fontSize: "16px",
|
||||
fontWeight: "680",
|
||||
lineHeight: "1.3",
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
},
|
||||
".cm-line.cm-ow-heading-5": {
|
||||
fontSize: "15px",
|
||||
fontWeight: "660",
|
||||
lineHeight: "1.35",
|
||||
paddingTop: "3px",
|
||||
paddingBottom: "3px",
|
||||
},
|
||||
".cm-line.cm-ow-heading-6": {
|
||||
fontSize: "14px",
|
||||
fontWeight: "650",
|
||||
lineHeight: "1.4",
|
||||
paddingTop: "2px",
|
||||
paddingBottom: "2px",
|
||||
},
|
||||
});
|
||||
|
||||
export default function LiveMarkdownEditor(props: Props) {
|
||||
let hostEl: HTMLDivElement | undefined;
|
||||
let view: EditorView | undefined;
|
||||
|
||||
const createState = (doc: string) =>
|
||||
EditorState.create({
|
||||
doc,
|
||||
extensions: [
|
||||
history(),
|
||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||
markdown(),
|
||||
EditorView.lineWrapping,
|
||||
cmPlaceholder(props.placeholder ?? ""),
|
||||
editorTheme,
|
||||
obsidianishLivePreview(),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (!update.docChanged) return;
|
||||
props.onChange(update.state.doc.toString());
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!hostEl) return;
|
||||
view = new EditorView({
|
||||
state: createState(props.value ?? ""),
|
||||
parent: hostEl,
|
||||
});
|
||||
|
||||
if (props.autofocus) {
|
||||
queueMicrotask(() => view?.focus());
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!view) return;
|
||||
const next = props.value ?? "";
|
||||
const current = view.state.doc.toString();
|
||||
if (next === current) return;
|
||||
view.dispatch({ changes: { from: 0, to: current.length, insert: next } });
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
view?.destroy();
|
||||
view = undefined;
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class={props.class}
|
||||
aria-label={props.ariaLabel}
|
||||
role="textbox"
|
||||
ref={(el) => (hostEl = el)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
62
packages/app/src/app/components/session/scratchpad-panel.tsx
Normal file
62
packages/app/src/app/components/session/scratchpad-panel.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Show } from "solid-js";
|
||||
import { FileText, Trash2, X } from "lucide-solid";
|
||||
|
||||
import LiveMarkdownEditor from "../live-markdown-editor";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onClose?: () => void;
|
||||
onClear?: () => void;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export default function ScratchpadPanel(props: Props) {
|
||||
const title = () => props.title ?? "Notes";
|
||||
const canClear = () => typeof props.onClear === "function";
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full min-h-0">
|
||||
<div class="h-14 px-4 border-b border-dls-border flex items-center justify-between bg-dls-sidebar">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<FileText size={16} class="text-dls-secondary shrink-0" />
|
||||
<div class="text-sm font-semibold text-dls-text truncate">{title()}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Show when={canClear()}>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-lg text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
onClick={() => props.onClear?.()}
|
||||
title="Clear notes"
|
||||
aria-label="Clear notes"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={typeof props.onClose === "function"}>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-lg text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
onClick={() => props.onClose?.()}
|
||||
title="Close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<LiveMarkdownEditor
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
placeholder="# Write notes like Obsidian\n\n- Use # headings\n- Use *italic* and **bold**\n"
|
||||
ariaLabel="Scratchpad"
|
||||
class="h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
FileText,
|
||||
HardDrive,
|
||||
History,
|
||||
ListTodo,
|
||||
@@ -63,6 +65,7 @@ import soulSetupTemplate from "../data/commands/give-me-a-soul.md?raw";
|
||||
import MessageList from "../components/session/message-list";
|
||||
import Composer from "../components/session/composer";
|
||||
import type { SidebarSectionState } from "../components/session/sidebar";
|
||||
import ScratchpadPanel from "../components/session/scratchpad-panel";
|
||||
import FlyoutItem from "../components/flyout-item";
|
||||
import QuestionModal from "../components/question-modal";
|
||||
import ArtifactsPanel from "../components/session/artifacts-panel";
|
||||
@@ -238,6 +241,78 @@ export default function SessionView(props: SessionViewProps) {
|
||||
const [markdownEditorOpen, setMarkdownEditorOpen] = createSignal(false);
|
||||
const [markdownEditorPath, setMarkdownEditorPath] = createSignal<string | null>(null);
|
||||
|
||||
const SCRATCHPAD_OPEN_KEY = "openwork.scratchpad.open.v1";
|
||||
const SCRATCHPAD_VALUE_PREFIX = "openwork.scratchpad.value.v1";
|
||||
const readBool = (key: string, fallback: boolean) => {
|
||||
if (typeof window === "undefined") return fallback;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
if (raw === "true") return true;
|
||||
if (raw === "false") return false;
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
const writeBool = (key: string, value: boolean) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(key, value ? "true" : "false");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
const readString = (key: string) => {
|
||||
if (typeof window === "undefined") return "";
|
||||
try {
|
||||
return window.localStorage.getItem(key) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const writeString = (key: string, value: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
const trimmed = value ?? "";
|
||||
if (!trimmed.trim()) {
|
||||
window.localStorage.removeItem(key);
|
||||
} else {
|
||||
window.localStorage.setItem(key, trimmed);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const [scratchpadOpen, setScratchpadOpen] = createSignal(readBool(SCRATCHPAD_OPEN_KEY, true));
|
||||
createEffect(() => writeBool(SCRATCHPAD_OPEN_KEY, scratchpadOpen()));
|
||||
|
||||
const scratchpadStorageKey = createMemo(() => {
|
||||
const workspaceId = props.activeWorkspaceId || "workspace";
|
||||
const sessionId = props.selectedSessionId || "draft";
|
||||
return `${SCRATCHPAD_VALUE_PREFIX}.${workspaceId}.${sessionId}`;
|
||||
});
|
||||
const [scratchpadValue, setScratchpadValue] = createSignal("");
|
||||
|
||||
createEffect(
|
||||
on(scratchpadStorageKey, (key) => {
|
||||
setScratchpadValue(readString(key));
|
||||
})
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
const key = scratchpadStorageKey();
|
||||
const value = scratchpadValue();
|
||||
let timer: number | undefined;
|
||||
if (typeof window !== "undefined") {
|
||||
timer = window.setTimeout(() => writeString(key, value), 150);
|
||||
}
|
||||
onCleanup(() => {
|
||||
if (typeof window !== "undefined" && timer) window.clearTimeout(timer);
|
||||
});
|
||||
});
|
||||
|
||||
// When a session is selected (i.e. we are in SessionView), the right sidebar is
|
||||
// navigation-only. Avoid showing any tab as "selected" to reduce confusion.
|
||||
const showRightSidebarSelection = createMemo(() => !props.selectedSessionId);
|
||||
@@ -2024,7 +2099,7 @@ export default function SessionView(props: SessionViewProps) {
|
||||
|
||||
<main class="flex-1 flex flex-col overflow-hidden bg-dls-surface">
|
||||
<header class="h-14 border-b border-dls-border flex items-center justify-between px-6 bg-dls-surface z-10 shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Show when={showUpdatePill()}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2053,9 +2128,8 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
<h1 class="text-sm font-semibold text-dls-text">
|
||||
{selectedSessionTitle() || "New task"}
|
||||
</h1>
|
||||
|
||||
<h1 class="text-sm font-semibold text-dls-text truncate">{selectedSessionTitle() || "New task"}</h1>
|
||||
<Show when={props.developerMode}>
|
||||
<span class="text-xs text-dls-secondary">{props.headerStatus}</span>
|
||||
</Show>
|
||||
@@ -2065,6 +2139,20 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class={`hidden lg:flex h-9 items-center gap-2 rounded-xl border border-dls-border px-3 text-xs font-medium transition-colors ${scratchpadOpen()
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "bg-dls-hover text-dls-secondary hover:text-dls-text hover:bg-dls-active"
|
||||
}`}
|
||||
onClick={() => setScratchpadOpen((v) => !v)}
|
||||
title="Toggle notes"
|
||||
aria-label="Toggle notes"
|
||||
>
|
||||
<FileText size={14} class="shrink-0" />
|
||||
<span>Notes</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={`h-9 w-9 flex items-center justify-center rounded-lg transition-colors ${
|
||||
@@ -2215,17 +2303,18 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex-1 flex overflow-hidden relative">
|
||||
<div
|
||||
class="flex-1 overflow-y-auto px-12 py-10 scroll-smooth bg-dls-surface"
|
||||
ref={(el) => (chatContainerEl = el)}
|
||||
>
|
||||
<div class="max-w-5xl mx-auto w-full">
|
||||
<Show when={props.messages.length === 0}>
|
||||
<div class="text-center py-16 px-6 space-y-6">
|
||||
<div class="w-16 h-16 bg-dls-hover rounded-3xl mx-auto flex items-center justify-center border border-dls-border">
|
||||
<Zap class="text-dls-secondary" />
|
||||
</div>
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<div class="flex-1 min-w-0 relative overflow-hidden">
|
||||
<div
|
||||
class="h-full overflow-y-auto px-12 py-10 scroll-smooth bg-dls-surface"
|
||||
ref={(el) => (chatContainerEl = el)}
|
||||
>
|
||||
<div class="max-w-5xl mx-auto w-full">
|
||||
<Show when={props.messages.length === 0}>
|
||||
<div class="text-center py-16 px-6 space-y-6">
|
||||
<div class="w-16 h-16 bg-dls-hover rounded-3xl mx-auto flex items-center justify-center border border-dls-border">
|
||||
<Zap class="text-dls-secondary" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-xl font-medium">What do you want to do?</h3>
|
||||
<p class="text-dls-secondary text-sm max-w-sm mx-auto">
|
||||
@@ -2295,22 +2384,34 @@ export default function SessionView(props: SessionViewProps) {
|
||||
}
|
||||
/>
|
||||
|
||||
<div ref={(el) => (messagesEndEl = el)} />
|
||||
</div>
|
||||
</div>
|
||||
<div ref={(el) => (messagesEndEl = el)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!autoScrollEnabled() && props.messages.length > 0}>
|
||||
<div class="absolute bottom-4 left-0 right-0 z-20 flex justify-center pointer-events-none">
|
||||
<button
|
||||
type="button"
|
||||
class="pointer-events-auto rounded-full border border-gray-6 bg-gray-1/90 px-4 py-2 text-xs text-gray-11 shadow-lg shadow-gray-12/5 backdrop-blur-md hover:bg-gray-2 transition-colors"
|
||||
onClick={() => scrollToLatest("smooth")}
|
||||
>
|
||||
Jump to latest
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={!autoScrollEnabled() && props.messages.length > 0}>
|
||||
<div class="absolute bottom-4 left-0 right-0 z-20 flex justify-center pointer-events-none">
|
||||
<button
|
||||
type="button"
|
||||
class="pointer-events-auto rounded-full border border-gray-6 bg-gray-1/90 px-4 py-2 text-xs text-gray-11 shadow-lg shadow-gray-12/5 backdrop-blur-md hover:bg-gray-2 transition-colors"
|
||||
onClick={() => scrollToLatest("smooth")}
|
||||
>
|
||||
Jump to latest
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={scratchpadOpen()}>
|
||||
<aside class="hidden lg:flex w-[420px] shrink-0 border-l border-dls-border bg-dls-sidebar">
|
||||
<ScratchpadPanel
|
||||
value={scratchpadValue()}
|
||||
onChange={setScratchpadValue}
|
||||
onClose={() => setScratchpadOpen(false)}
|
||||
onClear={() => setScratchpadValue("")}
|
||||
/>
|
||||
</aside>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={todoCount() > 0}>
|
||||
<div class="mx-auto w-full max-w-[68ch] px-4">
|
||||
|
||||
199
pnpm-lock.yaml
generated
199
pnpm-lock.yaml
generated
@@ -27,6 +27,21 @@ importers:
|
||||
|
||||
packages/app:
|
||||
dependencies:
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.8.0
|
||||
version: 6.10.2
|
||||
'@codemirror/lang-markdown':
|
||||
specifier: ^6.3.3
|
||||
version: 6.5.0
|
||||
'@codemirror/language':
|
||||
specifier: ^6.11.0
|
||||
version: 6.12.1
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.4
|
||||
'@codemirror/view':
|
||||
specifier: ^6.38.0
|
||||
version: 6.39.14
|
||||
'@opencode-ai/sdk':
|
||||
specifier: ^1.1.31
|
||||
version: 1.1.39
|
||||
@@ -384,6 +399,36 @@ packages:
|
||||
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@codemirror/autocomplete@6.20.0':
|
||||
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
|
||||
|
||||
'@codemirror/commands@6.10.2':
|
||||
resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
|
||||
|
||||
'@codemirror/lang-html@6.4.11':
|
||||
resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
|
||||
|
||||
'@codemirror/lang-javascript@6.2.4':
|
||||
resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==}
|
||||
|
||||
'@codemirror/lang-markdown@6.5.0':
|
||||
resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==}
|
||||
|
||||
'@codemirror/language@6.12.1':
|
||||
resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
|
||||
|
||||
'@codemirror/lint@6.9.4':
|
||||
resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==}
|
||||
|
||||
'@codemirror/state@6.5.4':
|
||||
resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
|
||||
|
||||
'@codemirror/view@6.39.14':
|
||||
resolution: {integrity: sha512-WJcvgHm/6Q7dvGT0YFv/6PSkoc36QlR0VCESS6x9tGsnF1lWLmmYxOgX3HH6v8fo6AvSLgpcs+H0Olre6MKXlg==}
|
||||
|
||||
'@dimforge/rapier2d-simd-compat@0.17.3':
|
||||
resolution: {integrity: sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg==}
|
||||
|
||||
@@ -838,6 +883,30 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@lezer/common@1.5.1':
|
||||
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
|
||||
|
||||
'@lezer/css@1.3.0':
|
||||
resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
|
||||
|
||||
'@lezer/html@1.3.13':
|
||||
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
|
||||
|
||||
'@lezer/javascript@1.5.4':
|
||||
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
|
||||
|
||||
'@lezer/lr@1.4.8':
|
||||
resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
|
||||
|
||||
'@lezer/markdown@1.6.3':
|
||||
resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@next/env@14.2.5':
|
||||
resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==}
|
||||
|
||||
@@ -1512,6 +1581,9 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2318,6 +2390,9 @@ packages:
|
||||
resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
style-mod@4.1.3:
|
||||
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||
|
||||
styled-jsx@5.1.1:
|
||||
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -2480,6 +2555,9 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
web-tree-sitter@0.25.10:
|
||||
resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==}
|
||||
peerDependencies:
|
||||
@@ -2764,6 +2842,86 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@codemirror/autocomplete@6.20.0':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@codemirror/commands@6.10.2':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.0
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/css': 1.3.0
|
||||
|
||||
'@codemirror/lang-html@6.4.11':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.0
|
||||
'@codemirror/lang-css': 6.3.1
|
||||
'@codemirror/lang-javascript': 6.2.4
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/css': 1.3.0
|
||||
'@lezer/html': 1.3.13
|
||||
|
||||
'@codemirror/lang-javascript@6.2.4':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.0
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/lint': 6.9.4
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/javascript': 1.5.4
|
||||
|
||||
'@codemirror/lang-markdown@6.5.0':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.0
|
||||
'@codemirror/lang-html': 6.4.11
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/markdown': 1.6.3
|
||||
|
||||
'@codemirror/language@6.12.1':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.8
|
||||
style-mod: 4.1.3
|
||||
|
||||
'@codemirror/lint@6.9.4':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.39.14
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/state@6.5.4':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/view@6.39.14':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
crelt: 1.0.6
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@dimforge/rapier2d-simd-compat@0.17.3':
|
||||
optional: true
|
||||
|
||||
@@ -3139,6 +3297,41 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@lezer/common@1.5.1': {}
|
||||
|
||||
'@lezer/css@1.3.0':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.8
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@lezer/html@1.3.13':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.8
|
||||
|
||||
'@lezer/javascript@1.5.4':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.8
|
||||
|
||||
'@lezer/lr@1.4.8':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@lezer/markdown@1.6.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@next/env@14.2.5': {}
|
||||
|
||||
'@next/swc-darwin-arm64@14.2.5':
|
||||
@@ -3744,6 +3937,8 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
@@ -4534,6 +4729,8 @@ snapshots:
|
||||
'@tokenizer/token': 0.3.0
|
||||
peek-readable: 4.1.0
|
||||
|
||||
style-mod@4.1.3: {}
|
||||
|
||||
styled-jsx@5.1.1(react@18.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
@@ -4680,6 +4877,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
web-tree-sitter@0.25.10: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
BIN
pr/hybrid-editor-live-preview/full.png
Normal file
BIN
pr/hybrid-editor-live-preview/full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
pr/hybrid-editor-live-preview/heading-active-line.png
Normal file
BIN
pr/hybrid-editor-live-preview/heading-active-line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
pr/hybrid-editor-live-preview/heading-live-preview.png
Normal file
BIN
pr/hybrid-editor-live-preview/heading-live-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
pr/hybrid-editor-live-preview/italic-live-preview.png
Normal file
BIN
pr/hybrid-editor-live-preview/italic-live-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
pr/hybrid-editor-live-preview/list-live-preview.png
Normal file
BIN
pr/hybrid-editor-live-preview/list-live-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Reference in New Issue
Block a user