Skip to content

Commit c5b21ac

Browse files
committed
feat(acp): add markdown and streamed code blocks
1 parent 0b7ce95 commit c5b21ac

9 files changed

Lines changed: 1419 additions & 26 deletions

File tree

anycode-base/src/editor.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export interface EditorSettings {
2424
buffer: number;
2525
}
2626

27+
export interface EditorOptions {
28+
line?: number;
29+
column?: number;
30+
theme?: any;
31+
readOnly?: boolean;
32+
}
33+
2734
export interface EditorState {
2835
code: Code;
2936
offset: number;
@@ -32,6 +39,7 @@ export interface EditorState {
3239
errorLines: Map<number, string>;
3340
settings: EditorSettings;
3441
diffs?: Map<number, DiffInfo>;
42+
readOnly?: boolean;
3543
}
3644

3745
export class AnycodeEditor {
@@ -71,14 +79,16 @@ export class AnycodeEditor {
7179
private diffEnabled: boolean = false;
7280
private originalCode?: string;
7381
private diffs?: Map<number, DiffInfo>;
82+
private readonly readOnly: boolean;
7483

7584
constructor(
7685
initialText = '',
7786
filename: string = 'test.txt',
7887
language: string = 'javascript',
79-
options: any = {}
88+
options: EditorOptions = {}
8089
) {
8190
this.code = new Code(initialText, filename, language);
91+
this.readOnly = options.readOnly ?? false;
8292
// Set initial cursor position
8393
if (options.line !== undefined && options.column !== undefined) {
8494
this.offset = this.code.getOffset(options.line, options.column);
@@ -114,18 +124,20 @@ export class AnycodeEditor {
114124

115125
this.codeContent = document.createElement('div');
116126
this.codeContent.className = 'code';
117-
this.codeContent.setAttribute("contentEditable", "true");
127+
this.codeContent.setAttribute("contentEditable", this.readOnly ? "false" : "true");
118128
this.codeContent.setAttribute("spellcheck", "false");
119129
this.codeContent.setAttribute("autocorrect", "off");
120130
this.codeContent.setAttribute("autocapitalize", "off");
131+
if (this.readOnly) {
132+
this.container.classList.add('readonly');
133+
}
121134

122135
this.container.appendChild(this.buttonsColumn);
123136
this.container.appendChild(this.gutter);
124137
this.container.appendChild(this.codeContent);
125138
}
126139

127140
public clean() {
128-
console.log('clean');
129141
this.removeEventListeners();
130142
this.offset = 0;
131143
this.selection = null;
@@ -136,10 +148,12 @@ export class AnycodeEditor {
136148
}
137149

138150
public setOnChange(func: (t: Change) => void) {
151+
if (this.readOnly) return;
139152
this.code.setOnChange(func);
140153
}
141154

142155
public setHistory(changes: Change[], index: number) {
156+
if (this.readOnly) return;
143157
this.code.setHistory(changes, index);
144158
}
145159

@@ -152,6 +166,62 @@ export class AnycodeEditor {
152166
}
153167
}
154168

169+
public updateTextIncremental(newText: string) {
170+
const currentText = this.code.getContent();
171+
if (currentText === newText) return;
172+
173+
const maxPrefix = Math.min(currentText.length, newText.length);
174+
let prefixLength = 0;
175+
while (
176+
prefixLength < maxPrefix
177+
&& currentText.charCodeAt(prefixLength) === newText.charCodeAt(prefixLength)
178+
) {
179+
prefixLength += 1;
180+
}
181+
182+
let currentSuffixStart = currentText.length;
183+
let nextSuffixStart = newText.length;
184+
while (
185+
currentSuffixStart > prefixLength
186+
&& nextSuffixStart > prefixLength
187+
&& currentText.charCodeAt(currentSuffixStart - 1) === newText.charCodeAt(nextSuffixStart - 1)
188+
) {
189+
currentSuffixStart -= 1;
190+
nextSuffixStart -= 1;
191+
}
192+
193+
const removedLength = currentSuffixStart - prefixLength;
194+
const insertedText = newText.slice(prefixLength, nextSuffixStart);
195+
196+
if (removedLength === 0 && insertedText.length === 0) return;
197+
198+
if (removedLength > 0) {
199+
this.code.remove(prefixLength, removedLength);
200+
}
201+
202+
if (insertedText.length > 0) {
203+
this.code.insert(insertedText, prefixLength);
204+
}
205+
206+
this.selection = null;
207+
this.offset = Math.min(this.offset, this.code.getContentLength());
208+
209+
if (this.diffEnabled && this.originalCode !== undefined) {
210+
const updatedText = this.code.getContent();
211+
this.diffs = computeGitChanges(this.originalCode, updatedText);
212+
} else {
213+
this.diffs = undefined;
214+
}
215+
216+
if (this.search.isActive()) {
217+
const matches = this.code.search(this.search.getPattern());
218+
this.search.setMatches(matches);
219+
}
220+
221+
this.renderer.renderChanges(this.getEditorState(), this.search);
222+
this.verifyDiffRendering();
223+
}
224+
155225
public getText(): string {
156226
return this.code.getContent();
157227
}
@@ -162,7 +232,9 @@ export class AnycodeEditor {
162232

163233
public async init() {
164234
await this.code.init();
165-
this.setupEventListeners();
235+
if (!this.readOnly) {
236+
this.setupEventListeners();
237+
}
166238
}
167239

168240
public getContainer(): HTMLDivElement {
@@ -180,6 +252,7 @@ export class AnycodeEditor {
180252
}
181253

182254
public requestFocus(line: number, column: number, center: boolean = false): void {
255+
if (this.readOnly) return;
183256
this.needFocus = true;
184257
const offset = this.code.getOffset(line, column);
185258
this.offset = offset;
@@ -303,6 +376,7 @@ export class AnycodeEditor {
303376
buffer: this.settings.buffer,
304377
},
305378
diffs: this.diffs,
379+
readOnly: this.readOnly,
306380
};
307381
}
308382

@@ -311,6 +385,7 @@ export class AnycodeEditor {
311385
}
312386

313387
public renderCursorOrSelection() {
388+
if (this.readOnly) return;
314389
this.renderer.renderCursorOrSelection(this.getEditorState());
315390
}
316391

anycode-base/src/renderer/Renderer.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ export class Renderer {
8383
}
8484

8585
public render(state: EditorState, search?: Search) {
86-
console.log("render");
87-
88-
const { code, offset, selection, runLines, errorLines, settings, diffs } = state;
86+
const { code, offset, selection, runLines, errorLines, settings, diffs, readOnly } = state;
8987

9088
// Build unified visual rows model (real lines + ghost lines)
9189
const totalRealLines = code.linesLength();
@@ -154,7 +152,7 @@ export class Renderer {
154152
this.codeContent.replaceChildren(codeFrag);
155153

156154
// Render cursor or selection
157-
if (!search || !search.isActive() || !search.isFocused()) {
155+
if (!readOnly && (!search || !search.isActive() || !search.isFocused())) {
158156
if (!selection || selection.isEmpty()) {
159157
const { line, column } = code.getPosition(offset);
160158
this.renderCursor(line, column, false);
@@ -164,7 +162,7 @@ export class Renderer {
164162
}
165163

166164
// Render search highlights
167-
if (search && search.isActive()) {
165+
if (!readOnly && search && search.isActive()) {
168166
this.searchRenderer.updateSearchHighlights(search);
169167
}
170168

anycode-base/src/styles.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@
5454
outline: none;
5555
}
5656

57+
.anyeditor.readonly .buttons,
58+
.anyeditor.readonly .gutter {
59+
display: none;
60+
}
61+
62+
.anyeditor.readonly .code {
63+
cursor: text;
64+
}
65+
5766
.anyeditor .code div {
5867
line-height: 20px;
5968
}
@@ -289,4 +298,4 @@
289298

290299
.highlight.selected {
291300
background: rgba(210, 210, 210, 0.9);
292-
}
301+
}

anycode/App.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,8 @@
400400
border-radius: 1px;
401401
}
402402

403-
/* Right panel (editor) spacing for toolbar - only when terminal is closed */
404-
.app-container:not(.terminal-visible) .anyeditor .code div:last-child {
403+
/* Main editor spacing for toolbar - only when terminal is closed */
404+
.app-container:not(.terminal-visible) .editor-container .anyeditor .code div:last-child {
405405
padding-bottom: calc(var(--toolbar-total-space));
406406
}
407407

anycode/components/agent/AcpMessage.css

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,142 @@
3939
width: 100%;
4040
}
4141

42+
.acp-message-markdown {
43+
white-space: normal;
44+
line-height: 1.5;
45+
color: inherit;
46+
overflow-wrap: anywhere;
47+
}
48+
49+
.acp-message-markdown > :first-child {
50+
margin-top: 0;
51+
}
52+
53+
.acp-message-markdown > :last-child {
54+
margin-bottom: 0;
55+
}
56+
57+
.acp-message-markdown p,
58+
.acp-message-markdown ul,
59+
.acp-message-markdown ol,
60+
.acp-message-markdown blockquote,
61+
.acp-message-markdown pre {
62+
margin: 0 0 0.75em 0;
63+
}
64+
65+
.acp-message-markdown h1,
66+
.acp-message-markdown h2,
67+
.acp-message-markdown h3,
68+
.acp-message-markdown h4,
69+
.acp-message-markdown h5,
70+
.acp-message-markdown h6 {
71+
margin: 0.75em 0 0.35em 0;
72+
line-height: 1.2;
73+
}
74+
75+
.acp-message-markdown h1 {
76+
font-size: 1.25em;
77+
}
78+
79+
.acp-message-markdown h2 {
80+
font-size: 1.15em;
81+
}
82+
83+
.acp-message-markdown h3 {
84+
font-size: 1.05em;
85+
}
86+
87+
.acp-message-markdown ul,
88+
.acp-message-markdown ol {
89+
padding-left: 1.25em;
90+
}
91+
92+
.acp-message-markdown li + li {
93+
margin-top: 0.25em;
94+
}
95+
96+
.acp-message-markdown blockquote {
97+
padding-left: 0.85em;
98+
border-left: 3px solid var(--border-color, #333);
99+
color: var(--text-color-secondary, #888);
100+
}
101+
102+
.acp-message-markdown code {
103+
font-family: "JetBrains Mono", monospace;
104+
font-size: 0.92em;
105+
}
106+
107+
.acp-message-markdown .acp-inline-code {
108+
padding: 0.12em 0.35em;
109+
border-radius: 4px;
110+
background-color: var(--code-bg, #0d0d0d);
111+
color: var(--code-color, #d4d4d4);
112+
113+
}
114+
115+
.acp-message-markdown pre {
116+
overflow-x: auto;
117+
white-space: pre;
118+
}
119+
120+
.acp-message-markdown pre code {
121+
background: transparent;
122+
padding: 0;
123+
white-space: pre;
124+
display: block;
125+
}
126+
127+
.acp-message-markdown table {
128+
width: 100%;
129+
border-collapse: collapse;
130+
margin-bottom: 0.75em;
131+
}
132+
133+
.acp-message-markdown th,
134+
.acp-message-markdown td {
135+
border: 1px solid var(--border-color, #333);
136+
padding: 0.35em 0.5em;
137+
text-align: left;
138+
vertical-align: top;
139+
}
140+
141+
.acp-message-markdown a {
142+
color: var(--accent-color, #7ab7ff);
143+
text-decoration: underline;
144+
word-break: break-word;
145+
}
146+
147+
.acp-code {
148+
padding: 0.5em;
149+
margin: 0 0 0.75em 0;
150+
border-radius: 8px;
151+
max-height: 33vh;
152+
overflow: auto;
153+
border: 1px solid var(--border-color, #333);
154+
background: var(--assistant-message-bg, #2a2a2a);
155+
}
156+
157+
.acp-code-host {
158+
min-height: 0;
159+
}
160+
161+
.acp-code .anyeditor {
162+
height: auto !important;
163+
min-height: 0;
164+
background: transparent;
165+
}
166+
167+
.acp-code-block-fallback {
168+
margin: 0;
169+
padding: 0.75em;
170+
background-color: transparent;
171+
color: inherit;
172+
overflow: auto;
173+
white-space: pre;
174+
font-size: 12px;
175+
line-height: 1.5;
176+
}
177+
42178
.acp-message-content-with-actions {
43179
position: relative;
44180
}

0 commit comments

Comments
 (0)