turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

mention.go · 156 lines · 4.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 11h ago1package acp
2
3import (
4 "mime"
5 "path/filepath"
6 "strings"
7)
8
9// Mention is a file named in a prompt with "@", which the agent is given as
10// context beside the words.
11//
12// Name is what was typed after the @ — the path relative to the project, which
13// is what the picker offers and what the text keeps. Path is where the file
14// actually is, which is what the agent is told.
15//
16// acp.Mention{Name: "internal/golang/scanner.go", Path: "/src/p/internal/golang/scanner.go"}
17type Mention struct {
18 Name string
19 Path string
20}
21
22// URI returns the file: URI the protocol wants for the mention.
23func (m Mention) URI() string { return "file://" + filepath.ToSlash(m.Path) }
24
25// blocksFor turns one prompt into content blocks: a text block for each run of
26// words, and one resource block for each mention, in the order they were
27// typed. A prompt with no mentions is one text block, exactly as before.
28//
29// The mention's "@name" is taken **out** of the text and replaced by the
30// block, which is how Zed sends the same thing: the agent sees the file where
31// the name was, not a name it has to match against an attachment.
32//
33// embed says the agent accepts embedded context; read is how the file's text
34// is obtained when it does. A file that cannot be read is sent as a link
35// rather than dropped, so the agent is at least told which file was meant.
36func blocksFor(text string, mentions []Mention, embed bool, read func(string) (string, error)) []ContentBlock {
37 var blocks []ContentBlock
38 var words strings.Builder
39
40 flush := func() {
41 if words.Len() > 0 {
42 blocks = append(blocks, ContentBlock{Type: ContentText, Text: words.String()})
43 words.Reset()
44 }
45 }
46
47 runes := []rune(text)
48 for at := 0; at < len(runes); {
49 mention, width, ok := mentionAt(runes, at, mentions)
50 if !ok {
51 words.WriteRune(runes[at])
52 at++
53 continue
54 }
55 flush()
56 blocks = append(blocks, resourceBlock(mention, embed, read))
57 at += width
58 }
59 flush()
60 return blocks
61}
62
63// mentionAt reports whether a mention starts at position at, and how many
64// runes it takes. The longest name wins, so "@a/b.go" is not read as "@a"
65// followed by "/b.go"; and the name has to end the text or be followed by a
66// space, so "@main.gopher" does not name main.go.
67func mentionAt(runes []rune, at int, mentions []Mention) (Mention, int, bool) {
68 if runes[at] != mentionRune {
69 return Mention{}, 0, false
70 }
71
72 var best Mention
73 width := 0
74 for _, mention := range mentions {
75 name := []rune(string(mentionRune) + mention.Name)
76 if len(name) <= width || !hasRunes(runes[at:], name) {
77 continue
78 }
79 if end := at + len(name); end < len(runes) && !isSpace(runes[end]) {
80 continue
81 }
82 best, width = mention, len(name)
83 }
84 return best, width, width > 0
85}
86
87// hasRunes reports whether text begins with prefix.
88func hasRunes(text, prefix []rune) bool {
89 if len(prefix) > len(text) {
90 return false
91 }
92 for i, r := range prefix {
93 if text[i] != r {
94 return false
95 }
96 }
97 return true
98}
99
100// isSpace reports whether a rune ends a word.
101func isSpace(r rune) bool { return r == ' ' || r == '\t' || r == '\n' }
102
103// resourceBlock is the block a mention becomes: the file's text when the agent
104// takes it and the file can be read, a link to it otherwise.
105func resourceBlock(mention Mention, embed bool, read func(string) (string, error)) ContentBlock {
106 kind := mimeOf(mention.Path)
107 if embed && read != nil {
108 if text, err := read(mention.Path); err == nil {
109 return ContentBlock{
110 Type: ContentResource,
111 Resource: &EmbeddedResource{URI: mention.URI(), MimeType: kind, Text: text},
112 }
113 }
114 }
115 return ContentBlock{Type: ContentResourceLink, URI: mention.URI(), Name: mention.Name, MimeType: kind}
116}
117
118// knownTypes are the media types of the files this family of editors is for.
119//
120// They are listed rather than looked up because the system's table is not the
121// same on two machines — Linux and macOS disagree about ".go" — and an agent
122// that keys its behaviour on the type deserves the same answer from every copy
123// of the editor.
124var knownTypes = map[string]string{
125 ".go": "text/x-go",
126 ".rs": "text/x-rust",
127 ".py": "text/x-python",
128 ".mbt": "text/x-moonbit",
129 ".golo": "text/x-golo",
130 ".js": "text/javascript",
131 ".mjs": "text/javascript",
132 ".ts": "text/typescript",
133 ".md": "text/markdown",
134 ".toml": "application/toml",
135 ".yaml": "application/yaml",
136 ".yml": "application/yaml",
137 ".json": "application/json",
138 ".sh": "text/x-shellscript",
139 ".txt": "text/plain",
140}
141
142// mimeOf returns the media type to send for a file, and text/plain when nothing
143// better is known: every mention is a file somebody opened in a text editor.
144func mimeOf(path string) string {
145 extension := strings.ToLower(filepath.Ext(path))
146 if kind, ok := knownTypes[extension]; ok {
147 return kind
148 }
149 if kind := mime.TypeByExtension(extension); kind != "" {
150 if semicolon := strings.IndexByte(kind, ';'); semicolon >= 0 {
151 kind = strings.TrimSpace(kind[:semicolon])
152 }
153 return kind
154 }
155 return "text/plain"
156}