cc-face 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +102 -0
  2. package/dist/bin/cli.d.ts +2 -0
  3. package/dist/bin/cli.js +142 -0
  4. package/dist/bin/cli.js.map +1 -0
  5. package/dist/src/config.d.ts +24 -0
  6. package/dist/src/config.js +32 -0
  7. package/dist/src/config.js.map +1 -0
  8. package/dist/src/face.d.ts +59 -0
  9. package/dist/src/face.js +493 -0
  10. package/dist/src/face.js.map +1 -0
  11. package/dist/src/frames.d.ts +1 -0
  12. package/dist/src/frames.js +6 -0
  13. package/dist/src/frames.js.map +1 -0
  14. package/dist/src/generator.d.ts +16 -0
  15. package/dist/src/generator.js +122 -0
  16. package/dist/src/generator.js.map +1 -0
  17. package/dist/src/ipc.d.ts +34 -0
  18. package/dist/src/ipc.js +203 -0
  19. package/dist/src/ipc.js.map +1 -0
  20. package/dist/src/loader.d.ts +12 -0
  21. package/dist/src/loader.js +63 -0
  22. package/dist/src/loader.js.map +1 -0
  23. package/dist/src/main.d.ts +2 -0
  24. package/dist/src/main.js +210 -0
  25. package/dist/src/main.js.map +1 -0
  26. package/dist/src/patterns.d.ts +6 -0
  27. package/dist/src/patterns.js +14 -0
  28. package/dist/src/patterns.js.map +1 -0
  29. package/dist/src/pty.d.ts +17 -0
  30. package/dist/src/pty.js +84 -0
  31. package/dist/src/pty.js.map +1 -0
  32. package/dist/src/scroll.d.ts +36 -0
  33. package/dist/src/scroll.js +56 -0
  34. package/dist/src/scroll.js.map +1 -0
  35. package/dist/src/state.d.ts +36 -0
  36. package/dist/src/state.js +164 -0
  37. package/dist/src/state.js.map +1 -0
  38. package/dist/src/theme.d.ts +15 -0
  39. package/dist/src/theme.js +51 -0
  40. package/dist/src/theme.js.map +1 -0
  41. package/dist/src/types.d.ts +60 -0
  42. package/dist/src/types.js +3 -0
  43. package/dist/src/types.js.map +1 -0
  44. package/dist/src/utils.d.ts +8 -0
  45. package/dist/src/utils.js +13 -0
  46. package/dist/src/utils.js.map +1 -0
  47. package/faces/idle/config.json +7 -0
  48. package/faces/idle/img/1.jpeg +0 -0
  49. package/faces/idle/img/10.jpeg +0 -0
  50. package/faces/idle/img/11.jpeg +0 -0
  51. package/faces/idle/img/12.jpeg +0 -0
  52. package/faces/idle/img/13.jpeg +0 -0
  53. package/faces/idle/img/14.jpeg +0 -0
  54. package/faces/idle/img/15.jpeg +0 -0
  55. package/faces/idle/img/16.jpeg +0 -0
  56. package/faces/idle/img/17.jpeg +0 -0
  57. package/faces/idle/img/18.jpeg +0 -0
  58. package/faces/idle/img/19.jpeg +0 -0
  59. package/faces/idle/img/2.jpeg +0 -0
  60. package/faces/idle/img/20.jpeg +0 -0
  61. package/faces/idle/img/21.jpeg +0 -0
  62. package/faces/idle/img/22.jpeg +0 -0
  63. package/faces/idle/img/23.jpeg +0 -0
  64. package/faces/idle/img/24.jpeg +0 -0
  65. package/faces/idle/img/25.jpeg +0 -0
  66. package/faces/idle/img/26.jpeg +0 -0
  67. package/faces/idle/img/27.jpeg +0 -0
  68. package/faces/idle/img/28.jpeg +0 -0
  69. package/faces/idle/img/29.jpeg +0 -0
  70. package/faces/idle/img/3.jpeg +0 -0
  71. package/faces/idle/img/30.jpeg +0 -0
  72. package/faces/idle/img/4.jpeg +0 -0
  73. package/faces/idle/img/5.jpeg +0 -0
  74. package/faces/idle/img/6.jpeg +0 -0
  75. package/faces/idle/img/7.jpeg +0 -0
  76. package/faces/idle/img/8.jpeg +0 -0
  77. package/faces/idle/img/9.jpeg +0 -0
  78. package/faces/listening/config.json +5 -0
  79. package/faces/listening/img/1.jpeg +0 -0
  80. package/faces/listening/img/10.jpeg +0 -0
  81. package/faces/listening/img/11.jpeg +0 -0
  82. package/faces/listening/img/12.jpeg +0 -0
  83. package/faces/listening/img/13.jpeg +0 -0
  84. package/faces/listening/img/14.jpeg +0 -0
  85. package/faces/listening/img/15.jpeg +0 -0
  86. package/faces/listening/img/16.jpeg +0 -0
  87. package/faces/listening/img/17.jpeg +0 -0
  88. package/faces/listening/img/18.jpeg +0 -0
  89. package/faces/listening/img/19.jpeg +0 -0
  90. package/faces/listening/img/2.jpeg +0 -0
  91. package/faces/listening/img/20.jpeg +0 -0
  92. package/faces/listening/img/21.jpeg +0 -0
  93. package/faces/listening/img/22.jpeg +0 -0
  94. package/faces/listening/img/23.jpeg +0 -0
  95. package/faces/listening/img/24.jpeg +0 -0
  96. package/faces/listening/img/25.jpeg +0 -0
  97. package/faces/listening/img/26.jpeg +0 -0
  98. package/faces/listening/img/27.jpeg +0 -0
  99. package/faces/listening/img/3.jpeg +0 -0
  100. package/faces/listening/img/4.jpeg +0 -0
  101. package/faces/listening/img/5.jpeg +0 -0
  102. package/faces/listening/img/6.jpeg +0 -0
  103. package/faces/listening/img/7.jpeg +0 -0
  104. package/faces/listening/img/8.jpeg +0 -0
  105. package/faces/listening/img/9.jpeg +0 -0
  106. package/faces/thinking/config.json +5 -0
  107. package/faces/thinking/img/1.jpeg +0 -0
  108. package/faces/thinking/img/10.jpeg +0 -0
  109. package/faces/thinking/img/11.jpeg +0 -0
  110. package/faces/thinking/img/12.jpeg +0 -0
  111. package/faces/thinking/img/13.jpeg +0 -0
  112. package/faces/thinking/img/14.jpeg +0 -0
  113. package/faces/thinking/img/15.jpeg +0 -0
  114. package/faces/thinking/img/16.jpeg +0 -0
  115. package/faces/thinking/img/17.jpeg +0 -0
  116. package/faces/thinking/img/18.jpeg +0 -0
  117. package/faces/thinking/img/19.jpeg +0 -0
  118. package/faces/thinking/img/2.jpeg +0 -0
  119. package/faces/thinking/img/20.jpeg +0 -0
  120. package/faces/thinking/img/3.jpeg +0 -0
  121. package/faces/thinking/img/4.jpeg +0 -0
  122. package/faces/thinking/img/5.jpeg +0 -0
  123. package/faces/thinking/img/6.jpeg +0 -0
  124. package/faces/thinking/img/7.jpeg +0 -0
  125. package/faces/thinking/img/8.jpeg +0 -0
  126. package/faces/thinking/img/9.jpeg +0 -0
  127. package/faces/typing/config.json +5 -0
  128. package/faces/typing/img/1.jpeg +0 -0
  129. package/faces/typing/img/10.jpeg +0 -0
  130. package/faces/typing/img/11.jpeg +0 -0
  131. package/faces/typing/img/12.jpeg +0 -0
  132. package/faces/typing/img/13.jpeg +0 -0
  133. package/faces/typing/img/14.jpeg +0 -0
  134. package/faces/typing/img/15.jpeg +0 -0
  135. package/faces/typing/img/16.jpeg +0 -0
  136. package/faces/typing/img/17.jpeg +0 -0
  137. package/faces/typing/img/18.jpeg +0 -0
  138. package/faces/typing/img/19.jpeg +0 -0
  139. package/faces/typing/img/2.jpeg +0 -0
  140. package/faces/typing/img/20.jpeg +0 -0
  141. package/faces/typing/img/21.jpeg +0 -0
  142. package/faces/typing/img/22.jpeg +0 -0
  143. package/faces/typing/img/23.jpeg +0 -0
  144. package/faces/typing/img/24.jpeg +0 -0
  145. package/faces/typing/img/25.jpeg +0 -0
  146. package/faces/typing/img/26.jpeg +0 -0
  147. package/faces/typing/img/27.jpeg +0 -0
  148. package/faces/typing/img/28.jpeg +0 -0
  149. package/faces/typing/img/29.jpeg +0 -0
  150. package/faces/typing/img/3.jpeg +0 -0
  151. package/faces/typing/img/30.jpeg +0 -0
  152. package/faces/typing/img/31.jpeg +0 -0
  153. package/faces/typing/img/32.jpeg +0 -0
  154. package/faces/typing/img/4.jpeg +0 -0
  155. package/faces/typing/img/5.jpeg +0 -0
  156. package/faces/typing/img/6.jpeg +0 -0
  157. package/faces/typing/img/7.jpeg +0 -0
  158. package/faces/typing/img/8.jpeg +0 -0
  159. package/faces/typing/img/9.jpeg +0 -0
  160. package/package.json +36 -0
@@ -0,0 +1,210 @@
1
+ import { spawnClaude } from './pty.js';
2
+ import { StateMachine } from './state.js';
3
+ import { FaceRenderer } from './face.js';
4
+ import { getTheme } from './theme.js';
5
+ import { initGenerator, buildFace } from './loader.js';
6
+ import { StateServer, StateClient, getSocketPath, writeDiscovery, readDiscovery, clearDiscovery } from './ipc.js';
7
+ // ── Required expression keys ────────────────────────────────────────
8
+ const REQUIRED_EXPRESSIONS = ['idle', 'typing', 'thinking', 'listening'];
9
+ // ── Face validation ─────────────────────────────────────────────────
10
+ function validateFace(face) {
11
+ if (!face.name || typeof face.name !== 'string') {
12
+ throw new Error('Face definition must have a "name" string property.');
13
+ }
14
+ if (typeof face.width !== 'number' || face.width <= 0) {
15
+ throw new Error('Face definition must have a positive "width" number.');
16
+ }
17
+ if (typeof face.height !== 'number' || face.height <= 0) {
18
+ throw new Error('Face definition must have a positive "height" number.');
19
+ }
20
+ if (!face.expressions || typeof face.expressions !== 'object') {
21
+ throw new Error('Face definition must have an "expressions" object.');
22
+ }
23
+ // Check all required expression keys are present
24
+ for (const key of REQUIRED_EXPRESSIONS) {
25
+ if (!(key in face.expressions)) {
26
+ throw new Error(`Face definition is missing required expression "${key}". ` +
27
+ `Required expressions: ${REQUIRED_EXPRESSIONS.join(', ')}.`);
28
+ }
29
+ const expr = face.expressions[key];
30
+ if (!expr.frames || !Array.isArray(expr.frames) || expr.frames.length === 0) {
31
+ throw new Error(`Expression "${key}" must have a non-empty "frames" array.`);
32
+ }
33
+ // Validate animation config
34
+ const config = expr.config;
35
+ if (!config || typeof config !== 'object') {
36
+ throw new Error(`Expression "${key}" must have a "config" object.`);
37
+ }
38
+ if (typeof config.loopDuration !== 'number' || config.loopDuration <= 0) {
39
+ throw new Error(`Expression "${key}": config.loopDuration must be a positive number.`);
40
+ }
41
+ if (typeof config.gap !== 'number' || config.gap < 0) {
42
+ throw new Error(`Expression "${key}": config.gap must be a non-negative number.`);
43
+ }
44
+ if (typeof config.reverse !== 'boolean') {
45
+ throw new Error(`Expression "${key}": config.reverse must be a boolean.`);
46
+ }
47
+ // Validate each frame
48
+ for (let i = 0; i < expr.frames.length; i++) {
49
+ const frame = expr.frames[i];
50
+ if (!frame.art || !Array.isArray(frame.art)) {
51
+ throw new Error(`Expression "${key}", frame ${i}: must have an "art" array of strings.`);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ // ── Load face definition ────────────────────────────────────────────
57
+ async function loadFace(facePath) {
58
+ if (facePath) {
59
+ try {
60
+ const imported = await import(facePath);
61
+ const face = imported.default ?? imported.face ?? imported;
62
+ validateFace(face);
63
+ return face;
64
+ }
65
+ catch (err) {
66
+ if (err instanceof Error) {
67
+ throw new Error(`Failed to load face from "${facePath}": ${err.message}`);
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+ const cols = process.stdout.columns ?? 80;
73
+ const rows = process.stdout.rows ?? 24;
74
+ return buildFace(cols, rows);
75
+ }
76
+ // ── Main entry point ────────────────────────────────────────────────
77
+ export async function run(options) {
78
+ await initGenerator();
79
+ // ── Face-only mode ──────────────────────────────────────────────
80
+ if (options.displayOnly) {
81
+ return runFaceOnly(options);
82
+ }
83
+ // ── Load face ───────────────────────────────────────────────────
84
+ const face = await loadFace(options.facePath);
85
+ // ── --no-face mode (plain passthrough) ──────────────────────────
86
+ if (options.noFace) {
87
+ const cols = process.stdout.columns ?? 80;
88
+ const rows = process.stdout.rows ?? 24;
89
+ const pty = spawnClaude(cols, rows, []);
90
+ process.stdin.setRawMode(true);
91
+ process.stdin.resume();
92
+ process.stdin.on('data', (data) => pty.write(data.toString()));
93
+ pty.onData((data) => process.stdout.write(data));
94
+ pty.onExit(({ exitCode }) => {
95
+ process.stdin.setRawMode(false);
96
+ process.exit(exitCode);
97
+ });
98
+ return;
99
+ }
100
+ // ── Wrapper mode (default) ──────────────────────────────────────
101
+ // Start IPC server
102
+ const socketPath = getSocketPath();
103
+ const server = new StateServer(socketPath);
104
+ writeDiscovery(socketPath);
105
+ process.stderr.write(`Face server listening. In another terminal run: cc-face -display\n`);
106
+ // Spawn Claude PTY at full terminal size
107
+ const cols = process.stdout.columns ?? 80;
108
+ const rows = process.stdout.rows ?? 24;
109
+ const pty = spawnClaude(cols, rows, []);
110
+ // State machine — broadcast state changes over IPC
111
+ const stateMachine = new StateMachine((event) => {
112
+ server.broadcast(event.next);
113
+ });
114
+ // Wire PTY data → stdout + state machine
115
+ pty.onData((data) => {
116
+ process.stdout.write(data);
117
+ stateMachine.feed(data);
118
+ });
119
+ // Wire stdin → PTY + notify state machine of human input
120
+ process.stdin.setRawMode(true);
121
+ process.stdin.resume();
122
+ process.stdin.on('data', (data) => {
123
+ stateMachine.notifyHumanInput();
124
+ pty.write(data.toString());
125
+ });
126
+ // Handle SIGWINCH (terminal resize)
127
+ process.on('SIGWINCH', () => {
128
+ const newCols = process.stdout.columns ?? 80;
129
+ const newRows = process.stdout.rows ?? 24;
130
+ pty.resize(newCols, newRows);
131
+ });
132
+ // Cleanup (guarded against double invocation from concurrent signals)
133
+ let cleaned = false;
134
+ const cleanup = () => {
135
+ if (cleaned)
136
+ return;
137
+ cleaned = true;
138
+ stateMachine.destroy();
139
+ server.broadcastShutdown();
140
+ clearDiscovery(socketPath);
141
+ server.close();
142
+ try {
143
+ process.stdin.setRawMode(false);
144
+ }
145
+ catch {
146
+ // stdin may already be destroyed
147
+ }
148
+ };
149
+ pty.onExit(({ exitCode }) => {
150
+ cleanup();
151
+ process.exit(exitCode);
152
+ });
153
+ process.on('SIGINT', () => {
154
+ cleanup();
155
+ process.exit(0);
156
+ });
157
+ process.on('SIGTERM', () => {
158
+ cleanup();
159
+ process.exit(0);
160
+ });
161
+ }
162
+ // ── Face-only mode ──────────────────────────────────────────────────
163
+ async function runFaceOnly(options) {
164
+ const face = await loadFace(options.facePath);
165
+ const theme = getTheme(options.theme);
166
+ const renderer = new FaceRenderer(face, theme);
167
+ // If --sock given, use it directly. Otherwise, discover via the active file.
168
+ const socketPathOrResolver = options.socketPath
169
+ ? options.socketPath
170
+ : () => readDiscovery();
171
+ renderer.showMessage('Waiting for cc-face...');
172
+ // Connect to wrapper's IPC server
173
+ const client = new StateClient(socketPathOrResolver, (state) => {
174
+ renderer.setState(state);
175
+ }, {
176
+ onDisconnect() {
177
+ renderer.showMessage('Disconnected. Waiting for cc-face...');
178
+ },
179
+ onConnect() {
180
+ renderer.setState('idle');
181
+ },
182
+ onShutdown() {
183
+ renderer.destroy();
184
+ client.close();
185
+ process.exit(0);
186
+ },
187
+ });
188
+ // Handle resize — invalidate pre-rendered frame cache
189
+ process.on('SIGWINCH', () => {
190
+ renderer.invalidateCache();
191
+ });
192
+ // Cleanup (guarded against double invocation from concurrent signals)
193
+ let cleaned = false;
194
+ const cleanup = () => {
195
+ if (cleaned)
196
+ return;
197
+ cleaned = true;
198
+ renderer.destroy();
199
+ client.close();
200
+ };
201
+ process.on('SIGINT', () => {
202
+ cleanup();
203
+ process.exit(0);
204
+ });
205
+ process.on('SIGTERM', () => {
206
+ cleanup();
207
+ process.exit(0);
208
+ });
209
+ }
210
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { FaceState } from './types.js';
2
+ export interface PatternEntry {
3
+ state: FaceState;
4
+ patterns: RegExp[];
5
+ }
6
+ export declare const realtimePatterns: PatternEntry[];
@@ -0,0 +1,14 @@
1
+ // ── Realtime Patterns ────────────────────────────────────────────────
2
+ // Checked during streaming (each feed() call). Priority = array order.
3
+ export const realtimePatterns = [
4
+ {
5
+ state: 'thinking',
6
+ patterns: [
7
+ /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/, // braille spinners
8
+ /[·✢✳✶✻✽]/, // tweakcc phases
9
+ /Thinking/,
10
+ /Reasoning/,
11
+ ],
12
+ },
13
+ ];
14
+ //# sourceMappingURL=patterns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/patterns.ts"],"names":[],"mappings":"AASA,wEAAwE;AACxE,uEAAuE;AAEvE,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC9C;QACE,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE;YACR,cAAc,EAAK,mBAAmB;YACtC,UAAU,EAAe,iBAAiB;YAC1C,UAAU;YACV,WAAW;SACZ;KACF;CACF,CAAC"}
@@ -0,0 +1,17 @@
1
+ export interface PtyHandle {
2
+ onData(cb: (data: string) => void): void;
3
+ onExit(cb: (exit: {
4
+ exitCode: number;
5
+ signal?: number;
6
+ }) => void): void;
7
+ write(data: string): void;
8
+ resize(cols: number, rows: number): void;
9
+ kill(): void;
10
+ pid: number;
11
+ }
12
+ export declare function findClaude(): string;
13
+ export declare function resolveSymlink(p: string): string | null;
14
+ /**
15
+ * Spawn `claude` CLI in a pseudo-terminal.
16
+ */
17
+ export declare function spawnClaude(cols: number, rows: number, args?: string[]): PtyHandle;
@@ -0,0 +1,84 @@
1
+ import * as pty from 'node-pty';
2
+ import { accessSync, constants, readlinkSync } from 'node:fs';
3
+ import { execSync } from 'node:child_process';
4
+ export function findClaude() {
5
+ // 1. Explicit override via env var
6
+ if (process.env.CLAUDE_PATH)
7
+ return process.env.CLAUDE_PATH;
8
+ if (process.platform === 'win32')
9
+ return 'cmd.exe';
10
+ // 2. Resolve via user's login shell (picks up ~/.local/bin etc.)
11
+ try {
12
+ const resolved = execSync('which claude', { encoding: 'utf-8', env: process.env }).trim();
13
+ if (resolved)
14
+ return resolved;
15
+ }
16
+ catch { /* fall through */ }
17
+ // 3. Check common install locations
18
+ const home = process.env.HOME ?? '';
19
+ const candidates = [
20
+ `${home}/.local/bin/claude`,
21
+ '/usr/local/bin/claude',
22
+ '/opt/homebrew/bin/claude',
23
+ ];
24
+ for (const p of candidates) {
25
+ try {
26
+ accessSync(p, constants.X_OK);
27
+ return p;
28
+ }
29
+ catch { /* next */ }
30
+ }
31
+ throw new Error('Could not find "claude" CLI. Ensure Claude Code is installed and on your PATH,\n' +
32
+ 'or set CLAUDE_PATH=/path/to/claude.');
33
+ }
34
+ export function resolveSymlink(p) {
35
+ try {
36
+ return readlinkSync(p);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Spawn `claude` CLI in a pseudo-terminal.
44
+ */
45
+ export function spawnClaude(cols, rows, args = []) {
46
+ const claudePath = findClaude();
47
+ const shellArgs = process.platform === 'win32' ? ['/c', 'claude', ...args] : args;
48
+ let proc;
49
+ try {
50
+ proc = pty.spawn(claudePath, shellArgs, {
51
+ name: 'xterm-256color',
52
+ cols,
53
+ rows,
54
+ cwd: process.cwd(),
55
+ env: process.env,
56
+ });
57
+ }
58
+ catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ throw new Error(`Failed to spawn PTY for "${claudePath}": ${msg}\n` +
61
+ `Tip: verify with: ls -la ${claudePath} && ${claudePath} --version`);
62
+ }
63
+ return {
64
+ onData(cb) {
65
+ proc.onData(cb);
66
+ },
67
+ onExit(cb) {
68
+ proc.onExit(cb);
69
+ },
70
+ write(data) {
71
+ proc.write(data);
72
+ },
73
+ resize(c, r) {
74
+ proc.resize(c, r);
75
+ },
76
+ kill() {
77
+ proc.kill();
78
+ },
79
+ get pid() {
80
+ return proc.pid;
81
+ },
82
+ };
83
+ }
84
+ //# sourceMappingURL=pty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty.js","sourceRoot":"","sources":["../../src/pty.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ANSI scroll-region management.
3
+ *
4
+ * Uses DECSTBM (`ESC [ top ; bottom r`) to split the terminal into a
5
+ * fixed face region at the top and a scrollable content region below.
6
+ */
7
+ export interface ScrollDimensions {
8
+ faceRows: number;
9
+ contentRows: number;
10
+ totalRows: number;
11
+ totalCols: number;
12
+ }
13
+ /**
14
+ * Return current terminal dimensions split by face height.
15
+ */
16
+ export declare function getScrollDimensions(faceHeight: number): ScrollDimensions;
17
+ /**
18
+ * Set the terminal scroll region so that the top `faceHeight` rows are
19
+ * fixed and only the rows below them scroll.
20
+ *
21
+ * Returns a cleanup function that resets the scroll region.
22
+ */
23
+ export declare function setupScrollRegion(faceHeight: number): () => void;
24
+ /**
25
+ * Reset scroll region to the full terminal and show cursor.
26
+ */
27
+ export declare function teardownScrollRegion(): void;
28
+ /**
29
+ * Save cursor position and move to row 0, col 0 (top-left) for face
30
+ * rendering. Uses 1-indexed ANSI coordinates.
31
+ */
32
+ export declare function moveCursorToFace(): void;
33
+ /**
34
+ * Restore the previously saved cursor position.
35
+ */
36
+ export declare function restoreCursor(): void;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * ANSI scroll-region management.
3
+ *
4
+ * Uses DECSTBM (`ESC [ top ; bottom r`) to split the terminal into a
5
+ * fixed face region at the top and a scrollable content region below.
6
+ */
7
+ /**
8
+ * Return current terminal dimensions split by face height.
9
+ */
10
+ export function getScrollDimensions(faceHeight) {
11
+ const totalRows = process.stdout.rows ?? 24;
12
+ const totalCols = process.stdout.columns ?? 80;
13
+ const faceRows = faceHeight;
14
+ const contentRows = totalRows - faceHeight;
15
+ return { faceRows, contentRows, totalRows, totalCols };
16
+ }
17
+ /**
18
+ * Set the terminal scroll region so that the top `faceHeight` rows are
19
+ * fixed and only the rows below them scroll.
20
+ *
21
+ * Returns a cleanup function that resets the scroll region.
22
+ */
23
+ export function setupScrollRegion(faceHeight) {
24
+ const { totalRows } = getScrollDimensions(faceHeight);
25
+ // DECSTBM: set scroll region from (faceHeight+1) to totalRows (1-indexed)
26
+ process.stdout.write(`\x1b[${faceHeight + 1};${totalRows}r`);
27
+ // Move cursor to the first row of the content area
28
+ process.stdout.write(`\x1b[${faceHeight + 1};1H`);
29
+ return teardownScrollRegion;
30
+ }
31
+ /**
32
+ * Reset scroll region to the full terminal and show cursor.
33
+ */
34
+ export function teardownScrollRegion() {
35
+ // Reset scroll region
36
+ process.stdout.write('\x1b[r');
37
+ // Show cursor
38
+ process.stdout.write('\x1b[?25h');
39
+ }
40
+ /**
41
+ * Save cursor position and move to row 0, col 0 (top-left) for face
42
+ * rendering. Uses 1-indexed ANSI coordinates.
43
+ */
44
+ export function moveCursorToFace() {
45
+ // Save cursor position
46
+ process.stdout.write('\x1b7');
47
+ // Move to row 1, col 1 (top-left, 1-indexed)
48
+ process.stdout.write('\x1b[1;1H');
49
+ }
50
+ /**
51
+ * Restore the previously saved cursor position.
52
+ */
53
+ export function restoreCursor() {
54
+ process.stdout.write('\x1b8');
55
+ }
56
+ //# sourceMappingURL=scroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll.js","sourceRoot":"","sources":["../../src/scroll.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;IAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IAC/C,MAAM,QAAQ,GAAG,UAAU,CAAC;IAC5B,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;IAE3C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,MAAM,EAAE,SAAS,EAAE,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAEtD,0EAA0E;IAC1E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,UAAU,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC;IAE7D,mDAAmD;IACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC;IAElD,OAAO,oBAAoB,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,sBAAsB;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/B,cAAc;IACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,uBAAuB;IACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,6CAA6C;IAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { FaceState, StateEvent } from './types.js';
2
+ export declare class StateMachine {
3
+ private state;
4
+ private idleTimer;
5
+ private debounceTimer;
6
+ private feedTimer;
7
+ private listeningTimer;
8
+ private onStateChange;
9
+ private pendingData;
10
+ private hasPendingData;
11
+ private lastThinkingMatchTime;
12
+ private hasReceivedHumanInput;
13
+ constructor(onStateChange: (event: StateEvent) => void);
14
+ getState(): FaceState;
15
+ /**
16
+ * Called when the human presses a key. Transitions to 'listening'
17
+ * and resets the listening timeout. When the user stops typing for
18
+ * LISTENING_TIMEOUT_MS, transitions back to idle.
19
+ */
20
+ notifyHumanInput(): void;
21
+ /**
22
+ * Feed raw PTY data. Instead of processing every chunk synchronously
23
+ * (hundreds of regex ops + timer create/destroy per second during heavy
24
+ * output), we accumulate data and process it at a fixed sample rate.
25
+ * This adds up to 50ms of detection latency, which is imperceptible
26
+ * given the 100ms debounce on output.
27
+ */
28
+ feed(data: string): void;
29
+ destroy(): void;
30
+ /**
31
+ * Process accumulated data: strip ANSI once for the whole batch,
32
+ * run pattern matching once, check for typing once.
33
+ */
34
+ private processPendingData;
35
+ private transition;
36
+ }
@@ -0,0 +1,164 @@
1
+ import { stripAnsi } from './utils.js';
2
+ import { realtimePatterns } from './patterns.js';
3
+ import { config } from './config.js';
4
+ // ── Timeout constants ────────────────────────────────────────────────
5
+ const { idleTimeoutMs: IDLE_TIMEOUT_MS, debounceMs: DEBOUNCE_MS, feedSampleIntervalMs: FEED_SAMPLE_INTERVAL_MS, minPrintableLen: MIN_PRINTABLE_LEN, thinkingCooldownMs: THINKING_COOLDOWN_MS, listeningTimeoutMs: LISTENING_TIMEOUT_MS, } = config;
6
+ // ── Helper: match against a pattern list (first match wins) ─────────
7
+ function matchPatterns(text, entries) {
8
+ for (const entry of entries) {
9
+ for (const re of entry.patterns) {
10
+ if (re.test(text)) {
11
+ return entry;
12
+ }
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+ // ── State Machine ────────────────────────────────────────────────────
18
+ export class StateMachine {
19
+ state = 'idle';
20
+ idleTimer = null;
21
+ debounceTimer = null;
22
+ feedTimer = null;
23
+ listeningTimer = null;
24
+ onStateChange;
25
+ // Accumulated PTY data between processing ticks
26
+ pendingData = '';
27
+ hasPendingData = false;
28
+ // Timestamp of last thinking pattern match — used to suppress
29
+ // typing transitions during the thinking cooldown window
30
+ lastThinkingMatchTime = 0;
31
+ // Suppress typing transitions until the user has pressed a key.
32
+ // Prevents Claude's startup banner from triggering the typing state.
33
+ hasReceivedHumanInput = false;
34
+ constructor(onStateChange) {
35
+ this.onStateChange = onStateChange;
36
+ }
37
+ getState() {
38
+ return this.state;
39
+ }
40
+ /**
41
+ * Called when the human presses a key. Transitions to 'listening'
42
+ * and resets the listening timeout. When the user stops typing for
43
+ * LISTENING_TIMEOUT_MS, transitions back to idle.
44
+ */
45
+ notifyHumanInput() {
46
+ this.hasReceivedHumanInput = true;
47
+ // Don't override thinking/typing — Claude is actively doing something
48
+ if (this.state === 'thinking' || this.state === 'typing')
49
+ return;
50
+ this.transition('listening');
51
+ // Reset listening timeout
52
+ if (this.listeningTimer !== null)
53
+ clearTimeout(this.listeningTimer);
54
+ this.listeningTimer = setTimeout(() => {
55
+ this.listeningTimer = null;
56
+ if (this.state === 'listening') {
57
+ this.transition('idle');
58
+ }
59
+ }, LISTENING_TIMEOUT_MS);
60
+ }
61
+ /**
62
+ * Feed raw PTY data. Instead of processing every chunk synchronously
63
+ * (hundreds of regex ops + timer create/destroy per second during heavy
64
+ * output), we accumulate data and process it at a fixed sample rate.
65
+ * This adds up to 50ms of detection latency, which is imperceptible
66
+ * given the 100ms debounce on output.
67
+ */
68
+ feed(data) {
69
+ if (data.length === 0)
70
+ return;
71
+ this.pendingData += data;
72
+ this.hasPendingData = true;
73
+ // Reset idle timer on every chunk (this is cheap — just a clear + set)
74
+ if (this.idleTimer !== null)
75
+ clearTimeout(this.idleTimer);
76
+ this.idleTimer = setTimeout(() => {
77
+ this.idleTimer = null;
78
+ this.transition('idle');
79
+ }, IDLE_TIMEOUT_MS);
80
+ // Schedule processing if not already pending
81
+ if (this.feedTimer === null) {
82
+ this.feedTimer = setTimeout(() => this.processPendingData(), FEED_SAMPLE_INTERVAL_MS);
83
+ }
84
+ }
85
+ // ── Cleanup ──────────────────────────────────────────────────────
86
+ destroy() {
87
+ if (this.idleTimer !== null) {
88
+ clearTimeout(this.idleTimer);
89
+ this.idleTimer = null;
90
+ }
91
+ if (this.debounceTimer !== null) {
92
+ clearTimeout(this.debounceTimer);
93
+ this.debounceTimer = null;
94
+ }
95
+ if (this.feedTimer !== null) {
96
+ clearTimeout(this.feedTimer);
97
+ this.feedTimer = null;
98
+ }
99
+ if (this.listeningTimer !== null) {
100
+ clearTimeout(this.listeningTimer);
101
+ this.listeningTimer = null;
102
+ }
103
+ this.pendingData = '';
104
+ this.hasPendingData = false;
105
+ }
106
+ // ── Private helpers ──────────────────────────────────────────────
107
+ /**
108
+ * Process accumulated data: strip ANSI once for the whole batch,
109
+ * run pattern matching once, check for typing once.
110
+ */
111
+ processPendingData() {
112
+ this.feedTimer = null;
113
+ if (!this.hasPendingData)
114
+ return;
115
+ const raw = this.pendingData;
116
+ this.pendingData = '';
117
+ this.hasPendingData = false;
118
+ const clean = stripAnsi(raw);
119
+ // Realtime pattern matching (priority order)
120
+ const match = matchPatterns(clean, realtimePatterns);
121
+ if (match) {
122
+ if (match.state === 'thinking') {
123
+ this.lastThinkingMatchTime = Date.now();
124
+ }
125
+ // Cancel listening state — Claude is responding
126
+ if (this.listeningTimer !== null) {
127
+ clearTimeout(this.listeningTimer);
128
+ this.listeningTimer = null;
129
+ }
130
+ this.transition(match.state);
131
+ return;
132
+ }
133
+ // "typing" — non-trivial printable text from Claude (not human echo).
134
+ // If we're in listening state, PTY echo from human keystrokes will appear
135
+ // but it's short (1-2 chars per keystroke). MIN_PRINTABLE_LEN filters that.
136
+ // Suppress if we recently saw a thinking pattern.
137
+ if (this.state !== 'listening') {
138
+ if (!this.hasReceivedHumanInput)
139
+ return;
140
+ const inThinkingCooldown = Date.now() - this.lastThinkingMatchTime < THINKING_COOLDOWN_MS;
141
+ if (inThinkingCooldown)
142
+ return;
143
+ const printable = clean.replace(/[\x00-\x1f\x7f]/g, '').trim();
144
+ if (printable.length >= MIN_PRINTABLE_LEN) {
145
+ this.transition('typing');
146
+ }
147
+ }
148
+ }
149
+ transition(next) {
150
+ if (next === this.state)
151
+ return;
152
+ const prev = this.state;
153
+ this.state = next;
154
+ // Debounce the callback — only fire after state is stable for DEBOUNCE_MS.
155
+ // Internal state updates immediately so getState() is always current.
156
+ if (this.debounceTimer !== null)
157
+ clearTimeout(this.debounceTimer);
158
+ this.debounceTimer = setTimeout(() => {
159
+ this.debounceTimer = null;
160
+ this.onStateChange({ prev, next: this.state, timestamp: Date.now() });
161
+ }, DEBOUNCE_MS);
162
+ }
163
+ }
164
+ //# sourceMappingURL=state.js.map