feat: add LexAI status bar and suggestion panel
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:
john kevin asprec
2026-08-13 18:06:45 +08:00
parent fc19ead0a7
commit 8bc529ef2d
84 changed files with 10025 additions and 662 deletions

View 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);