clementine-agent 1.18.170 → 1.18.172
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/dist/agent/background-tasks.js +13 -0
- package/dist/agent/chat-skill-resolver.js +43 -5
- package/dist/agent/complex-task-detector.d.ts +9 -0
- package/dist/agent/complex-task-detector.js +118 -0
- package/dist/cli/dashboard.js +283 -41
- package/dist/gateway/router.d.ts +21 -1
- package/dist/gateway/router.js +230 -10
- package/dist/gateway/turn-ledger.d.ts +7 -0
- package/dist/gateway/turn-ledger.js +7 -1
- package/dist/tools/background-task-tools.js +4 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -113,6 +113,14 @@ export function markRunning(id, opts) {
|
|
|
113
113
|
safeWrite(pathFor(id, opts), task);
|
|
114
114
|
return task;
|
|
115
115
|
}
|
|
116
|
+
function writeFullResultFile(id, result, opts) {
|
|
117
|
+
if (result.length <= RESULT_TRUNCATE_BYTES)
|
|
118
|
+
return undefined;
|
|
119
|
+
const file = path.join(dirFor(opts), `${id}.result.md`);
|
|
120
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
writeFileSync(file, result);
|
|
122
|
+
return file;
|
|
123
|
+
}
|
|
116
124
|
/** Transition to 'done' with final result. */
|
|
117
125
|
export function markDone(id, result, deliverableNote, opts) {
|
|
118
126
|
const task = loadBackgroundTask(id, opts);
|
|
@@ -122,9 +130,14 @@ export function markDone(id, result, deliverableNote, opts) {
|
|
|
122
130
|
return task;
|
|
123
131
|
task.status = 'done';
|
|
124
132
|
task.completedAt = new Date().toISOString();
|
|
133
|
+
const resultPath = writeFullResultFile(id, result, opts);
|
|
134
|
+
if (resultPath)
|
|
135
|
+
task.resultPath = resultPath;
|
|
125
136
|
task.result = result;
|
|
126
137
|
if (deliverableNote)
|
|
127
138
|
task.deliverableNote = deliverableNote;
|
|
139
|
+
else if (resultPath)
|
|
140
|
+
task.deliverableNote = resultPath;
|
|
128
141
|
safeWrite(pathFor(id, opts), task);
|
|
129
142
|
return task;
|
|
130
143
|
}
|
|
@@ -58,10 +58,21 @@ import pino from 'pino';
|
|
|
58
58
|
import { searchSkills } from './skill-extractor.js';
|
|
59
59
|
const logger = pino({ name: 'clementine.chat-skill-resolver' });
|
|
60
60
|
// ── Tunables ──────────────────────────────────────────────────────────
|
|
61
|
-
/** Default minimum score
|
|
62
|
-
* legacy `assistant.ts:1492` threshold.
|
|
63
|
-
* this filter keeps weak matches from injecting unrelated tooling. */
|
|
61
|
+
/** Default minimum score for user-authored skill matches. Mirrors the
|
|
62
|
+
* legacy `assistant.ts:1492` threshold. */
|
|
64
63
|
const DEFAULT_MIN_SCORE = 4;
|
|
64
|
+
/** Higher threshold applied when ALL matches are auto-generated MCP-derived
|
|
65
|
+
* skills (no user-authored signal). 1.18.171 hotfix: a vague chat message
|
|
66
|
+
* ("did our changes break it?") matched three unrelated auto-skills
|
|
67
|
+
* (ElevenLabs + apify) at score 5.5 each because semantic-only matching
|
|
68
|
+
* drifted toward whatever embeddings were closest. Bumping the bar for
|
|
69
|
+
* auto-only match-sets keeps that noise out of the system prompt. */
|
|
70
|
+
const AUTO_ONLY_MIN_SCORE = 8;
|
|
71
|
+
/** When ALL matches are auto-generated AND they reference this many or
|
|
72
|
+
* more distinct servers, the cluster is treated as semantic-noise and
|
|
73
|
+
* the injection is skipped entirely. Three different services have no
|
|
74
|
+
* business being "all relevant" to a single user message. */
|
|
75
|
+
const AUTO_ONLY_SERVER_NOISE_THRESHOLD = 3;
|
|
65
76
|
/** Default top-K matches to aggregate. Single-tool requests usually
|
|
66
77
|
* return one strong match; category requests ("salesforce") return
|
|
67
78
|
* several similarly-scored auto-skills. Top-3 covers both. Raising
|
|
@@ -228,9 +239,36 @@ export function resolveSkillsForChat(userMessage, opts = {}) {
|
|
|
228
239
|
logger.debug({ err }, 'chat-skill-resolver: searchSkills failed (non-fatal)');
|
|
229
240
|
return empty;
|
|
230
241
|
}
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
// 1.18.171 hotfix: detect auto-only match-sets and apply the higher
|
|
243
|
+
// threshold + noise-cluster filter so vague chat messages don't surface
|
|
244
|
+
// unrelated MCP context. See the comment block on AUTO_ONLY_MIN_SCORE.
|
|
245
|
+
const isAutoMatch = (m) => m.name.startsWith('auto-');
|
|
246
|
+
const candidatesAllAuto = candidates.length > 0 && candidates.every(isAutoMatch);
|
|
247
|
+
const effectiveMinScore = candidatesAllAuto
|
|
248
|
+
? Math.max(minScore, AUTO_ONLY_MIN_SCORE)
|
|
249
|
+
: minScore;
|
|
250
|
+
let matches = candidates
|
|
251
|
+
.filter((m) => m.score >= effectiveMinScore)
|
|
233
252
|
.slice(0, limit);
|
|
253
|
+
// Auto-only noise cluster filter: when every survivor is auto AND they
|
|
254
|
+
// collectively reference too many distinct servers (no semantic
|
|
255
|
+
// clustering on a single service), treat as drift and drop.
|
|
256
|
+
if (matches.length >= 2 && matches.every(isAutoMatch)) {
|
|
257
|
+
const seenServers = new Set();
|
|
258
|
+
for (const m of matches) {
|
|
259
|
+
for (const s of extractMcpServersFromMatch(m))
|
|
260
|
+
seenServers.add(s);
|
|
261
|
+
}
|
|
262
|
+
if (seenServers.size >= AUTO_ONLY_SERVER_NOISE_THRESHOLD) {
|
|
263
|
+
logger.info({
|
|
264
|
+
droppedMatches: matches.map(m => ({ name: m.name, score: Number(m.score.toFixed(2)) })),
|
|
265
|
+
distinctServers: [...seenServers],
|
|
266
|
+
reason: 'auto_only_server_cluster_too_wide',
|
|
267
|
+
queryChars,
|
|
268
|
+
}, 'chat-skill-resolver: dropped match-set (semantic noise)');
|
|
269
|
+
matches = [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
234
272
|
if (matches.length === 0) {
|
|
235
273
|
return {
|
|
236
274
|
...empty,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ComplexTaskRecommendation {
|
|
2
|
+
score: number;
|
|
3
|
+
reasons: string[];
|
|
4
|
+
suggestedMaxMinutes: number;
|
|
5
|
+
plan: string[];
|
|
6
|
+
queueImmediately: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function detectComplexTaskForBackground(text: string): ComplexTaskRecommendation | null;
|
|
9
|
+
//# sourceMappingURL=complex-task-detector.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const SKILL_AUTHORING_RE = /\b(create|make|build|draft|write|teach|save|update)\b.{0,40}\b(skill|SKILL\.md)\b|\bskill[- ]creator\b/i;
|
|
2
|
+
const EXPLICIT_BACKGROUND_RE = /\b(background|deep mode|keep working|don't stop|dont stop|autonomous|long[- ]running|run overnight|take your time)\b/i;
|
|
3
|
+
const COMPLEX_WORK_RE = /\b(audit|research|analy[sz]e|review|scrape|crawl|extract|enrich|compile|compare|verify|cross[- ]check|triage|reconcile|draft|generate|update|sync|report back|write back)\b/i;
|
|
4
|
+
const BATCH_RE = /\b(all|every|each|bulk|batch|list of|contacts?|leads?|accounts?|tasks?|tickets?|records?|rows?|pages?|repos?|projects?)\b/i;
|
|
5
|
+
const SIDE_EFFECT_RE = /\b(update|write|create|draft|send|post|comment|reply|upload|append|sync|mark|close|move)\b/i;
|
|
6
|
+
const MULTI_STEP_RE = /\b(and then|then|after that|finally|from .* to |against .* and |across|compile .* into|check .* then)\b/i;
|
|
7
|
+
const SYSTEM_KEYWORDS = [
|
|
8
|
+
'asana',
|
|
9
|
+
'salesforce',
|
|
10
|
+
'google sheet',
|
|
11
|
+
'google sheets',
|
|
12
|
+
'sheet',
|
|
13
|
+
'sheets',
|
|
14
|
+
'dataforseo',
|
|
15
|
+
'hubspot',
|
|
16
|
+
'notion',
|
|
17
|
+
'github',
|
|
18
|
+
'gmail',
|
|
19
|
+
'outlook',
|
|
20
|
+
'slack',
|
|
21
|
+
'discord',
|
|
22
|
+
'website',
|
|
23
|
+
'websites',
|
|
24
|
+
'crm',
|
|
25
|
+
'spreadsheet',
|
|
26
|
+
'csv',
|
|
27
|
+
'airtable',
|
|
28
|
+
'linear',
|
|
29
|
+
'jira',
|
|
30
|
+
];
|
|
31
|
+
function countSystemMentions(text) {
|
|
32
|
+
const lower = text.toLowerCase();
|
|
33
|
+
let count = 0;
|
|
34
|
+
for (const keyword of SYSTEM_KEYWORDS) {
|
|
35
|
+
if (lower.includes(keyword))
|
|
36
|
+
count++;
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
function estimatedMinutes(score, systemCount) {
|
|
41
|
+
if (score >= 8 || systemCount >= 4)
|
|
42
|
+
return 90;
|
|
43
|
+
if (score >= 6 || systemCount >= 3)
|
|
44
|
+
return 60;
|
|
45
|
+
return 30;
|
|
46
|
+
}
|
|
47
|
+
function buildPlan(text, systemCount) {
|
|
48
|
+
const lower = text.toLowerCase();
|
|
49
|
+
const plan = [];
|
|
50
|
+
plan.push('Confirm the exact scope, filters, and write/send permissions before making side-effecting changes.');
|
|
51
|
+
if (systemCount > 0) {
|
|
52
|
+
plan.push('Connect to the named systems with official MCP/API/CLI tools and use the narrowest reliable query.');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
plan.push('Gather the source material with the available project, file, web, memory, or CLI tools.');
|
|
56
|
+
}
|
|
57
|
+
if (BATCH_RE.test(text)) {
|
|
58
|
+
plan.push('Process records in batches, track counts, and keep skipped/error records separate.');
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('enrich') || lower.includes('dataforseo')) {
|
|
61
|
+
plan.push('Enrich only qualified records and keep the signal used for each output row or draft.');
|
|
62
|
+
}
|
|
63
|
+
if (SIDE_EFFECT_RE.test(text)) {
|
|
64
|
+
plan.push('Create drafts or updates first; only send or commit irreversible changes after explicit approval.');
|
|
65
|
+
}
|
|
66
|
+
plan.push('Return a concise final report with counts, changed locations, failures, and recommended next action.');
|
|
67
|
+
return plan.slice(0, 6);
|
|
68
|
+
}
|
|
69
|
+
export function detectComplexTaskForBackground(text) {
|
|
70
|
+
const trimmed = text.trim();
|
|
71
|
+
if (!trimmed)
|
|
72
|
+
return null;
|
|
73
|
+
if (SKILL_AUTHORING_RE.test(trimmed))
|
|
74
|
+
return null;
|
|
75
|
+
const systemCount = countSystemMentions(trimmed);
|
|
76
|
+
const reasons = [];
|
|
77
|
+
let score = 0;
|
|
78
|
+
if (EXPLICIT_BACKGROUND_RE.test(trimmed)) {
|
|
79
|
+
score += 4;
|
|
80
|
+
reasons.push('explicit background/deep-work wording');
|
|
81
|
+
}
|
|
82
|
+
if (COMPLEX_WORK_RE.test(trimmed)) {
|
|
83
|
+
score += 2;
|
|
84
|
+
reasons.push('multi-step work verb');
|
|
85
|
+
}
|
|
86
|
+
if (BATCH_RE.test(trimmed)) {
|
|
87
|
+
score += 2;
|
|
88
|
+
reasons.push('batch or many-record scope');
|
|
89
|
+
}
|
|
90
|
+
if (SIDE_EFFECT_RE.test(trimmed)) {
|
|
91
|
+
score += 1;
|
|
92
|
+
reasons.push('write/draft/update side effects');
|
|
93
|
+
}
|
|
94
|
+
if (MULTI_STEP_RE.test(trimmed)) {
|
|
95
|
+
score += 1;
|
|
96
|
+
reasons.push('multi-step sequencing');
|
|
97
|
+
}
|
|
98
|
+
if (systemCount >= 2) {
|
|
99
|
+
score += Math.min(4, systemCount);
|
|
100
|
+
reasons.push(`${systemCount} named systems or data surfaces`);
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.length > 450) {
|
|
103
|
+
score += 1;
|
|
104
|
+
reasons.push('long detailed request');
|
|
105
|
+
}
|
|
106
|
+
const queueImmediately = EXPLICIT_BACKGROUND_RE.test(trimmed) && score >= 5;
|
|
107
|
+
const shouldOffer = queueImmediately || score >= 5 || (systemCount >= 2 && (BATCH_RE.test(trimmed) || SIDE_EFFECT_RE.test(trimmed)));
|
|
108
|
+
if (!shouldOffer)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
score,
|
|
112
|
+
reasons,
|
|
113
|
+
suggestedMaxMinutes: estimatedMinutes(score, systemCount),
|
|
114
|
+
plan: buildPlan(trimmed, systemCount),
|
|
115
|
+
queueImmediately,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=complex-task-detector.js.map
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -2936,6 +2936,40 @@ export async function cmdDashboard(opts) {
|
|
|
2936
2936
|
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2937
2937
|
}
|
|
2938
2938
|
});
|
|
2939
|
+
app.post('/api/background-offers/:id/accept', async (req, res) => {
|
|
2940
|
+
try {
|
|
2941
|
+
const id = req.params.id;
|
|
2942
|
+
if (!/^bo-[a-z0-9]+-[a-z0-9]{3,10}$/.test(id)) {
|
|
2943
|
+
res.status(400).json({ error: 'Invalid background offer id' });
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
const gateway = await getGateway();
|
|
2947
|
+
const result = gateway.acceptBackgroundOffer('dashboard:web', id);
|
|
2948
|
+
if (!result.ok) {
|
|
2949
|
+
res.status(404).json({ ok: false, error: result.response });
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
res.json(result);
|
|
2953
|
+
}
|
|
2954
|
+
catch (err) {
|
|
2955
|
+
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2956
|
+
}
|
|
2957
|
+
});
|
|
2958
|
+
app.post('/api/background-offers/:id/dismiss', async (req, res) => {
|
|
2959
|
+
try {
|
|
2960
|
+
const id = req.params.id;
|
|
2961
|
+
if (!/^bo-[a-z0-9]+-[a-z0-9]{3,10}$/.test(id)) {
|
|
2962
|
+
res.status(400).json({ error: 'Invalid background offer id' });
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
const gateway = await getGateway();
|
|
2966
|
+
const result = gateway.dismissBackgroundOffer('dashboard:web', id);
|
|
2967
|
+
res.status(result.ok ? 200 : 404).json(result.ok ? result : { ok: false, error: result.response });
|
|
2968
|
+
}
|
|
2969
|
+
catch (err) {
|
|
2970
|
+
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2939
2973
|
app.get('/api/heartbeat/agent/:slug', (req, res) => {
|
|
2940
2974
|
const slug = req.params.slug;
|
|
2941
2975
|
const state = getHeartbeat();
|
|
@@ -21604,40 +21638,54 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
21604
21638
|
</div>
|
|
21605
21639
|
</div>
|
|
21606
21640
|
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
21607
|
-
<button class="btn-secondary" onclick="openSkillStudio()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open the
|
|
21641
|
+
<button class="btn-secondary" onclick="openSkillStudio()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open the natural-language Skill Studio">
|
|
21608
21642
|
Open Studio
|
|
21609
21643
|
</button>
|
|
21610
|
-
<button class="btn-primary" onclick="openCreateSkillModalFromComposer()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open a
|
|
21611
|
-
|
|
21644
|
+
<button class="btn-primary" onclick="openCreateSkillModalFromComposer()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Open a blank SKILL.md editor">
|
|
21645
|
+
Manual editor
|
|
21612
21646
|
</button>
|
|
21613
21647
|
</div>
|
|
21614
21648
|
</div>
|
|
21649
|
+
<div id="skill-composer-home">
|
|
21615
21650
|
<div id="skill-composer" style="margin:0 0 16px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:14px 16px">
|
|
21616
|
-
<div style="display:
|
|
21651
|
+
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:12px">
|
|
21652
|
+
<div style="min-width:0">
|
|
21653
|
+
<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:3px">Skill Studio</div>
|
|
21654
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.45">Describe reusable work in plain language. Optional starting points only seed the draft; nothing runs until you save or test it.</div>
|
|
21655
|
+
</div>
|
|
21656
|
+
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;white-space:nowrap;margin-top:2px">Natural language first</div>
|
|
21657
|
+
</div>
|
|
21658
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px;align-items:start">
|
|
21617
21659
|
<div>
|
|
21618
|
-
<label for="skill-composer-text" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">
|
|
21619
|
-
<textarea id="skill-composer-text" rows="
|
|
21660
|
+
<label for="skill-composer-text" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">What should Clementine learn?</label>
|
|
21661
|
+
<textarea id="skill-composer-text" rows="5" oninput="updateSkillComposerDraftState()" placeholder="Find Salesforce contacts I have not touched in 15 days, enrich the accounts with DataForSEO signals, draft cold prospecting emails, then report the drafts back for review." style="width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:13px;line-height:1.45;resize:vertical;min-height:112px"></textarea>
|
|
21662
|
+
<div style="margin-top:8px;font-size:11px;color:var(--text-muted);line-height:1.45">Good skills name the repeatable outcome, required tools or data, approval boundaries, and what counts as done.</div>
|
|
21620
21663
|
</div>
|
|
21621
21664
|
<div>
|
|
21622
|
-
<div style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">
|
|
21665
|
+
<div style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Optional starting points</div>
|
|
21623
21666
|
<div id="skill-composer-modes" style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:4px;margin-bottom:10px">
|
|
21624
21667
|
<button type="button" class="skill-composer-mode" data-kind="outcome" onclick="setSkillComposerMode('outcome')" style="padding:7px 6px;border:1px solid var(--accent);border-radius:6px;background:rgba(255,141,0,0.10);color:var(--accent);font-size:11px;font-weight:600;cursor:pointer">Outcome</button>
|
|
21625
|
-
<button type="button" class="skill-composer-mode" data-kind="tool" onclick="setSkillComposerMode('tool')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">
|
|
21668
|
+
<button type="button" class="skill-composer-mode" data-kind="tool" onclick="setSkillComposerMode('tool')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">MCP/API</button>
|
|
21626
21669
|
<button type="button" class="skill-composer-mode" data-kind="cli" onclick="setSkillComposerMode('cli')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">CLI</button>
|
|
21627
21670
|
<button type="button" class="skill-composer-mode" data-kind="project" onclick="setSkillComposerMode('project')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">Project</button>
|
|
21628
21671
|
<button type="button" class="skill-composer-mode" data-kind="memory" onclick="setSkillComposerMode('memory')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">Memory</button>
|
|
21629
21672
|
</div>
|
|
21630
21673
|
<label id="skill-composer-anchor-label" for="skill-composer-anchor" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Anchor</label>
|
|
21631
|
-
<
|
|
21674
|
+
<div style="display:flex;gap:6px;align-items:center">
|
|
21675
|
+
<input id="skill-composer-anchor" list="skill-composer-anchor-options" oninput="updateSkillComposerDraftState()" onkeydown="if(event.key==='Enter'){event.preventDefault();addSkillComposerDependency();}" placeholder="Optional tool, project, command, or memory source" style="flex:1;min-width:0;box-sizing:border-box;padding:9px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:12px">
|
|
21676
|
+
<button type="button" class="btn-secondary" onclick="addSkillComposerDependency()" style="font-size:12px;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Add</button>
|
|
21677
|
+
</div>
|
|
21632
21678
|
<datalist id="skill-composer-anchor-options"></datalist>
|
|
21633
21679
|
<div id="skill-composer-anchor-summary" style="margin-top:8px;min-height:22px;font-size:11px;color:var(--text-muted);line-height:1.45">No starting point selected.</div>
|
|
21680
|
+
<div id="skill-composer-preview" style="margin-top:10px;max-height:180px;overflow:auto;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);padding:10px 12px;font-size:11px;line-height:1.45;color:var(--text-secondary)"></div>
|
|
21634
21681
|
<div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:12px">
|
|
21635
|
-
<button type="button" class="btn-secondary" onclick="
|
|
21636
|
-
<button type="button" class="btn-primary" id="skill-composer-draft-btn" onclick="startSkillComposerDraft()" disabled style="font-size:12px;padding:7px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:0.55">
|
|
21682
|
+
<button type="button" class="btn-secondary" onclick="startSkillComposerChat()" style="font-size:12px;padding:7px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Build in chat</button>
|
|
21683
|
+
<button type="button" class="btn-primary" id="skill-composer-draft-btn" onclick="startSkillComposerDraft()" disabled style="font-size:12px;padding:7px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:0.55">Review draft</button>
|
|
21637
21684
|
</div>
|
|
21638
21685
|
</div>
|
|
21639
21686
|
</div>
|
|
21640
21687
|
</div>
|
|
21688
|
+
</div>
|
|
21641
21689
|
<div style="display:grid;grid-template-columns:380px 1fr;gap:18px;height:calc(100vh - 360px);min-height:440px">
|
|
21642
21690
|
<div id="skills-list-pane" style="overflow-y:auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary)">
|
|
21643
21691
|
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px">
|
|
@@ -28725,6 +28773,7 @@ var _skillsState = {
|
|
|
28725
28773
|
|
|
28726
28774
|
var _skillComposerMode = 'outcome';
|
|
28727
28775
|
var _skillComposerOptionsCache = null;
|
|
28776
|
+
var _skillComposerDependencies = [];
|
|
28728
28777
|
|
|
28729
28778
|
var _skillComposerCopy = {
|
|
28730
28779
|
outcome: {
|
|
@@ -28785,34 +28834,77 @@ function updateSkillComposerDraftState() {
|
|
|
28785
28834
|
var text = (document.getElementById('skill-composer-text') || {}).value || '';
|
|
28786
28835
|
var anchor = (document.getElementById('skill-composer-anchor') || {}).value || '';
|
|
28787
28836
|
var btn = document.getElementById('skill-composer-draft-btn');
|
|
28788
|
-
var enabled = !!(text.trim() || anchor.trim());
|
|
28837
|
+
var enabled = !!(text.trim() || anchor.trim() || _skillComposerDependencies.length);
|
|
28789
28838
|
if (btn) {
|
|
28790
28839
|
btn.disabled = !enabled;
|
|
28791
28840
|
btn.style.opacity = enabled ? '1' : '0.55';
|
|
28792
28841
|
btn.style.cursor = enabled ? 'pointer' : 'not-allowed';
|
|
28793
28842
|
}
|
|
28794
28843
|
renderSkillComposerAnchorSummary(text, anchor);
|
|
28844
|
+
renderSkillComposerPreview();
|
|
28795
28845
|
}
|
|
28796
28846
|
|
|
28797
28847
|
function renderSkillComposerAnchorSummary(text, anchor) {
|
|
28798
28848
|
var box = document.getElementById('skill-composer-anchor-summary');
|
|
28799
28849
|
if (!box) return;
|
|
28800
28850
|
var trimmed = String(anchor || '').trim();
|
|
28801
|
-
|
|
28851
|
+
var deps = getSkillComposerDependencies(false);
|
|
28852
|
+
if (!trimmed && deps.length === 0) {
|
|
28802
28853
|
box.innerHTML = '<span style="color:var(--text-muted)">No starting point selected.</span>';
|
|
28803
28854
|
return;
|
|
28804
28855
|
}
|
|
28805
|
-
var
|
|
28806
|
-
var
|
|
28807
|
-
|
|
28808
|
-
|
|
28809
|
-
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
28856
|
+
var html = '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">';
|
|
28857
|
+
for (var i = 0; i < deps.length; i++) {
|
|
28858
|
+
var dep = deps[i];
|
|
28859
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
28860
|
+
html += '<span style="display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:2px 7px;border:1px solid var(--border);border-radius:999px;background:var(--bg-primary);color:var(--text-primary);font-size:10px">'
|
|
28861
|
+
+ '<strong style="color:var(--accent);font-weight:600">' + esc(copy.promptLabel) + '</strong>'
|
|
28862
|
+
+ '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px">' + esc(dep.value) + '</span>'
|
|
28863
|
+
+ '<button type="button" onclick="removeSkillComposerDependency(' + i + ')" title="Remove" style="border:none;background:transparent;color:var(--text-muted);cursor:pointer;padding:0;font-size:12px;line-height:1">×</button>'
|
|
28864
|
+
+ '</span>';
|
|
28865
|
+
}
|
|
28866
|
+
if (trimmed) {
|
|
28867
|
+
var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
|
|
28868
|
+
var tools = inferSkillComposerTools(_skillComposerMode, trimmed);
|
|
28869
|
+
html += '<span style="color:var(--text-muted)">Pending ' + esc(modeCopy.promptLabel) + ':</span>'
|
|
28870
|
+
+ '<code style="font-size:10px;background:var(--bg-primary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);max-width:100%;overflow:hidden;text-overflow:ellipsis">' + esc(trimmed) + '</code>'
|
|
28871
|
+
+ (tools.length ? '<span style="color:var(--text-muted)">adds ' + esc(tools.join(', ')) + '</span>' : '');
|
|
28872
|
+
}
|
|
28873
|
+
html += '</div>';
|
|
28874
|
+
box.innerHTML = html;
|
|
28875
|
+
}
|
|
28876
|
+
|
|
28877
|
+
function getSkillComposerDependencies(includePending) {
|
|
28878
|
+
var deps = _skillComposerDependencies.slice();
|
|
28879
|
+
if (includePending) {
|
|
28880
|
+
var pending = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
|
|
28881
|
+
if (pending) deps.push({ mode: _skillComposerMode || 'outcome', value: pending });
|
|
28882
|
+
}
|
|
28883
|
+
var seen = {};
|
|
28884
|
+
return deps.filter(function(dep) {
|
|
28885
|
+
var key = dep.mode + ':' + dep.value;
|
|
28886
|
+
if (seen[key]) return false;
|
|
28887
|
+
seen[key] = true;
|
|
28888
|
+
return true;
|
|
28889
|
+
});
|
|
28890
|
+
}
|
|
28891
|
+
|
|
28892
|
+
function addSkillComposerDependency() {
|
|
28893
|
+
var input = document.getElementById('skill-composer-anchor');
|
|
28894
|
+
var value = (input && input.value || '').trim();
|
|
28895
|
+
if (!value) return;
|
|
28896
|
+
var mode = _skillComposerMode || 'outcome';
|
|
28897
|
+
var key = mode + ':' + value;
|
|
28898
|
+
var exists = _skillComposerDependencies.some(function(dep) { return dep.mode + ':' + dep.value === key; });
|
|
28899
|
+
if (!exists) _skillComposerDependencies.push({ mode: mode, value: value });
|
|
28900
|
+
if (input) input.value = '';
|
|
28901
|
+
updateSkillComposerDraftState();
|
|
28902
|
+
}
|
|
28903
|
+
|
|
28904
|
+
function removeSkillComposerDependency(index) {
|
|
28905
|
+
if (index < 0 || index >= _skillComposerDependencies.length) return;
|
|
28906
|
+
_skillComposerDependencies.splice(index, 1);
|
|
28907
|
+
updateSkillComposerDraftState();
|
|
28816
28908
|
}
|
|
28817
28909
|
|
|
28818
28910
|
function inferSkillComposerTools(mode, anchor) {
|
|
@@ -28888,12 +28980,19 @@ function slugifySkillTitle(title) {
|
|
|
28888
28980
|
|
|
28889
28981
|
function buildSkillComposerDraftSeed() {
|
|
28890
28982
|
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
28891
|
-
var anchor = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
|
|
28892
28983
|
var mode = _skillComposerMode || 'outcome';
|
|
28893
28984
|
var modeCopy = _skillComposerCopy[mode] || _skillComposerCopy.outcome;
|
|
28985
|
+
var deps = getSkillComposerDependencies(true);
|
|
28986
|
+
var anchor = deps.map(function(dep) {
|
|
28987
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
28988
|
+
return copy.promptLabel + ': ' + dep.value;
|
|
28989
|
+
}).join('; ');
|
|
28894
28990
|
var title = inferSkillComposerTitle(text, anchor);
|
|
28895
28991
|
var name = slugifySkillTitle(title);
|
|
28896
|
-
var tools =
|
|
28992
|
+
var tools = [];
|
|
28993
|
+
for (var di = 0; di < deps.length; di++) {
|
|
28994
|
+
tools = tools.concat(inferSkillComposerTools(deps[di].mode, deps[di].value));
|
|
28995
|
+
}
|
|
28897
28996
|
var hay = (text + ' ' + anchor).toLowerCase();
|
|
28898
28997
|
if (hay.indexOf('salesforce') !== -1) tools = dedupeSkillComposerToolNames(tools.concat(['Bash']));
|
|
28899
28998
|
if (hay.indexOf('dataforseo') !== -1 && tools.indexOf('mcp__dataforseo__*') === -1) tools.push('mcp__dataforseo__*');
|
|
@@ -28909,13 +29008,18 @@ function buildSkillComposerDraftSeed() {
|
|
|
28909
29008
|
if (description.length > 1024) description = description.slice(0, 1024);
|
|
28910
29009
|
|
|
28911
29010
|
var toolLines = tools.length ? tools.map(function(t) { return '- ' + t; }).join('\\n') : '- No explicit allowlist yet; add only the tools this skill truly needs.';
|
|
28912
|
-
var anchorLine =
|
|
29011
|
+
var anchorLine = deps.length
|
|
29012
|
+
? deps.map(function(dep) {
|
|
29013
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
29014
|
+
return '- ' + copy.promptLabel + ': ' + dep.value;
|
|
29015
|
+
}).join('\\n')
|
|
29016
|
+
: '- None selected';
|
|
28913
29017
|
var body = '# ' + title + '\\n\\n'
|
|
28914
29018
|
+ '## Use When\\n\\n'
|
|
28915
29019
|
+ description + '\\n\\n'
|
|
28916
29020
|
+ '## Starting Point\\n\\n'
|
|
28917
29021
|
+ '- Outcome: ' + (text || 'Fill in the exact user goal before saving.') + '\\n'
|
|
28918
|
-
+
|
|
29022
|
+
+ anchorLine + '\\n\\n'
|
|
28919
29023
|
+ '## Required Tools\\n\\n'
|
|
28920
29024
|
+ toolLines + '\\n\\n'
|
|
28921
29025
|
+ '## Procedure\\n\\n'
|
|
@@ -28961,10 +29065,37 @@ function buildSkillComposerDraftSeed() {
|
|
|
28961
29065
|
description: description,
|
|
28962
29066
|
body: body,
|
|
28963
29067
|
tools: tools,
|
|
28964
|
-
note:
|
|
29068
|
+
note: deps.length ? ('Seeded from ' + deps.length + ' starting point' + (deps.length === 1 ? '' : 's') + '.') : 'Seeded from the natural language description.',
|
|
28965
29069
|
};
|
|
28966
29070
|
}
|
|
28967
29071
|
|
|
29072
|
+
function renderSkillComposerPreview() {
|
|
29073
|
+
var box = document.getElementById('skill-composer-preview');
|
|
29074
|
+
if (!box) return;
|
|
29075
|
+
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
29076
|
+
var deps = getSkillComposerDependencies(true);
|
|
29077
|
+
if (!text && deps.length === 0) {
|
|
29078
|
+
box.innerHTML = '<span style="color:var(--text-muted)">Preview appears here as you describe the skill or add tools.</span>';
|
|
29079
|
+
return;
|
|
29080
|
+
}
|
|
29081
|
+
var seed = buildSkillComposerDraftSeed();
|
|
29082
|
+
var depHtml = deps.length
|
|
29083
|
+
? deps.map(function(dep) {
|
|
29084
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
29085
|
+
return '<span style="display:inline-flex;margin:0 4px 4px 0;padding:1px 6px;border:1px solid var(--border);border-radius:999px;background:var(--bg-secondary);font-size:10px"><strong style="color:var(--accent);margin-right:4px">' + esc(copy.promptLabel) + '</strong>' + esc(dep.value) + '</span>';
|
|
29086
|
+
}).join('')
|
|
29087
|
+
: '<span style="color:var(--text-muted)">No explicit dependencies yet.</span>';
|
|
29088
|
+
var toolsHtml = seed.tools.length
|
|
29089
|
+
? seed.tools.map(function(t) { return '<code style="font-size:10px;background:var(--bg-secondary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);margin-right:4px">' + esc(t) + '</code>'; }).join('')
|
|
29090
|
+
: '<span style="color:var(--text-muted)">inherits default runtime surface</span>';
|
|
29091
|
+
box.innerHTML =
|
|
29092
|
+
'<div style="font-weight:600;color:var(--text-primary);margin-bottom:4px">' + esc(seed.title) + '</div>' +
|
|
29093
|
+
'<div style="color:var(--text-muted);margin-bottom:8px">' + esc(seed.name) + '</div>' +
|
|
29094
|
+
'<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Dependencies</strong><br>' + depHtml + '</div>' +
|
|
29095
|
+
'<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Tools</strong><br>' + toolsHtml + '</div>' +
|
|
29096
|
+
'<div><strong style="color:var(--text-secondary)">Trigger description</strong><br>' + esc(seed.description.slice(0, 260)) + (seed.description.length > 260 ? '...' : '') + '</div>';
|
|
29097
|
+
}
|
|
29098
|
+
|
|
28968
29099
|
async function hydrateSkillComposerOptions() {
|
|
28969
29100
|
if (_skillComposerOptionsCache) return _skillComposerOptionsCache;
|
|
28970
29101
|
var cache = { tool: [], cli: [], project: [], memory: [] };
|
|
@@ -29062,15 +29193,21 @@ async function renderSkillComposerOptions() {
|
|
|
29062
29193
|
|
|
29063
29194
|
function buildSkillComposerPrompt() {
|
|
29064
29195
|
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
29065
|
-
var
|
|
29066
|
-
var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
|
|
29196
|
+
var deps = getSkillComposerDependencies(true);
|
|
29067
29197
|
var lines = [
|
|
29068
29198
|
'Use skill-creator principles to draft a Clementine skill in Skill Studio.',
|
|
29069
29199
|
'',
|
|
29070
29200
|
'Outcome: ' + (text || '(ask me for the outcome before drafting)'),
|
|
29071
|
-
'Starting
|
|
29201
|
+
'Starting points:',
|
|
29072
29202
|
];
|
|
29073
|
-
if (
|
|
29203
|
+
if (deps.length) {
|
|
29204
|
+
for (var i = 0; i < deps.length; i++) {
|
|
29205
|
+
var copy = _skillComposerCopy[deps[i].mode] || _skillComposerCopy.outcome;
|
|
29206
|
+
lines.push('- ' + copy.promptLabel + ': ' + deps[i].value);
|
|
29207
|
+
}
|
|
29208
|
+
} else {
|
|
29209
|
+
lines.push('- None selected');
|
|
29210
|
+
}
|
|
29074
29211
|
lines.push(
|
|
29075
29212
|
'',
|
|
29076
29213
|
'Draft rules:',
|
|
@@ -29087,6 +29224,17 @@ function startSkillComposerDraft() {
|
|
|
29087
29224
|
openCreateSkillModalFromComposer({ draft: true });
|
|
29088
29225
|
}
|
|
29089
29226
|
|
|
29227
|
+
function startSkillComposerChat() {
|
|
29228
|
+
var prompt = buildSkillComposerPrompt();
|
|
29229
|
+
if (typeof askClementineWith !== 'function') {
|
|
29230
|
+
toast('Chat is not ready yet. Try again after the dashboard finishes loading.', 'error');
|
|
29231
|
+
return;
|
|
29232
|
+
}
|
|
29233
|
+
closeSkillStudio({ silent: true });
|
|
29234
|
+
askClementineWith(prompt, { autoSend: false });
|
|
29235
|
+
toast('Skill-creator prompt loaded in chat. Press send when you are ready.', 'info');
|
|
29236
|
+
}
|
|
29237
|
+
|
|
29090
29238
|
function openCreateSkillModalFromComposer(opts) {
|
|
29091
29239
|
opts = opts || {};
|
|
29092
29240
|
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
@@ -29096,6 +29244,7 @@ function openCreateSkillModalFromComposer(opts) {
|
|
|
29096
29244
|
return;
|
|
29097
29245
|
}
|
|
29098
29246
|
var seed = buildSkillComposerDraftSeed();
|
|
29247
|
+
closeSkillStudio({ silent: true });
|
|
29099
29248
|
_openSkillModal({ mode: 'create', prefill: seed });
|
|
29100
29249
|
if (seed.note && typeof toast === 'function') toast(seed.note, 'success');
|
|
29101
29250
|
}
|
|
@@ -29876,7 +30025,7 @@ async function _openSkillModal(opts) {
|
|
|
29876
30025
|
+ '<strong style="color:var(--text-secondary);font-weight:600">Format:</strong> '
|
|
29877
30026
|
+ '<code style="font-size:10px;background:var(--bg-secondary);padding:1px 4px;border-radius:3px">[WHAT it does] + [WHEN to use it] + [trigger phrases]</code>'
|
|
29878
30027
|
+ ' · under 1024 chars · no <code style="font-size:10px">< ></code> · '
|
|
29879
|
-
+ '<a href="javascript:void(0)" onclick="
|
|
30028
|
+
+ '<a href="javascript:void(0)" onclick="askSkillCreatorForDescription()" style="color:var(--accent);text-decoration:none">use skill-creator</a>'
|
|
29880
30029
|
+ '</div>'
|
|
29881
30030
|
+ '<textarea id="skill-modal-desc" rows="2" oninput="updateSkillModalCounters()" placeholder="Example: Analyzes Outlook emails and drafts triage replies. Use when user asks to triage email or mentions inbox cleanup." style="width:100%;padding:8px 10px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);margin-bottom:12px;font-family:inherit;resize:vertical"></textarea>'
|
|
29882
30031
|
+ '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;font-weight:500">Allowed tools <span style="color:var(--text-muted)">(comma-separated, leave blank for default)</span></label>'
|
|
@@ -29896,7 +30045,9 @@ async function _openSkillModal(opts) {
|
|
|
29896
30045
|
+ '</div>';
|
|
29897
30046
|
document.body.appendChild(modal);
|
|
29898
30047
|
}
|
|
29899
|
-
document.getElementById('skill-modal-heading').textContent = opts.mode === 'edit'
|
|
30048
|
+
document.getElementById('skill-modal-heading').textContent = opts.mode === 'edit'
|
|
30049
|
+
? 'Edit skill: ' + nameVal
|
|
30050
|
+
: (prefill.note ? 'Review skill draft' : 'Manual skill editor');
|
|
29900
30051
|
document.getElementById('skill-modal-original-name').value = opts.mode === 'edit' ? nameVal : '';
|
|
29901
30052
|
document.getElementById('skill-modal-name').value = nameVal;
|
|
29902
30053
|
document.getElementById('skill-modal-name').disabled = opts.mode === 'edit';
|
|
@@ -29917,7 +30068,8 @@ async function _openSkillModal(opts) {
|
|
|
29917
30068
|
var errEl = document.getElementById('skill-modal-error');
|
|
29918
30069
|
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
|
29919
30070
|
modal.style.display = 'flex';
|
|
29920
|
-
document.getElementById('skill-modal-
|
|
30071
|
+
var initialFocus = prefill.note ? document.getElementById('skill-modal-desc') : document.getElementById('skill-modal-name');
|
|
30072
|
+
if (initialFocus) initialFocus.focus();
|
|
29921
30073
|
if (typeof updateSkillModalCounters === 'function') updateSkillModalCounters();
|
|
29922
30074
|
if (typeof renderSkillModalToolsPreview === 'function') renderSkillModalToolsPreview();
|
|
29923
30075
|
// 1.18.168 — render the compact optional template seed in create mode only.
|
|
@@ -34514,6 +34666,54 @@ function renderMd(text) {
|
|
|
34514
34666
|
return s;
|
|
34515
34667
|
}
|
|
34516
34668
|
|
|
34669
|
+
function extractBackgroundOfferId(text) {
|
|
34670
|
+
var m = String(text || '').match(/Background offer:\s*(bo-[a-z0-9]+-[a-z0-9]{3,10})/i);
|
|
34671
|
+
return m ? m[1] : '';
|
|
34672
|
+
}
|
|
34673
|
+
|
|
34674
|
+
function appendBackgroundOfferActions(bubble, text) {
|
|
34675
|
+
if (!bubble || bubble.querySelector('.chat-bg-offer-actions')) return;
|
|
34676
|
+
var id = extractBackgroundOfferId(text);
|
|
34677
|
+
if (!id) return;
|
|
34678
|
+
var panel = document.createElement('div');
|
|
34679
|
+
panel.className = 'chat-bg-offer-actions';
|
|
34680
|
+
panel.style.cssText = 'margin-top:10px;display:flex;gap:6px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--border)';
|
|
34681
|
+
panel.innerHTML =
|
|
34682
|
+
'<button class="btn-sm btn-primary" data-bg-action="accept" style="font-size:11px;padding:5px 10px">Run in background</button>' +
|
|
34683
|
+
'<button class="btn-sm" data-bg-action="inline" style="font-size:11px;padding:5px 10px">Run inline anyway</button>' +
|
|
34684
|
+
'<button class="btn-sm" data-bg-action="skill" style="font-size:11px;padding:5px 10px">Save as skill first</button>' +
|
|
34685
|
+
'<button class="btn-sm" data-bg-action="dismiss" style="font-size:11px;padding:5px 10px;color:var(--text-muted)">Dismiss</button>';
|
|
34686
|
+
panel.querySelector('[data-bg-action="accept"]').onclick = function() { acceptBackgroundOffer(id, panel); };
|
|
34687
|
+
panel.querySelector('[data-bg-action="inline"]').onclick = function() { askClementineWith('run inline ' + id); };
|
|
34688
|
+
panel.querySelector('[data-bg-action="skill"]').onclick = function() { askClementineWith('save skill ' + id); };
|
|
34689
|
+
panel.querySelector('[data-bg-action="dismiss"]').onclick = function() { dismissBackgroundOffer(id, panel); };
|
|
34690
|
+
bubble.appendChild(panel);
|
|
34691
|
+
}
|
|
34692
|
+
|
|
34693
|
+
async function acceptBackgroundOffer(id, panel) {
|
|
34694
|
+
if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--text-muted)">Queueing background task...</span>';
|
|
34695
|
+
try {
|
|
34696
|
+
var r = await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/accept', { method: 'POST' });
|
|
34697
|
+
var d = await r.json();
|
|
34698
|
+
if (!r.ok || !d.ok) {
|
|
34699
|
+
if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">' + esc(d.error || 'Failed to queue task') + '</span>';
|
|
34700
|
+
return;
|
|
34701
|
+
}
|
|
34702
|
+
if (panel) panel.innerHTML = '<div style="font-size:12px;line-height:1.45;color:var(--text-secondary)">' + renderMd(d.response || 'Background task queued.') + '</div>';
|
|
34703
|
+
toast('Background task queued', 'success');
|
|
34704
|
+
setTimeout(refreshAll, 500);
|
|
34705
|
+
} catch (err) {
|
|
34706
|
+
if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">Failed: ' + esc(String(err)) + '</span>';
|
|
34707
|
+
}
|
|
34708
|
+
}
|
|
34709
|
+
|
|
34710
|
+
async function dismissBackgroundOffer(id, panel) {
|
|
34711
|
+
try {
|
|
34712
|
+
await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
|
|
34713
|
+
} catch (_) { /* best-effort */ }
|
|
34714
|
+
if (panel) panel.remove();
|
|
34715
|
+
}
|
|
34716
|
+
|
|
34517
34717
|
let chatHistory = [];
|
|
34518
34718
|
async function sendChat() {
|
|
34519
34719
|
const input = document.getElementById('chat-input');
|
|
@@ -34621,6 +34821,7 @@ async function sendChat() {
|
|
|
34621
34821
|
finalText = evt.response || finalText || 'No response';
|
|
34622
34822
|
finalTrace = evt.trace || null;
|
|
34623
34823
|
renderAssistantText(finalText);
|
|
34824
|
+
appendBackgroundOfferActions(asstBubble, finalText);
|
|
34624
34825
|
} else if (evt.type === 'error') {
|
|
34625
34826
|
throw new Error(evt.error || 'Stream error');
|
|
34626
34827
|
}
|
|
@@ -35220,7 +35421,7 @@ document.addEventListener('click', function(e) {
|
|
|
35220
35421
|
// Back-compat shim — older call sites still reference loadProfiles().
|
|
35221
35422
|
function loadProfiles() { return refreshChatAgentPicker(); }
|
|
35222
35423
|
|
|
35223
|
-
// ── Skill Studio — opens the
|
|
35424
|
+
// ── Skill Studio — opens the natural-language composer as a real modal ──────────
|
|
35224
35425
|
|
|
35225
35426
|
function openSkillStudio() {
|
|
35226
35427
|
navigateTo('skills');
|
|
@@ -35228,15 +35429,56 @@ function openSkillStudio() {
|
|
|
35228
35429
|
try { initSkillComposer(); } catch (_) { /* non-fatal */ }
|
|
35229
35430
|
var composer = document.getElementById('skill-composer');
|
|
35230
35431
|
var input = document.getElementById('skill-composer-text');
|
|
35231
|
-
|
|
35232
|
-
if (
|
|
35233
|
-
|
|
35234
|
-
|
|
35432
|
+
var modal = document.getElementById('skill-studio-modal');
|
|
35433
|
+
if (!modal) {
|
|
35434
|
+
modal = document.createElement('div');
|
|
35435
|
+
modal.id = 'skill-studio-modal';
|
|
35436
|
+
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.42);z-index:1050;display:none;align-items:center;justify-content:center;padding:20px';
|
|
35437
|
+
modal.innerHTML =
|
|
35438
|
+
'<div style="width:min(1040px,96vw);max-height:92vh;background:var(--bg-primary);border:1px solid var(--border);border-radius:10px;box-shadow:0 18px 56px rgba(0,0,0,0.35);display:flex;flex-direction:column;overflow:hidden">'
|
|
35439
|
+
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:16px 20px;border-bottom:1px solid var(--border);background:var(--bg-secondary)">'
|
|
35440
|
+
+ '<div style="min-width:0">'
|
|
35441
|
+
+ '<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:3px">Skill Studio</div>'
|
|
35442
|
+
+ '<div style="font-size:12px;color:var(--text-muted);line-height:1.45">Start with the outcome. Add MCP, CLI, project, or memory anchors only when they are real dependencies. Review the generated SKILL.md before saving.</div>'
|
|
35443
|
+
+ '</div>'
|
|
35444
|
+
+ '<button type="button" onclick="closeSkillStudio()" title="Close Skill Studio" style="background:none;border:none;font-size:20px;color:var(--text-muted);cursor:pointer;padding:0 4px;line-height:1">×</button>'
|
|
35445
|
+
+ '</div>'
|
|
35446
|
+
+ '<div id="skill-studio-modal-body" style="padding:18px 20px;overflow:auto;flex:1;min-height:0"></div>'
|
|
35447
|
+
+ '<div style="display:flex;align-items:center;gap:8px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--border);background:var(--bg-secondary)">'
|
|
35448
|
+
+ '<button type="button" onclick="closeSkillStudio()" style="font-size:13px;padding:7px 14px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Keep on page</button>'
|
|
35449
|
+
+ '<button type="button" onclick="startSkillComposerChat()" style="font-size:13px;padding:7px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);cursor:pointer">Build in chat</button>'
|
|
35450
|
+
+ '<button type="button" onclick="startSkillComposerDraft()" class="btn-primary" style="font-size:13px;padding:7px 16px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer">Review draft</button>'
|
|
35451
|
+
+ '</div>'
|
|
35452
|
+
+ '</div>';
|
|
35453
|
+
document.body.appendChild(modal);
|
|
35454
|
+
}
|
|
35455
|
+
var body = document.getElementById('skill-studio-modal-body');
|
|
35456
|
+
if (composer && body && composer.parentElement !== body) {
|
|
35457
|
+
composer.dataset.originalMargin = composer.style.margin || '';
|
|
35458
|
+
body.appendChild(composer);
|
|
35459
|
+
composer.style.margin = '0';
|
|
35235
35460
|
}
|
|
35461
|
+
modal.style.display = 'flex';
|
|
35462
|
+
updateSkillComposerDraftState();
|
|
35236
35463
|
if (input) input.focus();
|
|
35237
35464
|
}, 80);
|
|
35238
35465
|
}
|
|
35239
35466
|
|
|
35467
|
+
function closeSkillStudio(opts) {
|
|
35468
|
+
opts = opts || {};
|
|
35469
|
+
var modal = document.getElementById('skill-studio-modal');
|
|
35470
|
+
var composer = document.getElementById('skill-composer');
|
|
35471
|
+
var home = document.getElementById('skill-composer-home');
|
|
35472
|
+
if (composer && home && composer.parentElement !== home) {
|
|
35473
|
+
home.appendChild(composer);
|
|
35474
|
+
composer.style.margin = composer.dataset.originalMargin || '0 0 16px';
|
|
35475
|
+
}
|
|
35476
|
+
if (modal) modal.style.display = 'none';
|
|
35477
|
+
if (!opts.silent && composer && composer.scrollIntoView) {
|
|
35478
|
+
composer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
35479
|
+
}
|
|
35480
|
+
}
|
|
35481
|
+
|
|
35240
35482
|
function updateBuilderMode() {
|
|
35241
35483
|
var type = (document.getElementById('builder-type') || {}).value || 'skill';
|
|
35242
35484
|
var title = document.getElementById('builder-page-title');
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Manages per-user/channel sessions for conversation continuity.
|
|
6
6
|
*/
|
|
7
7
|
import { PersonalAssistant, type ProjectMeta } from '../agent/assistant.js';
|
|
8
|
-
import type { OnProgressCallback, OnTextCallback, OnToolActivityCallback, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
|
|
8
|
+
import type { BackgroundTask, OnProgressCallback, OnTextCallback, OnToolActivityCallback, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
|
|
9
9
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
10
10
|
import { TeamRouter } from '../agent/team-router.js';
|
|
11
11
|
import { TeamBus } from '../agent/team-bus.js';
|
|
@@ -23,6 +23,7 @@ export declare class Gateway {
|
|
|
23
23
|
private approvalResolvers;
|
|
24
24
|
private approvalCounter;
|
|
25
25
|
private sessions;
|
|
26
|
+
private pendingBackgroundOffers;
|
|
26
27
|
private auditLog;
|
|
27
28
|
private draining;
|
|
28
29
|
/** Side-channel for the metadata of the *most recent* cron run, populated
|
|
@@ -53,6 +54,25 @@ export declare class Gateway {
|
|
|
53
54
|
private isTrustedPersonalSession;
|
|
54
55
|
private runningUnleashedTasks;
|
|
55
56
|
private extractBackgroundTaskId;
|
|
57
|
+
private makeBackgroundOfferId;
|
|
58
|
+
private backgroundAgentForSession;
|
|
59
|
+
private pruneExpiredBackgroundOffers;
|
|
60
|
+
private latestBackgroundOfferForSession;
|
|
61
|
+
private getBackgroundOfferForSession;
|
|
62
|
+
private createBackgroundOffer;
|
|
63
|
+
private queueBackgroundOffer;
|
|
64
|
+
private formatBackgroundQueuedResponse;
|
|
65
|
+
private formatBackgroundOfferResponse;
|
|
66
|
+
acceptBackgroundOffer(sessionKey: string, id: string): {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
response: string;
|
|
69
|
+
task?: BackgroundTask;
|
|
70
|
+
};
|
|
71
|
+
dismissBackgroundOffer(sessionKey: string, id: string): {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
response: string;
|
|
74
|
+
};
|
|
75
|
+
private resolveBackgroundOfferControl;
|
|
56
76
|
private isAgentScopedSession;
|
|
57
77
|
private readUnleashedStatus;
|
|
58
78
|
private taskElapsedMinutes;
|
package/dist/gateway/router.js
CHANGED
|
@@ -16,7 +16,7 @@ import { lanes } from './lanes.js';
|
|
|
16
16
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
17
17
|
import { TeamRouter } from '../agent/team-router.js';
|
|
18
18
|
import { TeamBus } from '../agent/team-bus.js';
|
|
19
|
-
import { listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/background-tasks.js';
|
|
19
|
+
import { createBackgroundTask, listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/background-tasks.js';
|
|
20
20
|
import { applyAssistantExperienceUpdate, detectApprovalReply, detectLocalTurn } from '../agent/local-turn.js';
|
|
21
21
|
import { buildApprovalFollowupPrompt, detectActionExpectation } from '../agent/action-enforcer.js';
|
|
22
22
|
import { updateClementineJson } from '../config/clementine-json.js';
|
|
@@ -36,6 +36,7 @@ import { isLiveUnleashedStatus } from './unleashed-status.js';
|
|
|
36
36
|
import { buildActiveContextSnapshot } from './active-context.js';
|
|
37
37
|
import { markContextEventBySource } from './context-events.js';
|
|
38
38
|
import { EventLog } from './event-log.js';
|
|
39
|
+
import { detectComplexTaskForBackground } from '../agent/complex-task-detector.js';
|
|
39
40
|
export { isLiveUnleashedStatus } from './unleashed-status.js';
|
|
40
41
|
const logger = pino({ name: 'clementine.gateway' });
|
|
41
42
|
const INTERACTIVE_FAILURE_LOG = path.join(BASE_DIR, 'self-improve', 'interactive-failures.jsonl');
|
|
@@ -130,6 +131,7 @@ export class Gateway {
|
|
|
130
131
|
approvalResolvers = new Map();
|
|
131
132
|
approvalCounter = 0;
|
|
132
133
|
sessions = new Map();
|
|
134
|
+
pendingBackgroundOffers = new Map();
|
|
133
135
|
auditLog = [];
|
|
134
136
|
draining = false;
|
|
135
137
|
/** Side-channel for the metadata of the *most recent* cron run, populated
|
|
@@ -245,15 +247,157 @@ export class Gateway {
|
|
|
245
247
|
extractBackgroundTaskId(text) {
|
|
246
248
|
return text.match(BACKGROUND_TASK_ID_RE)?.[0]?.toLowerCase();
|
|
247
249
|
}
|
|
250
|
+
makeBackgroundOfferId() {
|
|
251
|
+
return `bo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
252
|
+
}
|
|
253
|
+
backgroundAgentForSession(sessionKey) {
|
|
254
|
+
return this._agentSlugFromSessionKey(sessionKey) ?? this.getSessionProfile(sessionKey) ?? 'clementine';
|
|
255
|
+
}
|
|
256
|
+
pruneExpiredBackgroundOffers() {
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
for (const [id, offer] of this.pendingBackgroundOffers) {
|
|
259
|
+
if (offer.expiresAt <= now)
|
|
260
|
+
this.pendingBackgroundOffers.delete(id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
latestBackgroundOfferForSession(sessionKey) {
|
|
264
|
+
this.pruneExpiredBackgroundOffers();
|
|
265
|
+
const offers = [...this.pendingBackgroundOffers.values()]
|
|
266
|
+
.filter((offer) => offer.sessionKey === sessionKey)
|
|
267
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
268
|
+
return offers[0];
|
|
269
|
+
}
|
|
270
|
+
getBackgroundOfferForSession(sessionKey, id) {
|
|
271
|
+
this.pruneExpiredBackgroundOffers();
|
|
272
|
+
const offer = this.pendingBackgroundOffers.get(id);
|
|
273
|
+
if (!offer || offer.sessionKey !== sessionKey)
|
|
274
|
+
return undefined;
|
|
275
|
+
return offer;
|
|
276
|
+
}
|
|
277
|
+
createBackgroundOffer(sessionKey, prompt, recommendation) {
|
|
278
|
+
this.pruneExpiredBackgroundOffers();
|
|
279
|
+
const offer = {
|
|
280
|
+
id: this.makeBackgroundOfferId(),
|
|
281
|
+
sessionKey,
|
|
282
|
+
fromAgent: this.backgroundAgentForSession(sessionKey),
|
|
283
|
+
prompt,
|
|
284
|
+
recommendation,
|
|
285
|
+
createdAt: Date.now(),
|
|
286
|
+
expiresAt: Date.now() + 30 * 60_000,
|
|
287
|
+
};
|
|
288
|
+
this.pendingBackgroundOffers.set(offer.id, offer);
|
|
289
|
+
return offer;
|
|
290
|
+
}
|
|
291
|
+
queueBackgroundOffer(offer) {
|
|
292
|
+
const task = createBackgroundTask({
|
|
293
|
+
fromAgent: offer.fromAgent,
|
|
294
|
+
prompt: offer.prompt,
|
|
295
|
+
maxMinutes: offer.recommendation.suggestedMaxMinutes,
|
|
296
|
+
sessionKey: offer.sessionKey,
|
|
297
|
+
});
|
|
298
|
+
this.pendingBackgroundOffers.delete(offer.id);
|
|
299
|
+
logger.info({
|
|
300
|
+
offerId: offer.id,
|
|
301
|
+
taskId: task.id,
|
|
302
|
+
sessionKey: offer.sessionKey,
|
|
303
|
+
fromAgent: offer.fromAgent,
|
|
304
|
+
maxMinutes: task.maxMinutes,
|
|
305
|
+
}, 'Queued background task from chat offer');
|
|
306
|
+
return task;
|
|
307
|
+
}
|
|
308
|
+
formatBackgroundQueuedResponse(task) {
|
|
309
|
+
return [
|
|
310
|
+
`Queued background task **${task.id}**.`,
|
|
311
|
+
'',
|
|
312
|
+
`It will run as **${task.fromAgent}** with a ${task.maxMinutes} minute cap.`,
|
|
313
|
+
`Use \`status ${task.id}\` or check the dashboard Background Tasks panel for progress.`,
|
|
314
|
+
].join('\n');
|
|
315
|
+
}
|
|
316
|
+
formatBackgroundOfferResponse(offer) {
|
|
317
|
+
const lines = [
|
|
318
|
+
'This looks like long-running, multi-tool work. I recommend running it in the background so chat does not go stale.',
|
|
319
|
+
'',
|
|
320
|
+
'**Plan**',
|
|
321
|
+
...offer.recommendation.plan.map((step, idx) => `${idx + 1}. ${step}`),
|
|
322
|
+
'',
|
|
323
|
+
`**Why background:** ${offer.recommendation.reasons.join('; ')}.`,
|
|
324
|
+
`**Estimated cap:** ${offer.recommendation.suggestedMaxMinutes} minutes.`,
|
|
325
|
+
`**Background offer:** ${offer.id}`,
|
|
326
|
+
'',
|
|
327
|
+
`Reply \`run background ${offer.id}\` to queue it, \`run inline ${offer.id}\` to run it in this chat, or \`save skill ${offer.id}\` to make it reusable first.`,
|
|
328
|
+
];
|
|
329
|
+
return lines.join('\n');
|
|
330
|
+
}
|
|
331
|
+
acceptBackgroundOffer(sessionKey, id) {
|
|
332
|
+
const offer = this.getBackgroundOfferForSession(sessionKey, id);
|
|
333
|
+
if (!offer) {
|
|
334
|
+
return { ok: false, response: `I could not find an active background offer for ${id}. It may have expired.` };
|
|
335
|
+
}
|
|
336
|
+
const task = this.queueBackgroundOffer(offer);
|
|
337
|
+
return { ok: true, response: this.formatBackgroundQueuedResponse(task), task };
|
|
338
|
+
}
|
|
339
|
+
dismissBackgroundOffer(sessionKey, id) {
|
|
340
|
+
const offer = this.getBackgroundOfferForSession(sessionKey, id);
|
|
341
|
+
if (!offer)
|
|
342
|
+
return { ok: false, response: `No active background offer found for ${id}.` };
|
|
343
|
+
this.pendingBackgroundOffers.delete(id);
|
|
344
|
+
return { ok: true, response: `Dismissed background offer ${id}.` };
|
|
345
|
+
}
|
|
346
|
+
resolveBackgroundOfferControl(sessionKey, text) {
|
|
347
|
+
const normalized = text.trim().toLowerCase();
|
|
348
|
+
const explicitOfferId = text.match(/\bbo-[a-z0-9]+-[a-z0-9]{3,10}\b/i)?.[0]?.toLowerCase();
|
|
349
|
+
const offer = explicitOfferId
|
|
350
|
+
? this.getBackgroundOfferForSession(sessionKey, explicitOfferId)
|
|
351
|
+
: this.latestBackgroundOfferForSession(sessionKey);
|
|
352
|
+
if (!offer)
|
|
353
|
+
return null;
|
|
354
|
+
if (/^(run|start|queue|approve|yes|go|do it).{0,30}\bbackground\b/i.test(normalized)
|
|
355
|
+
|| /^run it in the background\b/i.test(normalized)
|
|
356
|
+
|| /^background\b/i.test(normalized)
|
|
357
|
+
|| /^(yes|yep|approved?|go|do it|please do|start it)$/i.test(normalized)) {
|
|
358
|
+
const task = this.queueBackgroundOffer(offer);
|
|
359
|
+
return {
|
|
360
|
+
response: this.formatBackgroundQueuedResponse(task),
|
|
361
|
+
executionMode: 'background_queued',
|
|
362
|
+
backgroundTaskId: task.id,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (/^run.{0,30}\binline\b/i.test(normalized) || /^run inline\b/i.test(normalized)) {
|
|
366
|
+
this.pendingBackgroundOffers.delete(offer.id);
|
|
367
|
+
return { inlineText: offer.prompt, executionMode: 'inline' };
|
|
368
|
+
}
|
|
369
|
+
if (/^(save|create|make|teach).{0,30}\bskill\b/i.test(normalized)) {
|
|
370
|
+
this.pendingBackgroundOffers.delete(offer.id);
|
|
371
|
+
return {
|
|
372
|
+
skillText: [
|
|
373
|
+
'Create a reusable Clementine skill for this workflow using skill-creator principles.',
|
|
374
|
+
'Write a concise Anthropic-compatible folder-form SKILL.md with clear trigger description, required tools/MCP/CLI dependencies, procedure, success criteria, and failure handling.',
|
|
375
|
+
'',
|
|
376
|
+
'Original workflow request:',
|
|
377
|
+
offer.prompt,
|
|
378
|
+
].join('\n'),
|
|
379
|
+
executionMode: 'inline',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
248
384
|
isAgentScopedSession(sessionKey) {
|
|
249
385
|
return this._agentSlugFromSessionKey(sessionKey) !== undefined;
|
|
250
386
|
}
|
|
251
387
|
readUnleashedStatus(jobName) {
|
|
252
388
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
389
|
+
const candidates = [
|
|
390
|
+
jobName,
|
|
391
|
+
jobName.startsWith('bg-') ? `bg:${jobName}` : '',
|
|
392
|
+
].filter(Boolean);
|
|
393
|
+
for (const candidate of candidates) {
|
|
394
|
+
const safeJob = candidate.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
395
|
+
const statusPath = path.join(BASE_DIR, 'unleashed', safeJob, 'status.json');
|
|
396
|
+
if (existsSync(statusPath)) {
|
|
397
|
+
return JSON.parse(readFileSync(statusPath, 'utf-8'));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
257
401
|
}
|
|
258
402
|
catch {
|
|
259
403
|
return null;
|
|
@@ -300,10 +444,14 @@ export class Gateway {
|
|
|
300
444
|
: (task.status === 'failed' || task.status === 'aborted') && task.error
|
|
301
445
|
? ` Reason: ${this.taskSummary(task.error, 120)}`
|
|
302
446
|
: '';
|
|
303
|
-
|
|
447
|
+
const deliverable = task.status === 'done' && task.deliverableNote
|
|
448
|
+
? ` Deliverable: ${this.taskSummary(task.deliverableNote, 120)}`
|
|
449
|
+
: '';
|
|
450
|
+
return `- ${task.id}: ${task.status}${phase}, ${elapsed} min${cap}. ${taskText}${terminalDetail}${deliverable}`;
|
|
304
451
|
}
|
|
305
452
|
writeUnleashedCancel(jobName) {
|
|
306
|
-
const
|
|
453
|
+
const safeJob = (jobName.startsWith('bg-') ? `bg:${jobName}` : jobName).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
454
|
+
const cancelDir = path.join(BASE_DIR, 'unleashed', safeJob);
|
|
307
455
|
mkdirSync(cancelDir, { recursive: true });
|
|
308
456
|
writeFileSync(path.join(cancelDir, 'CANCEL'), '');
|
|
309
457
|
}
|
|
@@ -1312,6 +1460,9 @@ export class Gateway {
|
|
|
1312
1460
|
actionExpectationSource: actionExpectationForLedger.source,
|
|
1313
1461
|
actionExpectationReason: actionExpectationForLedger.reason,
|
|
1314
1462
|
runId: ledgerRunMetadata.runId,
|
|
1463
|
+
executionMode: ledgerRunMetadata.executionMode,
|
|
1464
|
+
backgroundTaskId: ledgerRunMetadata.backgroundTaskId,
|
|
1465
|
+
skillsApplied: ledgerRunMetadata.skillsApplied,
|
|
1315
1466
|
permissionModeApplied: ledgerRunMetadata.permissionModeApplied,
|
|
1316
1467
|
allowedToolsApplied: ledgerRunMetadata.allowedToolsApplied?.slice(0, 80),
|
|
1317
1468
|
builtinToolsApplied: ledgerRunMetadata.builtinToolsApplied?.slice(0, 40),
|
|
@@ -1332,7 +1483,8 @@ export class Gateway {
|
|
|
1332
1483
|
});
|
|
1333
1484
|
}
|
|
1334
1485
|
async _handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress, ledgerRunMetadata) {
|
|
1335
|
-
|
|
1486
|
+
let originalText = text;
|
|
1487
|
+
let skipBackgroundOffer = false;
|
|
1336
1488
|
// Per-segment latency capture — emitted as a single 'chat:latency' line
|
|
1337
1489
|
// on the happy path so we can grep/aggregate without parsing many lines.
|
|
1338
1490
|
const tInnerStart = Date.now();
|
|
@@ -1438,6 +1590,30 @@ export class Gateway {
|
|
|
1438
1590
|
}, 'chat:latency');
|
|
1439
1591
|
return localResponse;
|
|
1440
1592
|
}
|
|
1593
|
+
const backgroundControl = this.resolveBackgroundOfferControl(sessionKey, text);
|
|
1594
|
+
if (backgroundControl?.response) {
|
|
1595
|
+
if (ledgerRunMetadata) {
|
|
1596
|
+
ledgerRunMetadata.executionMode = backgroundControl.executionMode;
|
|
1597
|
+
ledgerRunMetadata.backgroundTaskId = backgroundControl.backgroundTaskId;
|
|
1598
|
+
}
|
|
1599
|
+
if (onText) {
|
|
1600
|
+
try {
|
|
1601
|
+
await onText(backgroundControl.response);
|
|
1602
|
+
}
|
|
1603
|
+
catch { /* channel streaming is best-effort */ }
|
|
1604
|
+
}
|
|
1605
|
+
return backgroundControl.response;
|
|
1606
|
+
}
|
|
1607
|
+
if (backgroundControl?.inlineText) {
|
|
1608
|
+
text = backgroundControl.inlineText;
|
|
1609
|
+
originalText = backgroundControl.inlineText;
|
|
1610
|
+
skipBackgroundOffer = true;
|
|
1611
|
+
}
|
|
1612
|
+
else if (backgroundControl?.skillText) {
|
|
1613
|
+
text = backgroundControl.skillText;
|
|
1614
|
+
originalText = backgroundControl.skillText;
|
|
1615
|
+
skipBackgroundOffer = true;
|
|
1616
|
+
}
|
|
1441
1617
|
const approvalFollowupExpected = this.isTrustedPersonalSession(sessionKey)
|
|
1442
1618
|
&& detectApprovalReply(originalText) === true
|
|
1443
1619
|
&& this.assistant.hasRecentApprovalPrompt(sessionKey);
|
|
@@ -1685,6 +1861,46 @@ export class Gateway {
|
|
|
1685
1861
|
const resolvedProfile = profileSlug
|
|
1686
1862
|
? this.getAgentManager().get(profileSlug) ?? undefined
|
|
1687
1863
|
: undefined;
|
|
1864
|
+
const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
|
|
1865
|
+
if (!skipBackgroundOffer && !isBuilderSession && !isInternalMsg && this.isTrustedPersonalSession(sessionKey)) {
|
|
1866
|
+
const recommendation = detectComplexTaskForBackground(text);
|
|
1867
|
+
if (recommendation) {
|
|
1868
|
+
const offer = this.createBackgroundOffer(sessionKey, text, recommendation);
|
|
1869
|
+
if (recommendation.queueImmediately) {
|
|
1870
|
+
const task = this.queueBackgroundOffer(offer);
|
|
1871
|
+
const queued = this.formatBackgroundQueuedResponse(task);
|
|
1872
|
+
if (ledgerRunMetadata) {
|
|
1873
|
+
ledgerRunMetadata.executionMode = 'background_queued';
|
|
1874
|
+
ledgerRunMetadata.backgroundTaskId = task.id;
|
|
1875
|
+
}
|
|
1876
|
+
if (onText) {
|
|
1877
|
+
try {
|
|
1878
|
+
await onText(queued);
|
|
1879
|
+
}
|
|
1880
|
+
catch { /* channel streaming is best-effort */ }
|
|
1881
|
+
}
|
|
1882
|
+
return queued;
|
|
1883
|
+
}
|
|
1884
|
+
const offerText = this.formatBackgroundOfferResponse(offer);
|
|
1885
|
+
if (ledgerRunMetadata) {
|
|
1886
|
+
ledgerRunMetadata.executionMode = 'background_offer';
|
|
1887
|
+
}
|
|
1888
|
+
logger.info({
|
|
1889
|
+
sessionKey,
|
|
1890
|
+
offerId: offer.id,
|
|
1891
|
+
score: recommendation.score,
|
|
1892
|
+
reasons: recommendation.reasons,
|
|
1893
|
+
maxMinutes: recommendation.suggestedMaxMinutes,
|
|
1894
|
+
}, 'Offering background execution for complex chat request');
|
|
1895
|
+
if (onText) {
|
|
1896
|
+
try {
|
|
1897
|
+
await onText(offerText);
|
|
1898
|
+
}
|
|
1899
|
+
catch { /* channel streaming is best-effort */ }
|
|
1900
|
+
}
|
|
1901
|
+
return offerText;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1688
1904
|
const hygiene = assessGatewayContextHygiene({
|
|
1689
1905
|
sessionKey: effectiveSessionKey,
|
|
1690
1906
|
textChars: enrichedText.length,
|
|
@@ -1804,7 +2020,6 @@ export class Gateway {
|
|
|
1804
2020
|
// the agent only emits json-artifact blocks. Strip everything
|
|
1805
2021
|
// expensive; keep just SDK session resume so multi-turn
|
|
1806
2022
|
// artifact iteration sees its own prior turns.
|
|
1807
|
-
const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
|
|
1808
2023
|
// ── Skill auto-match (1.18.170) ─────────────────────────────
|
|
1809
2024
|
// Match the user's message against the skill catalog (auto-
|
|
1810
2025
|
// skills + user-authored). Top-3 matches above score ≥ 4 inform:
|
|
@@ -1903,7 +2118,6 @@ export class Gateway {
|
|
|
1903
2118
|
...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
|
|
1904
2119
|
onText: wrappedOnText,
|
|
1905
2120
|
onToolActivity: ({ tool, input }) => {
|
|
1906
|
-
toolActivityCount++;
|
|
1907
2121
|
if (wrappedOnToolActivity) {
|
|
1908
2122
|
return wrappedOnToolActivity(tool, input);
|
|
1909
2123
|
}
|
|
@@ -1913,6 +2127,12 @@ export class Gateway {
|
|
|
1913
2127
|
});
|
|
1914
2128
|
if (ledgerRunMetadata) {
|
|
1915
2129
|
ledgerRunMetadata.runId = runAgentResult.runId;
|
|
2130
|
+
ledgerRunMetadata.executionMode = ledgerRunMetadata.executionMode ?? 'inline';
|
|
2131
|
+
ledgerRunMetadata.skillsApplied = resolvedSkills?.matches.map((m) => ({
|
|
2132
|
+
name: m.name,
|
|
2133
|
+
source: 'auto',
|
|
2134
|
+
score: m.score,
|
|
2135
|
+
}));
|
|
1916
2136
|
ledgerRunMetadata.permissionModeApplied = runAgentResult.permissionMode;
|
|
1917
2137
|
ledgerRunMetadata.allowedToolsApplied = runAgentResult.allowedToolsApplied;
|
|
1918
2138
|
ledgerRunMetadata.builtinToolsApplied = runAgentResult.builtinToolsApplied;
|
|
@@ -17,6 +17,13 @@ export interface TurnLedgerEntry {
|
|
|
17
17
|
actionExpectationSource?: string;
|
|
18
18
|
actionExpectationReason?: string;
|
|
19
19
|
runId?: string;
|
|
20
|
+
executionMode?: 'inline' | 'background_offer' | 'background_queued';
|
|
21
|
+
backgroundTaskId?: string;
|
|
22
|
+
skillsApplied?: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
source: 'auto' | 'pinned';
|
|
25
|
+
score?: number;
|
|
26
|
+
}>;
|
|
20
27
|
permissionModeApplied?: string;
|
|
21
28
|
allowedToolsApplied?: string[];
|
|
22
29
|
builtinToolsApplied?: string[];
|
|
@@ -40,7 +40,12 @@ export function formatLastTurnLedger(sessionKey, baseDir = BASE_DIR) {
|
|
|
40
40
|
? `Tools used: ${last.toolCallsMade} (${last.toolNames.slice(0, 6).join(', ')}${last.toolNames.length > 6 ? ', ...' : ''}).`
|
|
41
41
|
: 'Tools used: none.';
|
|
42
42
|
const execution = last.permissionModeApplied || last.mcpServersApplied?.length
|
|
43
|
-
? `Execution: ${last.permissionModeApplied ?? 'unknown'}${last.mcpServersApplied?.length ? `; MCP: ${last.mcpServersApplied.slice(0, 6).join(', ')}${last.mcpServersApplied.length > 6 ? ', ...' : ''}` : ''}.`
|
|
43
|
+
? `Execution: ${last.executionMode ?? 'inline'}; ${last.permissionModeApplied ?? 'unknown'}${last.mcpServersApplied?.length ? `; MCP: ${last.mcpServersApplied.slice(0, 6).join(', ')}${last.mcpServersApplied.length > 6 ? ', ...' : ''}` : ''}.`
|
|
44
|
+
: last.executionMode
|
|
45
|
+
? `Execution: ${last.executionMode}${last.backgroundTaskId ? ` (${last.backgroundTaskId})` : ''}.`
|
|
46
|
+
: '';
|
|
47
|
+
const skills = last.skillsApplied?.length
|
|
48
|
+
? `Skills: ${last.skillsApplied.slice(0, 5).map((s) => s.name).join(', ')}${last.skillsApplied.length > 5 ? ', ...' : ''}.`
|
|
44
49
|
: '';
|
|
45
50
|
const response = last.responsePreview
|
|
46
51
|
? `Last response: "${last.responsePreview.replace(/\s+/g, ' ').slice(0, 240)}${last.responsePreview.length > 240 ? '...' : ''}"`
|
|
@@ -54,6 +59,7 @@ export function formatLastTurnLedger(sessionKey, baseDir = BASE_DIR) {
|
|
|
54
59
|
`Toolset: ${last.toolset ?? 'auto'}.`,
|
|
55
60
|
`Policy: ${last.policyReason ?? 'unknown'}; tools ${last.toolsEnabled ? 'enabled' : 'disabled'}.`,
|
|
56
61
|
execution,
|
|
62
|
+
skills,
|
|
57
63
|
response,
|
|
58
64
|
].filter(Boolean).join('\n');
|
|
59
65
|
}
|
|
@@ -65,6 +65,10 @@ export function registerBackgroundTaskTools(server) {
|
|
|
65
65
|
lines.push('');
|
|
66
66
|
lines.push(`Result:\n${task.result}`);
|
|
67
67
|
}
|
|
68
|
+
if (task.resultPath) {
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(`Full result: ${task.resultPath}`);
|
|
71
|
+
}
|
|
68
72
|
if (task.deliverableNote) {
|
|
69
73
|
lines.push('');
|
|
70
74
|
lines.push(`Deliverable: ${task.deliverableNote}`);
|
package/dist/types.d.ts
CHANGED