feat: add pink-purple theme, fix image paste race condition, allow @/commands anywhere in input

- Add Pink Purple theme (hot pink/purple/magenta on dark plum background)
- Fix race condition where clearPastedImages() in input-area ran before
  the async message handler could read the images, silently dropping them
- Allow @ file picker and / command menu to trigger at any cursor position,
  not just when the input is empty
- Update CHANGELOG and README with new changes
This commit is contained in:
2026-02-14 06:39:08 -05:00
parent ddbdb5eb3e
commit 6111530c08
84 changed files with 5643 additions and 1574 deletions

View File

@@ -22,12 +22,58 @@ export interface DangerCheckResult {
}
/**
* Check if a command matches any blocked pattern
* Split a shell command into individual sub-commands on chaining operators.
* Handles &&, ||, ;, and | (pipe). Respects quoted strings.
*/
const splitChainedCommands = (command: string): string[] => {
const parts: string[] = [];
let current = "";
let inSingle = false;
let inDouble = false;
for (let i = 0; i < command.length; i++) {
const ch = command[i];
const next = command[i + 1];
if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; continue; }
if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; continue; }
if (inSingle || inDouble) { current += ch; continue; }
if (ch === "&" && next === "&") { parts.push(current); current = ""; i++; continue; }
if (ch === "|" && next === "|") { parts.push(current); current = ""; i++; continue; }
if (ch === ";") { parts.push(current); current = ""; continue; }
if (ch === "|") { parts.push(current); current = ""; continue; }
current += ch;
}
if (current.trim()) parts.push(current);
return parts.map((p) => p.trim()).filter(Boolean);
};
/**
* Check if a command matches any blocked pattern.
* For chained commands (&&, ||, ;, |), each sub-command is checked individually
* to prevent dangerous commands hidden behind benign ones (e.g. cd /safe && rm -rf /).
*/
export const checkDangerousCommand = (command: string): DangerCheckResult => {
// Normalize command for checking
const normalizedCommand = command.trim();
const subCommands = splitChainedCommands(command);
for (const subCmd of subCommands) {
const normalized = subCmd.trim();
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.pattern.test(normalized)) {
return {
blocked: true,
pattern,
message: formatBlockedMessage(pattern),
};
}
}
}
// Also check the full command in case a pattern targets the chaining itself
const normalizedCommand = command.trim();
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.pattern.test(normalizedCommand)) {
return {