Skip to content

Commit aebfc38

Browse files
committed
feat(agent): support terminal selection quoting and refine quote attachment
- Add selection tracking to terminal with manual quote attachment in ACP input - Scope selection clearing to specific panel IDs to avoid tab/pane collisions - Fix markdown file link alignment and guard against malformed URI decoding - Expand unit test coverage for selectionQuoteStore and utils
1 parent 24f5a3a commit aebfc38

16 files changed

Lines changed: 2057 additions & 241 deletions

‎anycode-base/src/code.ts‎

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,24 @@ function getCachedQuery(
140140
}
141141

142142

143+
let treeSitterInitPromise: Promise<void> | null = null;
144+
function ensureTreeSitterInit(): Promise<void> {
145+
if (!treeSitterInitPromise) {
146+
treeSitterInitPromise = TreeSitterParser.init().catch((err) => {
147+
treeSitterInitPromise = null;
148+
throw err;
149+
});
150+
}
151+
return treeSitterInitPromise;
152+
}
153+
143154
async function loadLanguage(language: string): Promise<Language> {
144155
if (langsCache.has(language)) {
145156
return langsCache.get(language)!;
146157
}
147158

159+
await ensureTreeSitterInit();
160+
148161
let loadPromise = pendingLangsCache.get(language);
149162
if (!loadPromise) {
150163
const wasmPath = getWasmPath(`tree-sitter-${language}.wasm`);
@@ -159,6 +172,9 @@ async function loadLanguage(language: string): Promise<Language> {
159172
}
160173
langsCache.set(language, lang);
161174
return lang;
175+
} catch (err) {
176+
langsCache.delete(language);
177+
throw err;
162178
} finally {
163179
pendingLangsCache.delete(language);
164180
}
@@ -325,35 +341,34 @@ export class Code {
325341
return;
326342
}
327343

328-
let lang: Language;
329344
try {
330-
await TreeSitterParser.init();
331-
lang = await loadLanguage(this.language);
345+
await ensureTreeSitterInit();
346+
const lang = await loadLanguage(this.language);
347+
348+
this.parser = new TreeSitterParser();
349+
this.parser.setLanguage(lang);
350+
351+
this.tree = this.parser.parse(this.input) || undefined;
352+
353+
if (this.language) {
354+
let q = this.getQuery();
355+
if (q) this.query = getCachedQuery(lang, this.language, 'highlight', q);
356+
const foldsQ = this.getFoldsQuery();
357+
if (foldsQ) this.foldsQuery = getCachedQuery(lang, this.language, 'folds', foldsQ);
358+
const runnablesQ = this.getRunnablesQuery();
359+
if (runnablesQ) this.runnablesQuery = getCachedQuery(lang, this.language, 'runnables', runnablesQ);
360+
this.foldRangesInvalidated = true;
361+
if (this.query) await this.initInjections();
362+
}
332363
} catch (error) {
333364
console.error(`Failed to initialize tree-sitter language "${this.language}"`, error);
365+
if (this.language) {
366+
langsCache.delete(this.language);
367+
}
334368
this.useFastSyntax = true;
335369
this.clearSyntaxState();
336370
return;
337371
}
338-
339-
this.parser = new TreeSitterParser();
340-
this.parser.setLanguage(lang);
341-
342-
this.tree = this.parser.parse(this.input) || undefined;
343-
344-
if (this.language) {
345-
let q = this.getQuery();
346-
if (q) this.query = getCachedQuery(lang, this.language, 'highlight', q);
347-
const foldsQ = this.getFoldsQuery();
348-
if (foldsQ) this.foldsQuery = getCachedQuery(lang, this.language, 'folds', foldsQ);
349-
const runnablesQ = this.getRunnablesQuery();
350-
if (runnablesQ) this.runnablesQuery = getCachedQuery(lang, this.language, 'runnables', runnablesQ);
351-
this.foldRangesInvalidated = true;
352-
if (this.query) await this.initInjections();
353-
// let tq = this.getRunnablesQuery();
354-
// if (tq) this.runnablesQuery = lang.query(tq);
355-
// if (this.runnablesQuery || this.isExecutable()) this.updateRunnables();
356-
}
357372
}
358373

359374
public async initInjections() {

‎anycode-base/src/editor.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface EditorOptions {
5858
scrollbarWidth?: number;
5959
code?: Code;
6060
originalCode?: Code;
61+
buffer?: number;
6162
}
6263

6364
export interface EditorState {
@@ -177,7 +178,7 @@ export class AnycodeEditor {
177178

178179
this.settings = {
179180
lineHeight: 20,
180-
buffer: 25,
181+
buffer: options.buffer ?? 25,
181182
scrollbar: {
182183
style: options.scrollbarStyle,
183184
minSize: options.scrollbarMinSize,

‎anycode/components/agent/AcpIcons.tsx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,20 @@ export const AcpIcons = {
114114
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
115115
</svg>
116116
),
117+
Sparkles: ({ size = 14 }: { size?: number }) => (
118+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
119+
<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/>
120+
</svg>
121+
),
122+
Quote: ({ size = 14 }: { size?: number }) => (
123+
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
124+
<path d="M9.5 8.5C9.5 13 7.5 16.8 4 19C6.8 16.5 7.2 13.5 7 11.8A3.8 3.8 0 1 1 9.5 8.5Zm10 0C19.5 13 17.5 16.8 14 19C16.8 16.5 17.2 13.5 17 11.8A3.8 3.8 0 1 1 19.5 8.5Z" />
125+
</svg>
126+
),
127+
File: ({ size = 14 }: { size?: number }) => (
128+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
129+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
130+
<polyline points="14 2 14 8 20 8"/>
131+
</svg>
132+
),
117133
};

‎anycode/components/agent/AcpInput.css‎

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,196 @@
9999
padding: 0;
100100
}
101101

102+
/* Selection quote star button in controls row */
103+
.acp-input-quote-star-btn {
104+
display: inline-flex;
105+
align-items: center;
106+
justify-content: center;
107+
width: 24px;
108+
height: 24px;
109+
padding: 0;
110+
border: none;
111+
border-radius: 4px;
112+
background: transparent;
113+
color: #fabd2f;
114+
cursor: pointer;
115+
flex-shrink: 0;
116+
animation: acpStarPopIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
117+
}
118+
119+
.acp-input-quote-star-btn.acp-input-quote-star-exiting {
120+
animation: acpStarPopOut 0.18s cubic-bezier(0.4, 0, 0.2, 1) forwards;
121+
pointer-events: none;
122+
}
123+
124+
.acp-input-quote-star-btn:active {
125+
transform: scale(0.95);
126+
}
127+
128+
@keyframes acpStarPopIn {
129+
0% {
130+
opacity: 0;
131+
transform: scale(0.6);
132+
}
133+
100% {
134+
opacity: 1;
135+
transform: scale(1);
136+
}
137+
}
138+
139+
@keyframes acpStarPopOut {
140+
0% {
141+
opacity: 1;
142+
transform: scale(1);
143+
}
144+
100% {
145+
opacity: 0;
146+
transform: scale(0.6);
147+
}
148+
}
149+
150+
/* Interleaved text and quote blocks flow */
151+
.acp-input-blocks-flow {
152+
display: flex;
153+
flex-direction: column;
154+
gap: 8px;
155+
max-height: 400px;
156+
overflow-y: auto;
157+
}
158+
159+
.acp-input-block-text-row {
160+
display: flex;
161+
align-items: flex-end;
162+
gap: 6px;
163+
width: 100%;
164+
}
165+
166+
.acp-input-block-textarea {
167+
flex: 1;
168+
width: 100%;
169+
min-height: 24px;
170+
max-height: 160px;
171+
background: transparent;
172+
border: none;
173+
outline: none;
174+
padding: 4px 6px;
175+
color: var(--text-color, #fff);
176+
-webkit-text-fill-color: var(--text-color, #fff);
177+
font-family: var(--interface-font-family);
178+
font-size: var(--interface-font-size, 14px);
179+
font-weight: 400;
180+
line-height: 1.45;
181+
resize: none;
182+
box-sizing: border-box;
183+
}
184+
185+
.acp-input-block-textarea:focus {
186+
outline: none;
187+
}
188+
189+
.acp-input-block-textarea::placeholder {
190+
color: var(--text-color-secondary, rgba(255, 255, 255, 0.45));
191+
-webkit-text-fill-color: var(--text-color-secondary, rgba(255, 255, 255, 0.45));
192+
font-size: 0.93em;
193+
font-weight: 400;
194+
}
195+
196+
.acp-input-block-quote {
197+
animation: acpQuoteFadeIn 0.18s cubic-bezier(0.16, 1, 0.3, 1);
198+
}
199+
200+
.acp-input-quote-badge {
201+
display: flex;
202+
align-items: center;
203+
justify-content: space-between;
204+
gap: 8px;
205+
padding: 6px 10px;
206+
border-radius: 6px;
207+
border-left: 3px solid var(--theme-accent-background, #458588);
208+
background: var(--background-secondary, rgba(255, 255, 255, 0.05));
209+
border-top: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
210+
border-right: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
211+
border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
212+
transition: background-color 0.15s ease;
213+
animation: acpQuoteFadeIn 0.18s cubic-bezier(0.16, 1, 0.3, 1);
214+
}
215+
216+
.acp-input-quote-badge:hover {
217+
background: rgba(255, 255, 255, 0.075);
218+
}
219+
220+
.acp-input-quote-badge-left {
221+
display: flex;
222+
align-items: center;
223+
gap: 8px;
224+
min-width: 0;
225+
flex: 1;
226+
}
227+
228+
.acp-input-quote-badge-icon {
229+
display: inline-flex;
230+
align-items: center;
231+
color: var(--theme-accent-background, #458588);
232+
flex-shrink: 0;
233+
}
234+
235+
.acp-input-quote-badge-info {
236+
display: flex;
237+
flex-direction: column;
238+
gap: 2px;
239+
min-width: 0;
240+
flex: 1;
241+
}
242+
243+
.acp-input-quote-badge-label {
244+
font-size: 0.78em;
245+
font-weight: 600;
246+
color: var(--theme-accent-background, #83a598);
247+
white-space: nowrap;
248+
overflow: hidden;
249+
text-overflow: ellipsis;
250+
}
251+
252+
.acp-input-quote-badge-text {
253+
font-size: 0.8em;
254+
font-family: var(--font-family, monospace);
255+
color: var(--text-color-secondary, rgba(255, 255, 255, 0.8));
256+
white-space: nowrap;
257+
overflow: hidden;
258+
text-overflow: ellipsis;
259+
}
260+
261+
.acp-input-quote-badge-remove {
262+
background: transparent;
263+
border: none;
264+
color: var(--text-color-secondary, rgba(255, 255, 255, 0.6));
265+
cursor: pointer;
266+
padding: 3px;
267+
border-radius: 4px;
268+
display: flex;
269+
align-items: center;
270+
justify-content: center;
271+
line-height: 1;
272+
transition: color 0.15s, background-color 0.15s;
273+
flex-shrink: 0;
274+
}
275+
276+
.acp-input-quote-badge-remove:hover {
277+
color: var(--foreground-color, #ffffff);
278+
background: rgba(255, 255, 255, 0.1);
279+
}
280+
281+
@keyframes acpQuoteFadeIn {
282+
from {
283+
opacity: 0;
284+
transform: translateY(-4px);
285+
}
286+
to {
287+
opacity: 1;
288+
transform: translateY(0);
289+
}
290+
}
291+
102292
.acp-input-preview-text {
103293
flex: 1;
104294
font-size: 0.86em;

0 commit comments

Comments
 (0)