@chatpanel/events 0.101.0 → 0.103.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -348
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/note-actions.js
CHANGED
|
@@ -1,252 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// A note has three ways to ask a model to write: a NAMED ACTION on the selection or the whole
|
|
4
|
-
// note ("Continue writing", "Summarize", "Turn into tasks", "Improve writing"), an
|
|
5
|
-
// `@command instruction` typed on a line ("@table the G7 by population"), and a `/` palette
|
|
6
|
-
// that offers the actions at the caret. Every client has all three, and each has two halves
|
|
7
|
-
// that must agree: the PROMPT the model receives and the FRAME the output is written into —
|
|
8
|
-
// head, output, tail. Ask for a summary and land it at the caret, or ask for a checklist and
|
|
9
|
-
// replace the whole note with it, and the feature is wrong in a way no prompt fixes.
|
|
10
|
-
//
|
|
11
|
-
// These lived inside the extension's `notes.js` — five thousand lines, DOM-bound — and the
|
|
12
|
-
// desktop was about to type them a second time. Nothing here needs a window: the specs are
|
|
13
|
-
// data, the framing is body + selection in and three strings out, the triggers are text +
|
|
14
|
-
// caret in and a range out. So they are here, and the clients render the menu around them.
|
|
15
|
-
//
|
|
16
|
-
// THE CLIENT STILL OWNS: the model call (through its own streaming path), the editor write,
|
|
17
|
-
// attribution and versioning, and any tools it arms. `NOTE_COMMANDS[].tools` says a command
|
|
18
|
-
// WANTS live data; whether a target can fetch it is the client's to know.
|
|
1
|
+
const h=Object.freeze({continue:Object.freeze({label:"Continue writing",hint:"draft inline from here",system:"You are a writing assistant continuing the user's note. Match their voice, tone and markdown style, and continue naturally from where the text stops. Output ONLY the continuation — no preamble, no repeating prior text.",maxTokens:700,needsSelection:!1}),summarize:Object.freeze({label:"Summarize",hint:"a tight summary",system:"Summarize the text concisely in GitHub-flavored markdown — a few tight bullets or a short paragraph. Output ONLY the summary.",maxTokens:500,needsSelection:!1}),tasks:Object.freeze({label:"Turn into tasks",hint:"selection → checklist",system:'Convert the text into a GitHub-flavored markdown checklist: one actionable item per line as "- [ ] item". Output ONLY the checklist.',maxTokens:700,needsSelection:!0}),improve:Object.freeze({label:"Improve writing",hint:"rewrite clearer",system:"Rewrite the text to be clearer and more concise while preserving its meaning, tone and markdown formatting. Output ONLY the rewritten text.",maxTokens:1e3,needsSelection:!1})}),w=.5,p=Object.freeze(["continue","summarize","tasks","improve"]),x=e=>h[e]?.label||String(e||"");function N(e,n,r=0,s=r){if(!h[e])return{error:"unknown"};const t=String(n??""),a=f(r,0,t.length),l=f(s,a,t.length),i=t.slice(a,l);let c,u,m;if(e==="continue")c=t,u=t+(t&&!/\s$/.test(t)?`
|
|
19
2
|
|
|
20
|
-
|
|
21
|
-
* The named actions, keyed the way the menus and the `/` palette refer to them.
|
|
22
|
-
*
|
|
23
|
-
* `system` is the whole instruction; the user turn is the target text alone. Every prompt
|
|
24
|
-
* ends in "Output ONLY …" because the answer is written INTO the note — a "Sure! Here is
|
|
25
|
-
* your summary:" preamble would land in the document verbatim.
|
|
26
|
-
*/
|
|
27
|
-
export const NOTE_ACTIONS = Object.freeze({
|
|
28
|
-
continue: Object.freeze({
|
|
29
|
-
label: 'Continue writing',
|
|
30
|
-
hint: 'draft inline from here',
|
|
31
|
-
system: "You are a writing assistant continuing the user's note. Match their voice, tone and markdown style, and continue naturally from where the text stops. Output ONLY the continuation — no preamble, no repeating prior text.",
|
|
32
|
-
maxTokens: 700,
|
|
33
|
-
needsSelection: false,
|
|
34
|
-
}),
|
|
35
|
-
summarize: Object.freeze({
|
|
36
|
-
label: 'Summarize',
|
|
37
|
-
hint: 'a tight summary',
|
|
38
|
-
system: 'Summarize the text concisely in GitHub-flavored markdown — a few tight bullets or a short paragraph. Output ONLY the summary.',
|
|
39
|
-
maxTokens: 500,
|
|
40
|
-
needsSelection: false,
|
|
41
|
-
}),
|
|
42
|
-
tasks: Object.freeze({
|
|
43
|
-
label: 'Turn into tasks',
|
|
44
|
-
hint: 'selection → checklist',
|
|
45
|
-
system: 'Convert the text into a GitHub-flavored markdown checklist: one actionable item per line as "- [ ] item". Output ONLY the checklist.',
|
|
46
|
-
maxTokens: 700,
|
|
47
|
-
needsSelection: true,
|
|
48
|
-
}),
|
|
49
|
-
improve: Object.freeze({
|
|
50
|
-
label: 'Improve writing',
|
|
51
|
-
hint: 'rewrite clearer',
|
|
52
|
-
system: 'Rewrite the text to be clearer and more concise while preserving its meaning, tone and markdown formatting. Output ONLY the rewritten text.',
|
|
53
|
-
maxTokens: 1000,
|
|
54
|
-
needsSelection: false,
|
|
55
|
-
}),
|
|
56
|
-
});
|
|
3
|
+
`:""),m="";else if(e==="summarize")c=i||t,u=`${t.replace(/\s+$/,"")}
|
|
57
4
|
|
|
58
|
-
|
|
59
|
-
export const NOTE_ACTION_TEMPERATURE = 0.5;
|
|
5
|
+
## Summary
|
|
60
6
|
|
|
61
|
-
|
|
62
|
-
|
|
7
|
+
`,m="";else if(e==="tasks"){if(!i.trim())return{error:"select"};c=i,u=t.slice(0,a),m=t.slice(l)}else c=i||t,u=i?t.slice(0,a):"",m=i?t.slice(l):"";return c.trim()?{target:c,head:u,tail:m}:{error:"empty"}}const y=Object.freeze({select:"Select some text first",empty:"Nothing to work with yet",unknown:"Unknown action"}),d=Object.freeze([Object.freeze({cmd:"insert",label:"Insert",hint:"generate or fetch, then insert",tools:!0,system:"You insert content into the user's note. Follow the instruction. If it needs current, live, or web data, USE the web_search tool to fetch it — never guess or use stale knowledge. Output ONLY the content to insert as clean GitHub-flavored markdown — no preamble, no closing remarks."}),Object.freeze({cmd:"table",label:"Table",hint:"produce a markdown table",tools:!0,system:"Produce the requested data as a GitHub-flavored markdown table. If it needs live/current data, USE the web_search tool. Output ONLY the table."}),Object.freeze({cmd:"list",label:"List",hint:"produce a bullet/task list",tools:!0,system:"Produce the requested content as a GitHub-flavored markdown list (use - [ ] for actionable tasks). Use web_search for live data. Output ONLY the list."}),Object.freeze({cmd:"summarize",label:"Summarize",hint:"summarize a topic",tools:!1,system:"Summarize the requested topic concisely in markdown. Output ONLY the summary."}),Object.freeze({cmd:"translate",label:"Translate",hint:"translate to a language",tools:!1,system:"Translate the requested text to the requested language, preserving markdown. Output ONLY the translation."})]),T=.4,k=1800,b=new RegExp(`@(${d.map(e=>e.cmd).join("|")})\\b[ \\t]*(.*)$`,"i");function S(e,n){const r=String(e??""),s=f(n,0,r.length),o=r.lastIndexOf(`
|
|
8
|
+
`,s-1)+1;let t=r.indexOf(`
|
|
9
|
+
`,s);t<0&&(t=r.length);const l=r.slice(o,t).match(b);if(!l)return null;const i=d.find(u=>u.cmd===l[1].toLowerCase()),c=(l[2]||"").trim();return!i||!c?null:{spec:i,instruction:c,start:o+l.index,end:t}}function E(e,n,r,{hasSelection:s=!1}={}){if(s)return null;const o=String(e??""),t=f(n,0,o.length),a=o.slice(o.lastIndexOf(`
|
|
10
|
+
`,t-1)+1,t),l=g(String(r||"/")),i=a.match(new RegExp(`(?:^|\\s)${l}(\\w*)$`));return i?{word:i[1],start:t-i[1].length,end:t}:null}function z(e,n){const r=String(n||"").trim().toLowerCase(),s=Array.isArray(e)?e:[];return r?s.filter(o=>String(o.key||"").toLowerCase().startsWith(r)||String(o.label||"").toLowerCase().includes(r)):s}function _(e=p){return e.filter(n=>h[n]).map(n=>({key:n,label:h[n].label,hint:h[n].hint}))}const O=4e3;function v(e,n,{title:r="",max:s=O}={}){const o=`${e??""}
|
|
11
|
+
${n??""}`.replace(/\n{3,}/g,`
|
|
63
12
|
|
|
64
|
-
|
|
65
|
-
|
|
13
|
+
`).trim();if(!o)return"";const t=o.length>s?`${o.slice(0,s)}
|
|
14
|
+
…(note truncated)`:o,a=String(r||"").trim();return`The note I'm editing${a?` (title: "${a}")`:""} is below. Resolve any reference in my instruction — "this meeting", "above", a date, a name, a [[wikilink]] or a URL (a meeting link's #id identifies that exact meeting) — against THIS note, not a guess. If a tool lets you fetch something referenced here by id, use that id.
|
|
66
15
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* Returns `{ target, head, tail }`: the text the model is given, and what sits before and
|
|
71
|
-
* after its answer in the finished note. Or `{ error }` — one of the two ways an action
|
|
72
|
-
* cannot run — so a client shows the reason rather than sending an empty prompt and reading
|
|
73
|
-
* the model's confusion back into the document.
|
|
74
|
-
*
|
|
75
|
-
* continue — the whole note in, the answer appended after a blank line;
|
|
76
|
-
* summarize — the selection (or the note) in, a `## Summary` section appended at the end;
|
|
77
|
-
* tasks — the selection in, the checklist REPLACES it; needs a selection, because a
|
|
78
|
-
* checklist of the entire note replacing the entire note is never what was meant;
|
|
79
|
-
* improve — the selection in and replaced; with no selection the whole note is rewritten.
|
|
80
|
-
*
|
|
81
|
-
* `error` is 'select' (this action needs a selection) or 'empty' (nothing to work with).
|
|
82
|
-
*/
|
|
83
|
-
export function frameNoteAction(kind, body, selStart = 0, selEnd = selStart) {
|
|
84
|
-
const spec = NOTE_ACTIONS[kind];
|
|
85
|
-
if (!spec) return { error: 'unknown' };
|
|
86
|
-
const text = String(body ?? '');
|
|
87
|
-
const s0 = clamp(selStart, 0, text.length);
|
|
88
|
-
const s1 = clamp(selEnd, s0, text.length);
|
|
89
|
-
const sel = text.slice(s0, s1);
|
|
16
|
+
"""
|
|
17
|
+
${t}
|
|
18
|
+
"""`}function A(e,n){const r=String(e??"");return n?`${n}
|
|
90
19
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let tail;
|
|
94
|
-
if (kind === 'continue') {
|
|
95
|
-
target = text;
|
|
96
|
-
head = text + (text && !/\s$/.test(text) ? '\n\n' : '');
|
|
97
|
-
tail = '';
|
|
98
|
-
} else if (kind === 'summarize') {
|
|
99
|
-
target = sel || text;
|
|
100
|
-
head = `${text.replace(/\s+$/, '')}\n\n## Summary\n\n`;
|
|
101
|
-
tail = '';
|
|
102
|
-
} else if (kind === 'tasks') {
|
|
103
|
-
if (!sel.trim()) return { error: 'select' };
|
|
104
|
-
target = sel;
|
|
105
|
-
head = text.slice(0, s0);
|
|
106
|
-
tail = text.slice(s1);
|
|
107
|
-
} else {
|
|
108
|
-
target = sel || text;
|
|
109
|
-
head = sel ? text.slice(0, s0) : '';
|
|
110
|
-
tail = sel ? text.slice(s1) : '';
|
|
111
|
-
}
|
|
112
|
-
if (!target.trim()) return { error: 'empty' };
|
|
113
|
-
return { target, head, tail };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** What a client should tell the user for each `frameNoteAction` error. */
|
|
117
|
-
export const NOTE_ACTION_ERRORS = Object.freeze({
|
|
118
|
-
select: 'Select some text first',
|
|
119
|
-
empty: 'Nothing to work with yet',
|
|
120
|
-
unknown: 'Unknown action',
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* The `@command instruction` grammar — a line the user writes, and the model replaces.
|
|
125
|
-
*
|
|
126
|
-
* `tools: true` means the command is about live data and wants a web search armed. A target
|
|
127
|
-
* that cannot search (an HTTP model with no tools) still answers; the prompt tells it not to
|
|
128
|
-
* guess, and a client that knows its target cannot fetch may say so.
|
|
129
|
-
*/
|
|
130
|
-
export const NOTE_COMMANDS = Object.freeze([
|
|
131
|
-
Object.freeze({ cmd: 'insert', label: 'Insert', hint: 'generate or fetch, then insert', tools: true, system: 'You insert content into the user\'s note. Follow the instruction. If it needs current, live, or web data, USE the web_search tool to fetch it — never guess or use stale knowledge. Output ONLY the content to insert as clean GitHub-flavored markdown — no preamble, no closing remarks.' }),
|
|
132
|
-
Object.freeze({ cmd: 'table', label: 'Table', hint: 'produce a markdown table', tools: true, system: 'Produce the requested data as a GitHub-flavored markdown table. If it needs live/current data, USE the web_search tool. Output ONLY the table.' }),
|
|
133
|
-
Object.freeze({ cmd: 'list', label: 'List', hint: 'produce a bullet/task list', tools: true, system: 'Produce the requested content as a GitHub-flavored markdown list (use - [ ] for actionable tasks). Use web_search for live data. Output ONLY the list.' }),
|
|
134
|
-
Object.freeze({ cmd: 'summarize', label: 'Summarize', hint: 'summarize a topic', tools: false, system: 'Summarize the requested topic concisely in markdown. Output ONLY the summary.' }),
|
|
135
|
-
Object.freeze({ cmd: 'translate', label: 'Translate', hint: 'translate to a language', tools: false, system: 'Translate the requested text to the requested language, preserving markdown. Output ONLY the translation.' }),
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
/** The sampling a command runs at — tighter than an action, because it is following orders. */
|
|
139
|
-
export const NOTE_COMMAND_TEMPERATURE = 0.4;
|
|
140
|
-
export const NOTE_COMMAND_MAX_TOKENS = 1800;
|
|
141
|
-
|
|
142
|
-
// Matches `@command …` ANYWHERE on the line, not only at its start, so it works mid-sentence.
|
|
143
|
-
const NOTE_CMD_RE = new RegExp(`@(${NOTE_COMMANDS.map((c) => c.cmd).join('|')})\\b[ \\t]*(.*)$`, 'i');
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* The runnable `@command instruction` on the line the caret is on, or null.
|
|
147
|
-
*
|
|
148
|
-
* Returns `{ spec, instruction, start, end }` where [start, end) is the span to replace —
|
|
149
|
-
* from the `@` to the end of the line, so text BEFORE the command on that line survives.
|
|
150
|
-
* A command with no instruction is not runnable: "@table" alone is the user still typing.
|
|
151
|
-
*/
|
|
152
|
-
export function commandLineAt(text, pos) {
|
|
153
|
-
const v = String(text ?? '');
|
|
154
|
-
const p = clamp(pos, 0, v.length);
|
|
155
|
-
const lineStart = v.lastIndexOf('\n', p - 1) + 1;
|
|
156
|
-
let end = v.indexOf('\n', p);
|
|
157
|
-
if (end < 0) end = v.length;
|
|
158
|
-
const line = v.slice(lineStart, end);
|
|
159
|
-
const m = line.match(NOTE_CMD_RE);
|
|
160
|
-
if (!m) return null;
|
|
161
|
-
const spec = NOTE_COMMANDS.find((c) => c.cmd === m[1].toLowerCase());
|
|
162
|
-
const instruction = (m[2] || '').trim();
|
|
163
|
-
if (!spec || !instruction) return null;
|
|
164
|
-
return { spec, instruction, start: lineStart + m.index, end };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* The `<trigger>word` being typed at the caret — `/` for the palette, `@` for commands and
|
|
169
|
-
* mentions — or null. The trigger must open a token: at the start of the line or after
|
|
170
|
-
* whitespace, so `a/b` and `alex@example.com` never pop a menu. A `#` trigger with a space
|
|
171
|
-
* after it is a markdown heading, which `\w*` already refuses.
|
|
172
|
-
*
|
|
173
|
-
* Returns `{ word, start, end }` where [start, end) is the WORD (not the trigger), which is
|
|
174
|
-
* what a client replaces when an item is picked. `hasSelection` suppresses it: a palette over
|
|
175
|
-
* a selection is a different gesture from a palette at a caret.
|
|
176
|
-
*/
|
|
177
|
-
export function triggerQueryAt(text, pos, trigger, { hasSelection = false } = {}) {
|
|
178
|
-
if (hasSelection) return null;
|
|
179
|
-
const v = String(text ?? '');
|
|
180
|
-
const p = clamp(pos, 0, v.length);
|
|
181
|
-
const line = v.slice(v.lastIndexOf('\n', p - 1) + 1, p);
|
|
182
|
-
const t = escapeRe(String(trigger || '/'));
|
|
183
|
-
const m = line.match(new RegExp(`(?:^|\\s)${t}(\\w*)$`));
|
|
184
|
-
if (!m) return null;
|
|
185
|
-
return { word: m[1], start: p - m[1].length, end: p };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Filter the `/` palette by what has been typed after the slash: a prefix of the key or a
|
|
190
|
-
* substring of the label, case-insensitively. An empty word keeps everything, in order.
|
|
191
|
-
*/
|
|
192
|
-
export function filterNoteActions(items, word) {
|
|
193
|
-
const w = String(word || '').trim().toLowerCase();
|
|
194
|
-
const list = Array.isArray(items) ? items : [];
|
|
195
|
-
if (!w) return list;
|
|
196
|
-
return list.filter((it) => (
|
|
197
|
-
String(it.key || '').toLowerCase().startsWith(w)
|
|
198
|
-
|| String(it.label || '').toLowerCase().includes(w)
|
|
199
|
-
));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* The palette items for the actions this build lists, in order — `{ key, label, hint }`
|
|
204
|
-
* with no runner, so a client maps keys to its own handlers and drops any it cannot run.
|
|
205
|
-
*/
|
|
206
|
-
export function noteActionItems(order = NOTE_ACTION_ORDER) {
|
|
207
|
-
return order
|
|
208
|
-
.filter((k) => NOTE_ACTIONS[k])
|
|
209
|
-
.map((k) => ({ key: k, label: NOTE_ACTIONS[k].label, hint: NOTE_ACTIONS[k].hint }));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** How much of the note rides along with a command — bounds the tokens, not the meaning. */
|
|
213
|
-
export const NOTE_CONTEXT_MAX_CHARS = 4000;
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* The note itself (minus the command line) as grounding for an `@command` or `@[Agent]` task.
|
|
217
|
-
*
|
|
218
|
-
* "Summarize above", "action items for this meeting", "translate that" — the instruction
|
|
219
|
-
* points at the document it sits in, and a model given only the instruction answers that
|
|
220
|
-
* there is nothing to summarize. So the note travels with the task: everything before the
|
|
221
|
-
* line (`head`) and after it (`tail`), the title when there is one, and a preface telling
|
|
222
|
-
* the model to resolve references — a date, a [[wikilink]], a meeting link's #id — against
|
|
223
|
-
* THIS text rather than free-searching history for a guess.
|
|
224
|
-
*
|
|
225
|
-
* Capped at `max` characters so a long note cannot blow the budget; the client's redaction
|
|
226
|
-
* harness sees it like any other user turn. An empty note yields '' — there is nothing to
|
|
227
|
-
* ground in, and a frame around nothing would only cost tokens.
|
|
228
|
-
*/
|
|
229
|
-
export function noteCommandContext(head, tail, { title = '', max = NOTE_CONTEXT_MAX_CHARS } = {}) {
|
|
230
|
-
const body = `${head ?? ''}\n${tail ?? ''}`.replace(/\n{3,}/g, '\n\n').trim();
|
|
231
|
-
if (!body) return '';
|
|
232
|
-
const clipped = body.length > max ? `${body.slice(0, max)}\n…(note truncated)` : body;
|
|
233
|
-
const t = String(title || '').trim();
|
|
234
|
-
return `The note I'm editing${t ? ` (title: "${t}")` : ''} is below. Resolve any reference in my instruction — "this meeting", "above", a date, a name, a [[wikilink]] or a URL (a meeting link's #id identifies that exact meeting) — against THIS note, not a guess. If a tool lets you fetch something referenced here by id, use that id.\n\n"""\n${clipped}\n"""`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* The user turn for a command: the grounding (when there is any) and then the instruction,
|
|
239
|
-
* separated so the model cannot mistake the note for the ask. One function, because the
|
|
240
|
-
* separator is part of the contract — a client that joined them with a different rule
|
|
241
|
-
* would get a different answer from the same note.
|
|
242
|
-
*/
|
|
243
|
-
export function groundedInstruction(instruction, context) {
|
|
244
|
-
const ask = String(instruction ?? '');
|
|
245
|
-
return context ? `${context}\n\n---\nInstruction: ${ask}` : ask;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function clamp(n, lo, hi) {
|
|
249
|
-
const x = Number.isFinite(n) ? n : lo;
|
|
250
|
-
return Math.max(lo, Math.min(hi, x));
|
|
251
|
-
}
|
|
252
|
-
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
20
|
+
---
|
|
21
|
+
Instruction: ${r}`:r}function f(e,n,r){const s=Number.isFinite(e)?e:n;return Math.max(n,Math.min(r,s))}const g=e=>e.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&");export{h as NOTE_ACTIONS,y as NOTE_ACTION_ERRORS,p as NOTE_ACTION_ORDER,w as NOTE_ACTION_TEMPERATURE,d as NOTE_COMMANDS,k as NOTE_COMMAND_MAX_TOKENS,T as NOTE_COMMAND_TEMPERATURE,O as NOTE_CONTEXT_MAX_CHARS,S as commandLineAt,z as filterNoteActions,N as frameNoteAction,A as groundedInstruction,_ as noteActionItems,x as noteActionLabel,v as noteCommandContext,E as triggerQueryAt};
|
package/note-graph.js
CHANGED
|
@@ -1,158 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// A note graph drawn from `[[wikilinks]]` alone is mostly dust — people link deliberately and
|
|
4
|
-
// rarely, so the picture is a handful of pairs and a hundred lonely dots, which tells you
|
|
5
|
-
// nothing you did not already know. The connective tissue is what notes are ABOUT: a topic
|
|
6
|
-
// shared by two or more notes becomes a node of its own, and the notes hang off it.
|
|
7
|
-
//
|
|
8
|
-
// That is the whole idea, and it is pure: an index of `{ id, title, topics, tags, links }` in,
|
|
9
|
-
// nodes and links out. No DOM, no layout, no physics — a client draws it with SVG, canvas, or
|
|
10
|
-
// a native view, and all of them get the same graph. The extension's `graph-view.js` keeps the
|
|
11
|
-
// drawing; this is the model underneath it.
|
|
12
|
-
//
|
|
13
|
-
// TOPICS ON A SINGLE NOTE ARE DROPPED. A topic that touches one note connects nothing, and
|
|
14
|
-
// drawing it adds a labelled node with exactly one edge — clutter that makes the real clusters
|
|
15
|
-
// harder to see. The threshold is the difference between a graph and a hairball.
|
|
16
|
-
|
|
17
|
-
/** How many nodes are worth drawing before a view should start trimming. */
|
|
18
|
-
export const MAX_GRAPH_NODES = 140;
|
|
19
|
-
|
|
20
|
-
const lc = (s) => String(s || '').trim().toLowerCase();
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Build the note graph.
|
|
24
|
-
*
|
|
25
|
-
* @param {Array} index `{ id, title, topics?, tags?, links? }` — the lightweight list, not
|
|
26
|
-
* the bodies. `links` are wikilink TITLES, resolved here against the
|
|
27
|
-
* index's own titles, so a link to a page that does not exist yet draws
|
|
28
|
-
* nothing rather than a dangling node.
|
|
29
|
-
* @param {object} [opts]
|
|
30
|
-
* @param {boolean} [opts.topics=true] include topic hubs
|
|
31
|
-
* @param {number} [opts.minShared=2] how many notes a topic needs before it earns a node
|
|
32
|
-
* @returns {{nodes: Array, links: Array}}
|
|
33
|
-
*/
|
|
34
|
-
export function buildNoteGraph(index, { topics = true, minShared = 2 } = {}) {
|
|
35
|
-
const list = Array.isArray(index) ? index.filter(Boolean) : [];
|
|
36
|
-
const nodes = list.map((n) => ({ id: n.id, label: n.title || 'Untitled note', type: 'note' }));
|
|
37
|
-
const links = [];
|
|
38
|
-
const seen = new Set();
|
|
39
|
-
const addEdge = (s, t, kind) => {
|
|
40
|
-
if (!s || !t || s === t) return;
|
|
41
|
-
const key = s < t ? `${s}|${t}` : `${t}|${s}`;
|
|
42
|
-
if (seen.has(key)) return;
|
|
43
|
-
seen.add(key);
|
|
44
|
-
links.push({ s, t, kind });
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Explicit wikilinks first — a deliberate connection outranks an inferred one, and adding
|
|
48
|
-
// them first means the dedupe above can never drop one in favour of a topic edge.
|
|
49
|
-
const byTitle = new Map(list.map((n) => [lc(n.title), n.id]));
|
|
50
|
-
for (const n of list) {
|
|
51
|
-
for (const name of n.links || []) {
|
|
52
|
-
const target = byTitle.get(lc(name));
|
|
53
|
-
if (target) addEdge(n.id, target, 'link');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (topics) {
|
|
58
|
-
const hubs = new Map(); // lowercased topic → { label, ids[] }
|
|
59
|
-
for (const n of list) {
|
|
60
|
-
// Tags are the user's own topics and count the same: a corpus where somebody tags by
|
|
61
|
-
// hand should not draw a sparser graph than one where a model extracted the topics.
|
|
62
|
-
for (const raw of [...(n.topics || []), ...(n.tags || [])]) {
|
|
63
|
-
const label = String(raw || '').trim();
|
|
64
|
-
if (!label) continue;
|
|
65
|
-
const key = lc(label);
|
|
66
|
-
const e = hubs.get(key) || { label, ids: [] };
|
|
67
|
-
if (!e.ids.includes(n.id)) e.ids.push(n.id);
|
|
68
|
-
hubs.set(key, e);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
for (const [key, { label, ids }] of hubs) {
|
|
72
|
-
if (ids.length < minShared) continue;
|
|
73
|
-
const tid = `topic:${key}`;
|
|
74
|
-
nodes.push({ id: tid, label, type: 'topic' });
|
|
75
|
-
for (const id of ids) addEdge(id, tid, 'topic');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return { nodes, links };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The neighbourhood around one node — BFS out `hops`, keeping only links whose endpoints both
|
|
84
|
-
* survive.
|
|
85
|
-
*
|
|
86
|
-
* A whole-corpus graph answers "what does my thinking look like"; this answers "what is near
|
|
87
|
-
* THIS note", which is the question someone reading a note actually has. Topic hubs count as
|
|
88
|
-
* a hop, so `hops: 2` reaches the sibling notes that share a topic with this one — which is
|
|
89
|
-
* the entire point of having hubs at all.
|
|
90
|
-
*/
|
|
91
|
-
export function egoGraph(full, id, hops = 2) {
|
|
92
|
-
const nodes = full?.nodes || [];
|
|
93
|
-
const links = full?.links || [];
|
|
94
|
-
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
95
|
-
if (!byId.has(id)) return { nodes: [], links: [] };
|
|
96
|
-
const adj = new Map();
|
|
97
|
-
for (const l of links) {
|
|
98
|
-
if (!adj.has(l.s)) adj.set(l.s, new Set());
|
|
99
|
-
if (!adj.has(l.t)) adj.set(l.t, new Set());
|
|
100
|
-
adj.get(l.s).add(l.t);
|
|
101
|
-
adj.get(l.t).add(l.s);
|
|
102
|
-
}
|
|
103
|
-
const inSet = new Set([id]);
|
|
104
|
-
let frontier = new Set([id]);
|
|
105
|
-
for (let h = 0; h < hops; h++) {
|
|
106
|
-
const next = new Set();
|
|
107
|
-
for (const cur of frontier) {
|
|
108
|
-
for (const nb of adj.get(cur) || []) if (!inSet.has(nb)) { inSet.add(nb); next.add(nb); }
|
|
109
|
-
}
|
|
110
|
-
frontier = next;
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
nodes: [...inSet].map((nid) => {
|
|
114
|
-
const m = byId.get(nid);
|
|
115
|
-
return { id: nid, label: m?.label || 'Untitled note', type: m?.type || 'note', focus: nid === id };
|
|
116
|
-
}),
|
|
117
|
-
links: links.filter((l) => inSet.has(l.s) && inSet.has(l.t)),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Trim a graph to the most connected `max` nodes, keeping `keep` whatever its degree.
|
|
123
|
-
*
|
|
124
|
-
* A corpus of two thousand notes draws as a grey disc: every renderer slows to a crawl and
|
|
125
|
-
* nothing is legible anyway. Degree is the right thing to cut on — a node nothing connects to
|
|
126
|
-
* carries no information in a picture whose entire subject is connection — and the focused
|
|
127
|
-
* node is exempt, because a view of "around this note" that dropped the note would be absurd.
|
|
128
|
-
*/
|
|
129
|
-
export function trimGraph(graph, max = MAX_GRAPH_NODES, keep = '') {
|
|
130
|
-
const nodes = graph?.nodes || [];
|
|
131
|
-
const links = graph?.links || [];
|
|
132
|
-
if (nodes.length <= max) return { nodes, links };
|
|
133
|
-
const degree = new Map();
|
|
134
|
-
for (const l of links) {
|
|
135
|
-
degree.set(l.s, (degree.get(l.s) || 0) + 1);
|
|
136
|
-
degree.set(l.t, (degree.get(l.t) || 0) + 1);
|
|
137
|
-
}
|
|
138
|
-
const ranked = [...nodes].sort((a, b) => {
|
|
139
|
-
if (a.id === keep) return -1;
|
|
140
|
-
if (b.id === keep) return 1;
|
|
141
|
-
return (degree.get(b.id) || 0) - (degree.get(a.id) || 0);
|
|
142
|
-
}).slice(0, max);
|
|
143
|
-
const inSet = new Set(ranked.map((n) => n.id));
|
|
144
|
-
return { nodes: ranked, links: links.filter((l) => inSet.has(l.s) && inSet.has(l.t)) };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* How many notes, links and topic hubs — the one-line summary a pane shows above the picture,
|
|
149
|
-
* so a graph that looks empty says whether it IS empty or merely unconnected.
|
|
150
|
-
*/
|
|
151
|
-
export function graphStats(graph) {
|
|
152
|
-
const nodes = graph?.nodes || [];
|
|
153
|
-
return {
|
|
154
|
-
notes: nodes.filter((n) => n.type !== 'topic').length,
|
|
155
|
-
topics: nodes.filter((n) => n.type === 'topic').length,
|
|
156
|
-
links: (graph?.links || []).length,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
1
|
+
const k=140,h=o=>String(o||"").trim().toLowerCase();function w(o,{topics:r=!0,minShared:f=2}={}){const l=Array.isArray(o)?o.filter(Boolean):[],p=l.map(t=>({id:t.id,label:t.title||"Untitled note",type:"note"})),c=[],s=new Set,d=(t,e,a)=>{if(!t||!e||t===e)return;const i=t<e?`${t}|${e}`:`${e}|${t}`;s.has(i)||(s.add(i),c.push({s:t,t:e,kind:a}))},n=new Map(l.map(t=>[h(t.title),t.id]));for(const t of l)for(const e of t.links||[]){const a=n.get(h(e));a&&d(t.id,a,"link")}if(r){const t=new Map;for(const e of l)for(const a of[...e.topics||[],...e.tags||[]]){const i=String(a||"").trim();if(!i)continue;const u=h(i),g=t.get(u)||{label:i,ids:[]};g.ids.includes(e.id)||g.ids.push(e.id),t.set(u,g)}for(const[e,{label:a,ids:i}]of t){if(i.length<f)continue;const u=`topic:${e}`;p.push({id:u,label:a,type:"topic"});for(const g of i)d(g,u,"topic")}}return{nodes:p,links:c}}function S(o,r,f=2){const l=o?.nodes||[],p=o?.links||[],c=new Map(l.map(t=>[t.id,t]));if(!c.has(r))return{nodes:[],links:[]};const s=new Map;for(const t of p)s.has(t.s)||s.set(t.s,new Set),s.has(t.t)||s.set(t.t,new Set),s.get(t.s).add(t.t),s.get(t.t).add(t.s);const d=new Set([r]);let n=new Set([r]);for(let t=0;t<f;t++){const e=new Set;for(const a of n)for(const i of s.get(a)||[])d.has(i)||(d.add(i),e.add(i));n=e}return{nodes:[...d].map(t=>{const e=c.get(t);return{id:t,label:e?.label||"Untitled note",type:e?.type||"note",focus:t===r}}),links:p.filter(t=>d.has(t.s)&&d.has(t.t))}}function y(o,r=140,f=""){const l=o?.nodes||[],p=o?.links||[];if(l.length<=r)return{nodes:l,links:p};const c=new Map;for(const n of p)c.set(n.s,(c.get(n.s)||0)+1),c.set(n.t,(c.get(n.t)||0)+1);const s=[...l].sort((n,t)=>n.id===f?-1:t.id===f?1:(c.get(t.id)||0)-(c.get(n.id)||0)).slice(0,r),d=new Set(s.map(n=>n.id));return{nodes:s,links:p.filter(n=>d.has(n.s)&&d.has(n.t))}}function b(o){const r=o?.nodes||[];return{notes:r.filter(f=>f.type!=="topic").length,topics:r.filter(f=>f.type==="topic").length,links:(o?.links||[]).length}}export{k as MAX_GRAPH_NODES,w as buildNoteGraph,S as egoGraph,b as graphStats,y as trimGraph};
|
package/note-links.js
CHANGED
|
@@ -1,68 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Split from `note-mentions.js` for a reason that is not taxonomy. The mention grammar is
|
|
4
|
-
// asked a question by a KEYSTROKE handler — "is this line a delegation?" — which cannot await
|
|
5
|
-
// a module, so it must already be loaded; the extension's Notes page therefore carries it on
|
|
6
|
-
// its first-paint graph. Link picking happens inside an autocomplete popup, which is async by
|
|
7
|
-
// nature and can load what it needs when the user types the second bracket.
|
|
8
|
-
//
|
|
9
|
-
// Keeping them in one file put both on that graph and pushed the extension's Notes first
|
|
10
|
-
// paint 1.6 KB over its budget. The guard caught it, and this is the shape that answer takes:
|
|
11
|
-
// a module is the unit of loading, so "needed synchronously" and "needed on demand" are not
|
|
12
|
-
// allowed to share one.
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The `[[…` link query under the caret, or null — `{ query, start, end }`, where [start, end)
|
|
16
|
-
* is the text typed AFTER the opening brackets, which is what a pick replaces.
|
|
17
|
-
*
|
|
18
|
-
* Deliberately not a `triggerQueryAt` trigger: `[[` is two characters, the query may contain
|
|
19
|
-
* spaces (page titles do), and it ends at a `]]` rather than at whitespace. A closed link is
|
|
20
|
-
* not a query — once `]]` is typed the user is past it — and neither is one carrying a newline
|
|
21
|
-
* or another bracket, which mean the `[[` above belongs to something else entirely.
|
|
22
|
-
*/
|
|
23
|
-
export function wikiQueryAt(text, pos, { hasSelection = false } = {}) {
|
|
24
|
-
if (hasSelection) return null;
|
|
25
|
-
const v = String(text ?? '');
|
|
26
|
-
const p = Math.max(0, Math.min(Number.isFinite(pos) ? pos : 0, v.length));
|
|
27
|
-
const upto = v.slice(0, p);
|
|
28
|
-
const open = upto.lastIndexOf('[[');
|
|
29
|
-
if (open < 0) return null;
|
|
30
|
-
const between = upto.slice(open + 2);
|
|
31
|
-
if (/[[\]\n]/.test(between)) return null;
|
|
32
|
-
return { query: between, start: open + 2, end: p };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Rank link targets for what has been typed.
|
|
37
|
-
*
|
|
38
|
-
* Prefix matches first — someone typing "Roll" means a page whose name STARTS that way, and
|
|
39
|
-
* burying it under a note that mentions "rollback" in the middle of its title is the ranking
|
|
40
|
-
* being clever at the user's expense. Then substring, each group alphabetical so the list is
|
|
41
|
-
* stable between keystrokes.
|
|
42
|
-
*
|
|
43
|
-
* DEDUPED BY TITLE, because a `[[link]]` addresses a title and nothing else: two records
|
|
44
|
-
* called "Atlas" are one destination, and offering the name twice asks the user to choose
|
|
45
|
-
* between two things they cannot tell apart. The first of a title wins, which — given the
|
|
46
|
-
* caller passes its index in its own order — is the one it considers canonical.
|
|
47
|
-
*/
|
|
48
|
-
export function rankLinkTargets(targets, query, limit = 8) {
|
|
49
|
-
const q = String(query || '').trim().toLowerCase();
|
|
50
|
-
const byTitleSeen = new Set();
|
|
51
|
-
const list = (Array.isArray(targets) ? targets : []).filter((t) => {
|
|
52
|
-
if (!t || !t.title) return false;
|
|
53
|
-
const key = String(t.title).trim().toLowerCase();
|
|
54
|
-
if (byTitleSeen.has(key)) return false;
|
|
55
|
-
byTitleSeen.add(key);
|
|
56
|
-
return true;
|
|
57
|
-
});
|
|
58
|
-
const byTitle = (a, b) => String(a.title).localeCompare(String(b.title));
|
|
59
|
-
if (!q) return [...list].sort(byTitle).slice(0, limit);
|
|
60
|
-
const starts = [];
|
|
61
|
-
const has = [];
|
|
62
|
-
for (const t of list) {
|
|
63
|
-
const title = String(t.title).toLowerCase();
|
|
64
|
-
if (title.startsWith(q)) starts.push(t);
|
|
65
|
-
else if (title.includes(q)) has.push(t);
|
|
66
|
-
}
|
|
67
|
-
return [...starts.sort(byTitle), ...has.sort(byTitle)].slice(0, limit);
|
|
68
|
-
}
|
|
1
|
+
function f(l,c,{hasSelection:u=!1}={}){if(u)return null;const r=String(l??""),s=Math.max(0,Math.min(Number.isFinite(c)?c:0,r.length)),i=r.slice(0,s),e=i.lastIndexOf("[[");if(e<0)return null;const o=i.slice(e+2);return/[[\]\n]/.test(o)?null:{query:o,start:e+2,end:s}}function h(l,c,u=8){const r=String(c||"").trim().toLowerCase(),s=new Set,i=(Array.isArray(l)?l:[]).filter(t=>{if(!t||!t.title)return!1;const n=String(t.title).trim().toLowerCase();return s.has(n)?!1:(s.add(n),!0)}),e=(t,n)=>String(t.title).localeCompare(String(n.title));if(!r)return[...i].sort(e).slice(0,u);const o=[],a=[];for(const t of i){const n=String(t.title).toLowerCase();n.startsWith(r)?o.push(t):n.includes(r)&&a.push(t)}return[...o.sort(e),...a.sort(e)].slice(0,u)}export{h as rankLinkTargets,f as wikiQueryAt};
|
package/note-mentions.js
CHANGED
|
@@ -1,117 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// agent and scope it to a saved skill. Both are written as tokens in the text, which means
|
|
5
|
-
// both are grammar: something has to say where the token ends and the instruction begins, and
|
|
6
|
-
// two clients answering that differently is one feature behaving as two.
|
|
7
|
-
//
|
|
8
|
-
// These were `notes-util.js` in the extension — already pure, already tested, and already the
|
|
9
|
-
// right shape; they simply lived where only one client could reach them. The desktop needs
|
|
10
|
-
// exactly the same answers, and a mobile client will too.
|
|
11
|
-
//
|
|
12
|
-
// WHAT IS NOT HERE: running the agent, arming its tools, and streaming into the editor. Those
|
|
13
|
-
// need a target resolver, a transport and a document — all platform. What is here is the
|
|
14
|
-
// reading of the line and the resolving of a name against a list the caller supplies.
|
|
1
|
+
function a(i){const e=String(i||""),n=e.match(/@\[([^\]\n]+)\]/);if(!n)return{name:"",task:""};const t=n[1].trim(),s=`${e.slice(0,n.index)} ${e.slice(n.index+n[0].length)}`.replace(/\s+/g," ").trim();return{name:t,task:s}}function l(i,e){const n=String(i??""),t=Math.max(0,Math.min(Number.isFinite(e)?e:0,n.length)),s=n.lastIndexOf(`
|
|
2
|
+
`,t-1)+1;let o=n.indexOf(`
|
|
3
|
+
`,t);o<0&&(o=n.length);const{name:r,task:c}=a(n.slice(s,o));return!r||!c?null:{name:r,task:c,start:s,end:o}}function u(i){const e=String(i||""),n=e.match(/#\[([^\]\n]+)\]/);return n?{name:n[1].trim(),text:e.replace(n[0],"").replace(/[ \t]{2,}/g," ").trim()}:{name:"",text:e.trim()}}function m(i,e){const n=String(i||""),t=String(e||"");return/\{\{\s*input[^}]*\}\}/i.test(n)?n.replace(/\{\{\s*input[^}]*\}\}/gi,t):n?t?`${n}
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* The instruction may sit BEFORE or AFTER the token — "Update the plan @[Codex]" and
|
|
20
|
-
* "@[Codex] update the plan" are the same request — so the task is the whole line minus the
|
|
21
|
-
* token. Brackets are what let an agent's name contain spaces.
|
|
22
|
-
*
|
|
23
|
-
* Returns `{ name, task }`, both `''` when the line carries no runnable mention. A mention
|
|
24
|
-
* with no task is not runnable: `@[Codex]` alone is someone still typing.
|
|
25
|
-
*/
|
|
26
|
-
export function parseAgentMention(line) {
|
|
27
|
-
const s = String(line || '');
|
|
28
|
-
const m = s.match(/@\[([^\]\n]+)\]/);
|
|
29
|
-
if (!m) return { name: '', task: '' };
|
|
30
|
-
const name = m[1].trim();
|
|
31
|
-
const task = (`${s.slice(0, m.index)} ${s.slice(m.index + m[0].length)}`).replace(/\s+/g, ' ').trim();
|
|
32
|
-
return { name, task };
|
|
33
|
-
}
|
|
5
|
+
${t}`:n:t}function f(i,e){const n=String(e||"").trim().toLowerCase();if(!n)return null;const t=Array.isArray(i)?i:[],s=o=>String(o?.name||o?.title||"").toLowerCase();return t.find(o=>s(o)===n)||t.find(o=>s(o).includes(n))||null}function g(i,e){const n=String(e||"").trim().toLowerCase();if(!n)return null;const t=Array.isArray(i)?i:[],s=r=>String(r?.name||"").toLowerCase(),o=r=>String(r?.model||r?.bridgeAgent||r?.id||"").toLowerCase();return t.find(r=>s(r)===n)||t.find(r=>o(r)===n)||t.find(r=>s(r)&&s(r).includes(n))||t.find(r=>o(r).includes(n))||null}function d(i,e){return`${String(e||"").split(`
|
|
6
|
+
`).map(t=>`> ${t}`).join(`
|
|
7
|
+
`)}
|
|
34
8
|
|
|
35
|
-
|
|
36
|
-
* The runnable `@[Agent] task` on the caret's line, or null — `{ name, task, start, end }`
|
|
37
|
-
* where [start, end) is the WHOLE line, because the Q&A replaces the line it was written on.
|
|
38
|
-
*/
|
|
39
|
-
export function agentMentionAt(text, pos) {
|
|
40
|
-
const v = String(text ?? '');
|
|
41
|
-
const p = Math.max(0, Math.min(Number.isFinite(pos) ? pos : 0, v.length));
|
|
42
|
-
const start = v.lastIndexOf('\n', p - 1) + 1;
|
|
43
|
-
let end = v.indexOf('\n', p);
|
|
44
|
-
if (end < 0) end = v.length;
|
|
45
|
-
const { name, task } = parseAgentMention(v.slice(start, end));
|
|
46
|
-
if (!name || !task) return null;
|
|
47
|
-
return { name, task, start, end };
|
|
48
|
-
}
|
|
9
|
+
**${String(i||"Agent")}:**
|
|
49
10
|
|
|
50
|
-
|
|
51
|
-
* Pull a `#[Skill Name]` out of an instruction. Returns the name (or `''`) and the
|
|
52
|
-
* instruction with the token removed — the token is addressing, not content, and leaving it
|
|
53
|
-
* in means the model is asked to do something with the literal text "#[Weekly review]".
|
|
54
|
-
*/
|
|
55
|
-
export function parseSkillMention(instruction) {
|
|
56
|
-
const s = String(instruction || '');
|
|
57
|
-
const m = s.match(/#\[([^\]\n]+)\]/);
|
|
58
|
-
if (!m) return { name: '', text: s.trim() };
|
|
59
|
-
return { name: m[1].trim(), text: s.replace(m[0], '').replace(/[ \t]{2,}/g, ' ').trim() };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Merge a skill's saved prompt with the user's task.
|
|
64
|
-
*
|
|
65
|
-
* `{{input}}` placeholders are substituted when the author wrote any — a skill that says
|
|
66
|
-
* "Summarize {{input}} for an exec" wants the task in the middle, not stapled underneath.
|
|
67
|
-
* With no placeholder the task is appended, which is what an instruction-style skill expects.
|
|
68
|
-
*/
|
|
69
|
-
export function mergeSkillPrompt(prompt, task) {
|
|
70
|
-
const p = String(prompt || '');
|
|
71
|
-
const t = String(task || '');
|
|
72
|
-
if (/\{\{\s*input[^}]*\}\}/i.test(p)) return p.replace(/\{\{\s*input[^}]*\}\}/gi, t);
|
|
73
|
-
return p ? (t ? `${p}\n\n${t}` : p) : t;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Resolve a skill by display name — exact first, then contains. */
|
|
77
|
-
export function findSkillByName(skills, name) {
|
|
78
|
-
const q = String(name || '').trim().toLowerCase();
|
|
79
|
-
if (!q) return null;
|
|
80
|
-
const list = Array.isArray(skills) ? skills : [];
|
|
81
|
-
const of = (s) => String(s?.name || s?.title || '').toLowerCase();
|
|
82
|
-
return list.find((s) => of(s) === q) || list.find((s) => of(s).includes(q)) || null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolve a mentioned TARGET by name against whatever list the client has.
|
|
87
|
-
*
|
|
88
|
-
* Exact name, then a contains match, then the underlying model or agent id — so `@[Claude]`
|
|
89
|
-
* finds "Claude Code", and `@[codex]` finds a target named something else that runs `codex`.
|
|
90
|
-
* The entries are duck-typed (`name`, `model`, `bridgeAgent`, or a bare `id`) because the
|
|
91
|
-
* extension holds configured endpoints and agents while the desktop holds the gateway's model
|
|
92
|
-
* list, and neither should have to reshape its data to ask this question.
|
|
93
|
-
*/
|
|
94
|
-
export function findTargetByName(targets, name) {
|
|
95
|
-
const q = String(name || '').trim().toLowerCase();
|
|
96
|
-
if (!q) return null;
|
|
97
|
-
const list = Array.isArray(targets) ? targets : [];
|
|
98
|
-
const nameOf = (t) => String(t?.name || '').toLowerCase();
|
|
99
|
-
const idOf = (t) => String(t?.model || t?.bridgeAgent || t?.id || '').toLowerCase();
|
|
100
|
-
return list.find((t) => nameOf(t) === q)
|
|
101
|
-
|| list.find((t) => idOf(t) === q)
|
|
102
|
-
|| list.find((t) => nameOf(t) && nameOf(t).includes(q))
|
|
103
|
-
|| list.find((t) => idOf(t).includes(q))
|
|
104
|
-
|| null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* How a delegated answer is written into the note.
|
|
109
|
-
*
|
|
110
|
-
* The question is quoted and the agent named above its reply, so a note that collected three
|
|
111
|
-
* delegations reads as a transcript rather than as three anonymous blocks of prose. Returned
|
|
112
|
-
* separately from the answer because the client streams INTO the gap between them.
|
|
113
|
-
*/
|
|
114
|
-
export function mentionAnswerPrefix(name, task) {
|
|
115
|
-
const quoted = String(task || '').split('\n').map((l) => `> ${l}`).join('\n');
|
|
116
|
-
return `${quoted}\n\n**${String(name || 'Agent')}:**\n\n`;
|
|
117
|
-
}
|
|
11
|
+
`}export{l as agentMentionAt,f as findSkillByName,g as findTargetByName,d as mentionAnswerPrefix,m as mergeSkillPrompt,a as parseAgentMention,u as parseSkillMention};
|