evOo-local/apps/web/src/lib/csv-preview.ts
cynarAI e28c3acf02
feat: let the pane beside a conversation follow the bot's work (#123)
The live screen opens wide beside the chat while the bot works on it or asks
for a takeover, a file the bot presents as its result opens next to the
conversation, and a question that scrolled away gets a "Needs you" pill back
to it. Shared core rules derive the focus from signals clients already have
(pointer events, takeover requests, a new `present` flag on attach_file,
pending asks), move the pane only on focus edges, never replace a pane the
user opened, and learn per bot whether the screen opens on its own. Pane
widths persist per device; mobile shows a pill that opens the computer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7VSRHHzyt2KwoCkUkZ2Gc
2026-10-03 22:23:46 +02:00

44 lines
1.2 KiB
TypeScript

/** Parses enough of RFC 4180 to show a CSV as a table: quoted fields, doubled quotes, CRLF. */
export function parseCsvPreview(
text: string,
maxRows = 500,
): { rows: string[][]; truncated: boolean } {
const rows: string[][] = [];
let row: string[] = [];
let field = "";
let quoted = false;
let index = 0;
const endRow = () => {
row.push(field);
field = "";
if (row.length > 1 || row[0] !== "") rows.push(row);
row = [];
};
while (index < text.length) {
const char = text[index];
if (quoted) {
if (char === '"' && text[index + 1] === '"') {
field += '"';
index += 2;
continue;
}
if (char === '"') quoted = false;
else field += char;
index += 1;
continue;
}
if (char === '"' && field === "") quoted = true;
else if (char === ",") {
row.push(field);
field = "";
} else if (char === "\n" || char === "\r") {
endRow();
if (char === "\r" && text[index + 1] === "\n") index += 1;
if (rows.length > maxRows) break;
} else field += char;
index += 1;
}
if (rows.length <= maxRows && (field !== "" || row.length > 0)) endRow();
return { rows: rows.slice(0, maxRows), truncated: rows.length > maxRows };
}