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
44 lines
1.2 KiB
TypeScript
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 };
|
|
}
|