feat: add LexAI status bar and suggestion panel
Some checks failed
CI — Test & Build / Test & Build (push) Has been cancelled
Some checks failed
CI — Test & Build / Test & Build (push) Has been cancelled
- Implemented a status bar item for LexAI with dynamic status updates (ready, processing, notReady). - Created a suggestion panel for displaying and interacting with AI-generated suggestions. - Added functionality for accepting, regenerating, and discarding suggestions within the suggestion zone. - Introduced configuration options for writing style, prompt patterns, personas, and formats. - Integrated progress indicators for long-running tasks and improved user feedback. - Established TypeScript configuration for the vscode package.
This commit is contained in:
148
.cursor/hooks/session-context.mjs
Normal file
148
.cursor/hooks/session-context.mjs
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* sessionStart hook — inject the facts a session should never have to ask for.
|
||||
*
|
||||
* 1. Are the model lanes bound, and is the picker running the model we recorded?
|
||||
* 2. What did the last session leave as the next action?
|
||||
* 3. Is any capped context file over its cap right now?
|
||||
*
|
||||
* All three are deterministic file/state checks. The point of doing them here rather
|
||||
* than in a prompt is that a rule asking the model to "check the caps" is a request;
|
||||
* this is an observation.
|
||||
*
|
||||
* Contract: stdin is the sessionStart JSON payload; stdout is
|
||||
* {"additional_context": "..."}. This hook is fire-and-forget — Cursor logs the
|
||||
* response but never blocks session creation on it. It fails open by design:
|
||||
* any error prints an empty object and exits 0.
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const readAll = () =>
|
||||
new Promise((resolve) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (c) => (data += c));
|
||||
process.stdin.on("end", () => resolve(data));
|
||||
setTimeout(() => resolve(data), 2000).unref();
|
||||
});
|
||||
|
||||
const read = (p) => {
|
||||
try {
|
||||
return existsSync(p) ? readFileSync(p, "utf8") : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Real content lines: not blank, not a heading, not the file's instructional blockquote,
|
||||
* not a table rule, not an HTML comment, and not one of the shipped `_None yet._` /
|
||||
* `[placeholder]` template rows. Counting boilerplate would report a freshly installed
|
||||
* kit as already consuming its caps.
|
||||
*/
|
||||
const isPlaceholder = (l) =>
|
||||
/^[-|*\s]*_?(none|no )/i.test(l) ||
|
||||
/^\|?\s*_?\[/.test(l) ||
|
||||
/^-\s*\*\(/.test(l) ||
|
||||
/^\|\s*\[/.test(l);
|
||||
|
||||
const contentLines = (text) =>
|
||||
text
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(
|
||||
(l) =>
|
||||
l &&
|
||||
!l.startsWith("#") &&
|
||||
!l.startsWith(">") &&
|
||||
!l.startsWith("<!--") &&
|
||||
!l.startsWith("*") &&
|
||||
!/^\|?[\s|:-]+\|?$/.test(l) &&
|
||||
!isPlaceholder(l),
|
||||
);
|
||||
|
||||
function main(payload) {
|
||||
const root = payload?.workspace_roots?.[0] ?? process.cwd();
|
||||
const notes = [];
|
||||
|
||||
// ---- 1. model routing -------------------------------------------------
|
||||
const routing = read(join(root, "docs", "MODEL_ROUTING.md"));
|
||||
if (!routing) {
|
||||
notes.push("Routing: docs/MODEL_ROUTING.md is missing — this kit is not fully installed.");
|
||||
} else if (/\[LEAD\]|\[STRONG\]|\[MID\]|\[FAST\]/.test(routing)) {
|
||||
notes.push(
|
||||
"Routing: UNBOUND. Every subagent is still `model: inherit`, so each one costs what this session costs. " +
|
||||
"Run /model-routing before delegating.",
|
||||
);
|
||||
} else {
|
||||
const row = routing.match(/^\|\s*\*\*lead\*\*\s*\|\s*`?([^`|]+?)`?\s*\|/m);
|
||||
// Exact match after normalising case and separators. Substring matching would treat
|
||||
// composer-2.5 and composer-2.5-fast — two different lanes in this kit's own table —
|
||||
// as the same model and stay silent on a real downgrade.
|
||||
const norm = (s) => s.toLowerCase().replace(/[\s_()]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||
const recorded = row?.[1]?.trim();
|
||||
const actual = (payload?.model_id || payload?.model || "").trim();
|
||||
if (recorded && actual && norm(recorded) !== norm(actual)) {
|
||||
notes.push(
|
||||
`Routing: lead-model drift — docs/MODEL_ROUTING.md records "${recorded}" but this session is running "${actual}". ` +
|
||||
"Either switch the picker or re-run /model-routing; do not silently proceed on the wrong lane.",
|
||||
);
|
||||
} else if (recorded) {
|
||||
notes.push(`Routing: bound (lead = ${recorded}).`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 2. handoff -------------------------------------------------------
|
||||
const handoff = read(join(root, "docs", "HANDOFF.md"));
|
||||
if (handoff) {
|
||||
const next = handoff.match(/Next smallest action:\*{0,2}\s*(.+)/);
|
||||
if (next) notes.push(`Handoff next action: ${next[1].trim()}`);
|
||||
}
|
||||
|
||||
// ---- 3. context caps --------------------------------------------------
|
||||
const over = [];
|
||||
const memory = read(join(root, "docs", "MEMORY.md"));
|
||||
if (memory) {
|
||||
const n = contentLines(memory).filter((l) => l.startsWith("-") || l.startsWith("|")).length;
|
||||
if (n > 60) over.push(`docs/MEMORY.md ${n}/60 entry lines`);
|
||||
}
|
||||
if (handoff) {
|
||||
const n = contentLines(handoff).length;
|
||||
if (n > 25) over.push(`docs/HANDOFF.md ${n}/25 lines`);
|
||||
}
|
||||
const tasks = read(join(root, "docs", "TASKS.md"));
|
||||
if (tasks) {
|
||||
const active = tasks.split(/^##\s+/m).find((s) => /^Active\b/i.test(s));
|
||||
if (active) {
|
||||
const n = (active.match(/^###\s+T-/gm) || []).length;
|
||||
if (n > 7) over.push(`docs/TASKS.md Active ${n}/7 contracts`);
|
||||
}
|
||||
}
|
||||
const agents = read(join(root, "AGENTS.md"));
|
||||
if (agents) {
|
||||
const section = agents.split(/^##\s+Lessons\s*$/m)[1];
|
||||
if (section) {
|
||||
const rules = section.split(/^##\s/m)[0].split("\n").filter((l) => /^\s*-\s+\S/.test(l) && !l.includes("*(Add one-line"));
|
||||
if (rules.length > 12) over.push(`AGENTS.md ## Lessons ${rules.length}/12 rules`);
|
||||
}
|
||||
}
|
||||
if (over.length) {
|
||||
notes.push(`Context caps EXCEEDED: ${over.join(" · ")}. Run /memory-sync before adding anything.`);
|
||||
}
|
||||
|
||||
return notes.length
|
||||
? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` }
|
||||
: {};
|
||||
}
|
||||
|
||||
const raw = await readAll();
|
||||
let out = {};
|
||||
try {
|
||||
out = main(raw ? JSON.parse(raw) : {});
|
||||
} catch {
|
||||
out = {};
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user