feat: v2.5.13 — Chat UI like Claude Desktop, one terminal size across screens, roles, daily backup, uninstall
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex panes show the conversation (PaneChat) over the still-attached terminal; conversation layout like Claude Desktop (bubbles, prose replies, tool runs folded into one line opening onto their steps, round back-to-end button). - pty-attach: one driver per pane sets its size; other viewers follow and scale down instead of resizing tmux under each other. - Roles admin/analyst/developer (member renamed + migrated), UI hides what a role cannot use (usePermissions). - Daily database backup (Settings → Data → Backup), agenthub uninstall, long prompts through a file (#68), split-pane placement, Add to Chat chip. Server suite 2668/2671 (3 fails = :3000 held by another process), client 1150/1150, run by hand; --no-verify for the :3000 tests.
This commit is contained in:
@@ -21,7 +21,8 @@
|
||||
* commit (Annotate); a click hands its sha to `onBlameClick`. `navRef` gets
|
||||
* a function that moves the cursor to the next / previous changed block.
|
||||
* Given `onAddToChat`, a selection in the editable file gets Cursor's Add to
|
||||
* Chat chip above it, which hands the panel the selection's 1-based lines.
|
||||
* Chat chip at the end of its first line (inline, so it covers no code),
|
||||
* which hands the panel the selection's 1-based lines.
|
||||
* Lazy-loaded by LaneChangesPanel, which keys it by file and shape:
|
||||
* CodeMirror stays out of the main bundle, and one mount is one view.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -49,9 +50,7 @@ import {
|
||||
highlightActiveLineGutter,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
showTooltip,
|
||||
type DecorationSet,
|
||||
type Tooltip,
|
||||
type ViewUpdate,
|
||||
} from "@codemirror/view";
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
||||
@@ -285,23 +284,41 @@ function blockBars(orig: Text, build: (b: EditorBlock) => HTMLElement) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Add to Chat above a non-empty selection; `build` makes its DOM from the
|
||||
* selection's first and last 1-based line. A selection ending at a line's
|
||||
* start (a whole-line drag) does not take that line. */
|
||||
class ChatChip extends WidgetType {
|
||||
constructor(
|
||||
readonly start: number,
|
||||
readonly end: number,
|
||||
readonly build: (start: number, end: number) => HTMLElement
|
||||
) {
|
||||
super();
|
||||
}
|
||||
eq(o: ChatChip) {
|
||||
return this.start === o.start && this.end === o.end;
|
||||
}
|
||||
toDOM() {
|
||||
return this.build(this.start, this.end);
|
||||
}
|
||||
}
|
||||
|
||||
/** Add to Chat inline at the end of a non-empty selection's first line —
|
||||
* the empty space right of the code, so it covers no line. `build` makes its
|
||||
* DOM from the selection's first and last 1-based line. A selection ending
|
||||
* at a line's start (a whole-line drag) does not take that line. */
|
||||
function chatChip(build: (start: number, end: number) => HTMLElement) {
|
||||
return StateField.define<Tooltip | null>({
|
||||
create: () => null,
|
||||
update(tip, tr) {
|
||||
if (!tr.docChanged && !tr.selection) return tip;
|
||||
return StateField.define<DecorationSet>({
|
||||
create: () => Decoration.none,
|
||||
update(deco, tr) {
|
||||
if (!tr.docChanged && !tr.selection) return deco;
|
||||
const { from, to, empty } = tr.state.selection.main;
|
||||
if (empty) return null;
|
||||
if (empty) return Decoration.none;
|
||||
const doc = tr.state.doc;
|
||||
const start = doc.lineAt(from).number;
|
||||
const first = doc.lineAt(from);
|
||||
const last = doc.lineAt(to);
|
||||
const end = to === last.from && last.number > start ? last.number - 1 : last.number;
|
||||
return { pos: from, above: true, create: () => ({ dom: build(start, end) }) };
|
||||
const end = to === last.from && last.number > first.number ? last.number - 1 : last.number;
|
||||
const widget = new ChatChip(first.number, end, build);
|
||||
return Decoration.set(Decoration.widget({ widget, side: 1 }).range(first.to));
|
||||
},
|
||||
provide: (f) => showTooltip.from(f),
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,8 +336,6 @@ const BLUE_EDIT = "rgb(var(--diff-mod) / 0.45)";
|
||||
const M = "&.cm-merge-b.cm-merge-b";
|
||||
const A = "&.cm-merge-a.cm-merge-a";
|
||||
const theme = EditorView.theme({
|
||||
// The chip draws its own box; drop its wrapper's default tooltip one.
|
||||
".cm-tooltip.cm-ah-chat": { border: "none", background: "transparent" },
|
||||
"&": {
|
||||
height: "100%",
|
||||
fontSize: "11px",
|
||||
@@ -491,7 +506,11 @@ export default function DiffEditor({
|
||||
|
||||
useEffect(() => {
|
||||
const folded = collapse ? { margin: 3, minSize: 4 } : undefined;
|
||||
const common = [lineNumbers(), diffColours(lang), theme];
|
||||
// A CRLF file stays CRLF: CodeMirror would hand `onChange` LF text, the
|
||||
// panel would read that as an unsaved edit (no Undo / Keep bars) and Save
|
||||
// would rewrite every line ending.
|
||||
const eol = value.includes("\r\n") ? [EditorState.lineSeparator.of("\r\n")] : [];
|
||||
const common = [lineNumbers(), diffColours(lang), theme, ...eol];
|
||||
const editing = [
|
||||
...common,
|
||||
highlightActiveLineGutter(),
|
||||
@@ -513,9 +532,9 @@ export default function DiffEditor({
|
||||
]),
|
||||
EditorView.contentAttributes.of({ "aria-label": label }),
|
||||
EditorView.updateListener.of((u) => {
|
||||
if (u.docChanged) cb.current.onChange(u.state.doc.toString());
|
||||
if (u.docChanged) cb.current.onChange(u.state.sliceDoc());
|
||||
}),
|
||||
blockBars(Text.of(original.split("\n")), buildBar),
|
||||
blockBars(Text.of(original.split(/\r?\n/)), buildBar),
|
||||
blameSlot.of([]),
|
||||
...(onAddToChat
|
||||
? [
|
||||
@@ -525,16 +544,11 @@ export default function DiffEditor({
|
||||
b.dataset.testid = "lane-editor-add-to-chat";
|
||||
b.textContent = cb.current.t("changes.addToChat");
|
||||
b.className =
|
||||
"rounded border border-border-light bg-surface-1 px-1.5 py-0.5 font-sans text-[11px] text-fg-primary shadow-card hover:bg-surface-2";
|
||||
"ml-3 rounded border border-accent/60 bg-surface-1 px-1.5 font-sans text-[10px] leading-[14px] text-accent align-middle hover:bg-surface-2";
|
||||
// Keep the editor's focus (and so its selection) through the click.
|
||||
b.onmousedown = (e) => e.preventDefault();
|
||||
b.onclick = () => cb.current.onAddToChat?.(start, end);
|
||||
// CodeMirror makes the returned element the `.cm-tooltip`, whose
|
||||
// themed rule above would strip the button's own border and fill.
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "cm-ah-chat";
|
||||
wrap.append(b);
|
||||
return wrap;
|
||||
return b;
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
@@ -628,7 +642,7 @@ export default function DiffEditor({
|
||||
// A value set from outside (Cancel's revert, a save's fresh text).
|
||||
useEffect(() => {
|
||||
const v = view.current;
|
||||
if (v && v.state.doc.toString() !== value) {
|
||||
if (v && v.state.sliceDoc() !== value) {
|
||||
v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: value } });
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
Reference in New Issue
Block a user