Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

terminal: persistent claude sessions + git identity from config.json

Three related improvements to workflow inside ac-native:

1. Persistent PTY sessions (keep claude alive on piece-leave)
terminal.leave() no longer calls system.pty.kill(). The PTY child
lives as long as ac-native is running, so bouncing out to the
prompt and back via `code` re-attaches to the same claude session
instead of cold-starting the CLI every time. Matters because
claude's initial project scan + OAuth handshake + model handshake
takes several seconds, and losing it on every piece swap makes
"check something quickly in the prompt then come back" expensive.

terminal.boot() checks system.pty.active && lastCmd match on
entry; if so it just resizes + clears the grid cache and returns
(no fresh spawn). Fresh spawn only on first entry or after the
PTY child has exited on its own.

2. Plain Escape exits the terminal piece (no kill, no send-to-PTY)
Was passed through as \x1b to claude, which cancels the current
operation there but still left the user stuck in the terminal.
Now esc jumps to prompt directly, PTY stays warm for next visit.
Users who need to send a real ESC byte to claude (e.g. to cancel
a prompt without leaving) can use Ctrl+[ which produces \x1b
through the existing Ctrl+key handler.

3. Git identity + credentials derived from /mnt/config.json
pty.c used to hardcode "Jeffrey Alan Scudder" + "whistlegraph" for
git author + GitHub HTTPS credential helper. Now it reads the
baked config.json handle + email and uses those, falling back to
a generic ac-native / noreply identity only if the config is
unreadable. Paired with a `compush` git alias (add-all + commit
-m + push origin HEAD) so the on-device workflow matches the
repo's CLAUDE.md notation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+98 -6
+47 -3
fedac/native/pieces/terminal.mjs
··· 177 177 } 178 178 179 179 lastCmd = cmd; 180 + // Persistent-session behavior: if a PTY child from a previous visit is 181 + // still running (user left the piece without killing it) and the cmd 182 + // matches what we'd spawn now, RE-ATTACH to the live session instead of 183 + // spawning a fresh one. "code → claude → esc → prompt → code" should 184 + // drop the user back into the same claude conversation mid-turn rather 185 + // than a blank restart. The PTY is owned by the runtime (not the piece) 186 + // so the child kept running while the piece was out of scope. 187 + if (system.pty.active && globalThis.__terminalLastCmd === cmd) { 188 + console.log("[terminal] re-attaching to existing session for", cmd); 189 + // Force a resize in case geometry changed while the piece was gone, 190 + // and clear the local grid cache so the next paint pulls fresh state. 191 + if (cols > 0 && rows > 0) system.pty.resize(cols, rows); 192 + grid = null; 193 + started = true; 194 + return; 195 + } 196 + 197 + globalThis.__terminalLastCmd = cmd; 180 198 system.pty.spawn(cmd, args, cols, rows); 181 199 started = true; 182 200 } ··· 373 391 return; 374 392 } 375 393 394 + // Plain Escape while PTY is running → jump back to prompt WITHOUT 395 + // killing the child. Claude sessions stay warm in the background; 396 + // re-entering `code` re-attaches to the same conversation. This 397 + // reclaims the workflow ergonomics of "esc to prompt, then back to 398 + // code" without having to re-bootstrap Claude every time. 399 + // 400 + // Users who need to send an actual ESC to the PTY (e.g. to cancel 401 + // a prompt in claude without leaving the piece) can use Ctrl+[ 402 + // which produces the same \x1b byte via the Ctrl+key handler below. 403 + if (key === "escape" && !ctrlHeld && !shiftHeld && !altHeld) { 404 + system?.jump?.("prompt"); 405 + return; 406 + } 407 + 376 408 // Ctrl+N — open split view (left=current cmd, right=sh) 377 409 if (ctrlHeld && key === "n") { 378 410 const name = lastCmd.split("/").pop() || "claude"; ··· 417 449 } 418 450 419 451 function leave({ system }) { 420 - if (system.pty.active) { 421 - system.pty.kill(); 422 - } 452 + // Intentionally DO NOT kill the PTY child on piece-leave. Claude Code 453 + // sessions are expensive to re-establish (model handshake, project 454 + // scan, OAuth refresh) and losing one every time you bounce to the 455 + // prompt for a quick jump elsewhere is a workflow killer. Keeping the 456 + // child alive means re-entering `code` drops back into the same 457 + // session exactly where you left it. 458 + // 459 + // The PTY child still dies naturally when: 460 + // - it exits on its own (claude /exit, sh exit, etc.) 461 + // - the user explicitly kills from inside the session (Ctrl+C, etc.) 462 + // - ac-native itself exits (reboot, crash-respawn) 463 + // And `system.pty.kill()` is still available via Ctrl+\ shortcut for 464 + // users who really do want to terminate it. 465 + // No-op on leave. 466 + (void 0); 423 467 } 424 468 425 469 export { boot, paint, act, leave };
+51 -3
fedac/native/src/pty.c
··· 588 588 if (pat[0]) { 589 589 setenv("GH_TOKEN", pat, 1); 590 590 setenv("GITHUB_TOKEN", pat, 1); 591 - // Configure git credential helper 591 + // Derive git user identity from /mnt/config.json so 592 + // `git commit` attributes to the actual device owner 593 + // rather than being hardcoded. Falls back to a 594 + // generic "aesthetic.computer" identity if config is 595 + // unreadable (e.g. device booted from a USB that 596 + // lost its config partition). 597 + char cfg_handle[64] = ""; 598 + char cfg_email[128] = ""; 599 + FILE *cfg = fopen("/mnt/config.json", "r"); 600 + if (cfg) { 601 + char line[1024]; 602 + size_t n = fread(line, 1, sizeof(line) - 1, cfg); 603 + line[n] = 0; 604 + fclose(cfg); 605 + // Cheap JSON field extraction — the config is a 606 + // single flat object written by flash-mac / oven. 607 + const char *p; 608 + if ((p = strstr(line, "\"handle\""))) { 609 + p = strchr(p, ':'); if (p) p = strchr(p, '"'); 610 + if (p) { p++; const char *q = strchr(p, '"'); 611 + if (q && q - p < (long)sizeof(cfg_handle)) { 612 + memcpy(cfg_handle, p, q - p); 613 + cfg_handle[q - p] = 0; 614 + } } 615 + } 616 + if ((p = strstr(line, "\"email\""))) { 617 + p = strchr(p, ':'); if (p) p = strchr(p, '"'); 618 + if (p) { p++; const char *q = strchr(p, '"'); 619 + if (q && q - p < (long)sizeof(cfg_email)) { 620 + memcpy(cfg_email, p, q - p); 621 + cfg_email[q - p] = 0; 622 + } } 623 + } 624 + } 625 + const char *git_name = cfg_handle[0] ? cfg_handle : "ac-native"; 626 + const char *git_email = cfg_email[0] ? cfg_email : "noreply@aesthetic.computer"; 627 + // GitHub credential helper uses the handle as the 628 + // HTTPS username (GitHub's own rule: any username 629 + // paired with a valid PAT authenticates as the PAT's 630 + // owner). For @jeffrey that resolves to the admin 631 + // `whistlegraph` account naturally. 632 + const char *gh_user = cfg_handle[0] ? cfg_handle : "ac-native"; 592 633 mkdir("/tmp/.config", 0755); 593 634 mkdir("/tmp/.config/git", 0755); 594 635 FILE *gc = fopen("/tmp/.gitconfig", "w"); 595 636 if (gc) { 596 - fprintf(gc, "[user]\n\tname = Jeffrey Alan Scudder\n\temail = mail@aesthetic.computer\n"); 597 - fprintf(gc, "[credential \"https://github.com\"]\n\thelper = !f() { echo username=whistlegraph; echo password=%s; }; f\n", pat); 637 + fprintf(gc, "[user]\n\tname = %s\n\temail = %s\n", 638 + git_name, git_email); 639 + fprintf(gc, "[credential \"https://github.com\"]\n\thelper = !f() { echo username=%s; echo password=%s; }; f\n", 640 + gh_user, pat); 641 + // Enable `compush` alias — commit all staged + untracked 642 + // changes and push origin HEAD in one verb. Matches the 643 + // notation in CLAUDE.md ("compush = commit & push"). 644 + fprintf(gc, "[alias]\n" 645 + "\tcompush = !f() { git add -A && git commit -m \"${1:-compush}\" && git push origin HEAD; }; f\n"); 598 646 fclose(gc); 599 647 } 600 648 }