life-pulse 2.3.3 → 2.3.7
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.
- package/dist/agent.js +11 -12
- package/dist/cli.js +9 -3
- package/dist/crm.d.ts +1 -1
- package/dist/crm.js +62 -22
- package/dist/tui.d.ts +0 -5
- package/dist/tui.js +29 -41
- package/dist/ui/app.d.ts +1 -1
- package/dist/ui/app.js +17 -3
- package/dist/ui/progress.js +8 -4
- package/dist/ui/theme.js +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -348,22 +348,21 @@ progress, onCard) {
|
|
|
348
348
|
// User prompt
|
|
349
349
|
const prompt = `Run the executor protocol. One shot. Tell me what actually matters.
|
|
350
350
|
|
|
351
|
-
1. SCAN
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
- For unknown contacts: search_emails(sender=NAME) before labeling unknown
|
|
351
|
+
1. SCAN — call these in parallel:
|
|
352
|
+
get_claude_history(days=3), get_chatgpt_history, scan_sources, get_calendar(days_ahead=7), get_unanswered_messages, get_messages(days=7, limit=100), get_calls(missed_only=true)
|
|
353
|
+
For unknown contacts: search_emails(sender=NAME) before labeling unknown.
|
|
355
354
|
|
|
356
|
-
2.
|
|
355
|
+
2. STREAM — as SOON as you identify an item, dispatch a worker AND present it:
|
|
356
|
+
- Dispatch a Task worker to investigate (draft reply, gather context, etc.)
|
|
357
|
+
- Immediately call ask_user with your best recommendation. Don't wait for other workers.
|
|
358
|
+
- ${name} picks inline while other workers run in parallel.
|
|
359
|
+
- CRITICAL: present the FIRST card within 30 seconds. Don't wait until all scanning is done.
|
|
357
360
|
|
|
358
|
-
3.
|
|
359
|
-
|
|
360
|
-
4. For each decision, present via ask_user. ${name} picks inline.
|
|
361
|
-
|
|
362
|
-
5. SYNTHESIZE into final briefing.
|
|
361
|
+
3. SYNTHESIZE — after all cards are presented, output final JSON.
|
|
363
362
|
|
|
364
363
|
Output JSON (no markdown, no code fences):
|
|
365
364
|
{
|
|
366
|
-
"greeting": "
|
|
365
|
+
"greeting": "Short, warm one-liner. No counts or statistics.",
|
|
367
366
|
"promises": [{ "title": "...", "urgency": "now|today|this_week", "who": "...", "when_due": "...", "options": [{"label":"...","description":"..."}] }],
|
|
368
367
|
"blockers": [{ "title": "...", "urgency": "now|today|this_week", "who": "...", "waiting_since": "...", "options": [{"label":"...","description":"..."}] }],
|
|
369
368
|
"bumps": [{ "title": "...", "who": "...", "context": "...", "options": [{"label":"...","description":"..."}] }],
|
|
@@ -374,7 +373,7 @@ Output JSON (no markdown, no code fences):
|
|
|
374
373
|
|
|
375
374
|
RULES:
|
|
376
375
|
- Promises/blockers: 2-3 options. First = recommendation. Description = draft text.
|
|
377
|
-
- Present each decision via ask_user
|
|
376
|
+
- Present each decision via ask_user THE MOMENT you have enough context. Never batch.
|
|
378
377
|
- Bumps: 1-2 options. Alpha: plain strings.
|
|
379
378
|
- HARD LIMIT: 4 promises, 4 blockers, 3 bumps, 3 alpha, 3 handled.
|
|
380
379
|
- Empty sections = fine. Short briefing = gift.`;
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { saveState, saveDecisions } from './state.js';
|
|
|
6
6
|
// ProgressRenderer unused — removed
|
|
7
7
|
import { InkProgress } from './ui/progress.js';
|
|
8
8
|
import { addTodo, resolveTodos, pruneOld } from './todo.js';
|
|
9
|
-
import { renderIntro, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
|
|
9
|
+
import { renderIntro, renderCRMList, renderCRMContact, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
|
|
10
10
|
import { needsDiscovery, discoverPlatforms, savePlatforms } from './platforms.js';
|
|
11
11
|
import { generateArchetype } from './archetype.js';
|
|
12
12
|
import { runPermissionFlow, getMissingPermissions } from './permissions.js';
|
|
@@ -89,20 +89,26 @@ async function showCRM(apiKey, opts) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
opts?.spinner?.stop();
|
|
92
|
-
opts?.spinner?.start('reading the room');
|
|
93
92
|
const calendarContext = opts?.calendarContext ?? '';
|
|
94
93
|
const hour = new Date().getHours();
|
|
95
94
|
const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
|
|
95
|
+
// Stream contacts as they enrich — each name + relationship appears live
|
|
96
96
|
let brief = '';
|
|
97
97
|
const enriched = [];
|
|
98
|
+
const enrichedNames = new Set();
|
|
98
99
|
for await (const t of streamEnrichedCRM(crm, apiKey, {
|
|
99
100
|
calendarContext,
|
|
100
101
|
timeOfDay,
|
|
101
102
|
onBrief: (b) => { brief = b; },
|
|
102
103
|
})) {
|
|
103
104
|
enriched.push(t);
|
|
105
|
+
enrichedNames.add(t.name);
|
|
106
|
+
await renderCRMContact(t);
|
|
104
107
|
}
|
|
105
|
-
|
|
108
|
+
// Show remaining contacts (not enriched) — just names
|
|
109
|
+
const remaining = crm.threads.filter(t => !enrichedNames.has(t.name)).slice(0, 5);
|
|
110
|
+
if (remaining.length)
|
|
111
|
+
renderCRMList(remaining);
|
|
106
112
|
if (brief)
|
|
107
113
|
await renderBrief(brief);
|
|
108
114
|
saveContactSummaries(enriched);
|
package/dist/crm.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface CRM {
|
|
|
35
35
|
generatedAt: string;
|
|
36
36
|
}
|
|
37
37
|
export declare function buildCRM(): Promise<CRM>;
|
|
38
|
-
export declare function streamEnrichedCRM(crm: CRM,
|
|
38
|
+
export declare function streamEnrichedCRM(crm: CRM, _apiKey?: string, opts?: {
|
|
39
39
|
calendarContext?: string;
|
|
40
40
|
timeOfDay?: string;
|
|
41
41
|
onBrief?: (brief: string) => void;
|
package/dist/crm.js
CHANGED
|
@@ -12,7 +12,8 @@ import { join } from 'path';
|
|
|
12
12
|
import { openDb, safeQuery } from './db.js';
|
|
13
13
|
import { resolveName } from './contacts.js';
|
|
14
14
|
import * as rply from './rply-client.js';
|
|
15
|
-
|
|
15
|
+
// OpenRouter key for fast CRM enrichment (GLM-4.7)
|
|
16
|
+
const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY || '';
|
|
16
17
|
import dayjs from 'dayjs';
|
|
17
18
|
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
|
18
19
|
dayjs.extend(relativeTime);
|
|
@@ -324,9 +325,9 @@ function buildCRMFromSQLite() {
|
|
|
324
325
|
return { threads: threads.slice(0, 25), yourAvgResponseSec: yourAvg, generatedAt: dayjs().toISOString() };
|
|
325
326
|
}
|
|
326
327
|
// ─── Stream-enrich CRM ──────────────────────────────────────────
|
|
327
|
-
export async function* streamEnrichedCRM(crm,
|
|
328
|
+
export async function* streamEnrichedCRM(crm, _apiKey, opts) {
|
|
328
329
|
const top = crm.threads.slice(0, 10);
|
|
329
|
-
if (!top.length || !
|
|
330
|
+
if (!top.length || !OPENROUTER_KEY) {
|
|
330
331
|
for (const t of top)
|
|
331
332
|
yield t;
|
|
332
333
|
return;
|
|
@@ -367,13 +368,20 @@ export async function* streamEnrichedCRM(crm, apiKey, opts) {
|
|
|
367
368
|
curPriority = null;
|
|
368
369
|
};
|
|
369
370
|
try {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
371
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: {
|
|
374
|
+
'Authorization': `Bearer ${OPENROUTER_KEY}`,
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
'HTTP-Referer': 'https://life-pulse.dev',
|
|
377
|
+
},
|
|
378
|
+
body: JSON.stringify({
|
|
379
|
+
model: 'z-ai/glm-4.7',
|
|
380
|
+
max_tokens: 3000,
|
|
381
|
+
stream: true,
|
|
382
|
+
messages: [{
|
|
383
|
+
role: 'user',
|
|
384
|
+
content: `You are a personal chief of staff analyzing someone's message history this ${timeOfDay}.
|
|
377
385
|
|
|
378
386
|
Recent iMessage threads below. → = they sent, ← = contact sent.
|
|
379
387
|
${calendar}
|
|
@@ -404,35 +412,36 @@ STYLE GUIDE:
|
|
|
404
412
|
- If someone has a calendar meeting today, weave that in naturally.
|
|
405
413
|
|
|
406
414
|
${contactBlocks}`,
|
|
407
|
-
|
|
415
|
+
}],
|
|
416
|
+
}),
|
|
408
417
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
.map(b => b.text)
|
|
413
|
-
.join('');
|
|
418
|
+
if (!res.ok || !res.body)
|
|
419
|
+
throw new Error(`OpenRouter ${res.status}`);
|
|
420
|
+
// Stream SSE deltas — yield contacts the moment each block completes
|
|
414
421
|
let section = 'preamble';
|
|
415
422
|
const briefLines = [];
|
|
416
|
-
|
|
423
|
+
let lineBuf = '';
|
|
424
|
+
let sseBuf = '';
|
|
425
|
+
const processLine = (rawLine) => {
|
|
417
426
|
const line = rawLine.trim();
|
|
418
427
|
if (/^CONTACTS:?$/i.test(line)) {
|
|
419
428
|
section = 'contacts';
|
|
420
|
-
|
|
429
|
+
return;
|
|
421
430
|
}
|
|
422
431
|
if (/^BRIEF:?$/i.test(line)) {
|
|
423
432
|
flushContact();
|
|
424
433
|
section = 'brief';
|
|
425
|
-
|
|
434
|
+
return;
|
|
426
435
|
}
|
|
427
436
|
const bracketMatch = line.match(/^\[([^\]]+)\]/);
|
|
428
437
|
if (bracketMatch && (section === 'contacts' || section === 'preamble')) {
|
|
429
438
|
flushContact();
|
|
430
439
|
curName = bracketMatch[1].trim();
|
|
431
440
|
section = 'contacts';
|
|
432
|
-
|
|
441
|
+
return;
|
|
433
442
|
}
|
|
434
443
|
if (!line)
|
|
435
|
-
|
|
444
|
+
return;
|
|
436
445
|
if (section === 'contacts' || section === 'preamble') {
|
|
437
446
|
if (line.startsWith('Priority:')) {
|
|
438
447
|
curPriority = line.replace('Priority:', '').trim().toLowerCase();
|
|
@@ -444,7 +453,38 @@ ${contactBlocks}`,
|
|
|
444
453
|
else if (section === 'brief') {
|
|
445
454
|
briefLines.push(line);
|
|
446
455
|
}
|
|
456
|
+
};
|
|
457
|
+
const decoder = new TextDecoder();
|
|
458
|
+
const reader = res.body.getReader();
|
|
459
|
+
while (true) {
|
|
460
|
+
const { done, value } = await reader.read();
|
|
461
|
+
if (done)
|
|
462
|
+
break;
|
|
463
|
+
sseBuf += decoder.decode(value, { stream: true });
|
|
464
|
+
const sseLines = sseBuf.split('\n');
|
|
465
|
+
sseBuf = sseLines.pop() || '';
|
|
466
|
+
for (const sseLine of sseLines) {
|
|
467
|
+
if (!sseLine.startsWith('data: ') || sseLine === 'data: [DONE]')
|
|
468
|
+
continue;
|
|
469
|
+
try {
|
|
470
|
+
const json = JSON.parse(sseLine.slice(6));
|
|
471
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
472
|
+
if (!delta)
|
|
473
|
+
continue;
|
|
474
|
+
lineBuf += delta;
|
|
475
|
+
const parts = lineBuf.split('\n');
|
|
476
|
+
lineBuf = parts.pop() || '';
|
|
477
|
+
for (const line of parts)
|
|
478
|
+
processLine(line);
|
|
479
|
+
while (ready.length)
|
|
480
|
+
yield ready.shift();
|
|
481
|
+
}
|
|
482
|
+
catch { }
|
|
483
|
+
}
|
|
447
484
|
}
|
|
485
|
+
// Flush remaining buffer
|
|
486
|
+
if (lineBuf.trim())
|
|
487
|
+
processLine(lineBuf);
|
|
448
488
|
flushContact();
|
|
449
489
|
while (ready.length)
|
|
450
490
|
yield ready.shift();
|
|
@@ -452,7 +492,7 @@ ${contactBlocks}`,
|
|
|
452
492
|
opts.onBrief(briefLines.join(' '));
|
|
453
493
|
}
|
|
454
494
|
catch (err) {
|
|
455
|
-
|
|
495
|
+
// silent — contacts still show without enrichment
|
|
456
496
|
}
|
|
457
497
|
for (const t of top) {
|
|
458
498
|
if (!yielded.has(t.name) && t.recentSnippets?.length)
|
package/dist/tui.d.ts
CHANGED
|
@@ -17,11 +17,6 @@ export declare const HD: import("chalk").ChalkInstance;
|
|
|
17
17
|
export declare function renderIntro(name?: string): Promise<void>;
|
|
18
18
|
export declare function renderGreeting(name?: string): void;
|
|
19
19
|
export declare function renderContextLine(): void;
|
|
20
|
-
export declare function renderReveal(stats: {
|
|
21
|
-
conversations: number;
|
|
22
|
-
innerCircle: number;
|
|
23
|
-
aboutToBreak: number;
|
|
24
|
-
}): Promise<void>;
|
|
25
20
|
export declare function renderCRMList(contacts: {
|
|
26
21
|
name: string;
|
|
27
22
|
lastMsg: {
|
package/dist/tui.js
CHANGED
|
@@ -39,19 +39,21 @@ async function typewrite(text, style, delay = 30) {
|
|
|
39
39
|
await _sleep(delay);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
async function
|
|
42
|
+
async function fetchLocationWeather() {
|
|
43
43
|
try {
|
|
44
44
|
const ac = new AbortController();
|
|
45
|
-
const t = setTimeout(() => ac.abort(),
|
|
46
|
-
const r = await fetch(`https://wttr.in
|
|
45
|
+
const t = setTimeout(() => ac.abort(), 3000);
|
|
46
|
+
const r = await fetch(`https://wttr.in/?format=%l|%t|%C`, { signal: ac.signal, headers: { 'User-Agent': 'life-pulse' } });
|
|
47
47
|
clearTimeout(t);
|
|
48
48
|
const raw = (await r.text()).trim();
|
|
49
|
-
const [temp, cond] = raw.split('|');
|
|
50
|
-
if (!temp || !cond)
|
|
49
|
+
const [loc, temp, cond] = raw.split('|');
|
|
50
|
+
if (!loc || !temp || !cond)
|
|
51
51
|
return null;
|
|
52
|
+
// loc comes back as "City, Region, Country" — take just the city
|
|
53
|
+
const city = loc.split(',')[0].trim();
|
|
52
54
|
const deg = temp.replace(/[+\s]/g, '').replace(/°[CF]/, '°');
|
|
53
55
|
const sky = cond.trim().toLowerCase();
|
|
54
|
-
return `${deg} and ${sky}
|
|
56
|
+
return { city, weather: `${deg} and ${sky}` };
|
|
55
57
|
}
|
|
56
58
|
catch {
|
|
57
59
|
return null;
|
|
@@ -118,10 +120,8 @@ function renderScanField(cols, rows) {
|
|
|
118
120
|
}
|
|
119
121
|
export async function renderIntro(name) {
|
|
120
122
|
const firstName = name?.split(' ')[0] || name;
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
// Fire weather fetch while animation runs
|
|
124
|
-
const weatherP = city ? fetchWeather(city) : Promise.resolve(null);
|
|
123
|
+
// Fire IP-based location + weather fetch while animation runs
|
|
124
|
+
const locP = fetchLocationWeather();
|
|
125
125
|
const cols = process.stdout.columns || 80;
|
|
126
126
|
const rows = process.stdout.rows || 24;
|
|
127
127
|
// Push old content out of scrollback
|
|
@@ -144,12 +144,10 @@ export async function renderIntro(name) {
|
|
|
144
144
|
await typewrite(hey, G1, 35);
|
|
145
145
|
process.stdout.write('\n');
|
|
146
146
|
await _sleep(250);
|
|
147
|
-
// "I see you're in
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
: city ? `I see you're in ${city}.` : '';
|
|
152
|
-
if (locLine) {
|
|
147
|
+
// "I see you're in Short Hills. 42° and cloudy."
|
|
148
|
+
const loc = await locP;
|
|
149
|
+
if (loc) {
|
|
150
|
+
const locLine = `I see you're in ${loc.city}. ${weatherRemark(loc.weather)}`;
|
|
153
151
|
process.stdout.write(' ');
|
|
154
152
|
await typewrite(locLine, G2, 20);
|
|
155
153
|
process.stdout.write('\n');
|
|
@@ -157,11 +155,20 @@ export async function renderIntro(name) {
|
|
|
157
155
|
}
|
|
158
156
|
// "Hope you're having an incredible morning."
|
|
159
157
|
const h = new Date().getHours();
|
|
158
|
+
const greeting = timeGreeting(h);
|
|
160
159
|
process.stdout.write(' ');
|
|
161
|
-
await typewrite(
|
|
160
|
+
await typewrite(greeting, G3, 18);
|
|
162
161
|
process.stdout.write('\n\n');
|
|
162
|
+
await _sleep(1200);
|
|
163
|
+
// Build greeting lines for Ink to preserve
|
|
164
|
+
const greetLines = [''];
|
|
165
|
+
greetLines.push(` ${G1(hey)}`);
|
|
166
|
+
if (loc)
|
|
167
|
+
greetLines.push(` ${G2(`I see you're in ${loc.city}. ${weatherRemark(loc.weather)}`)}`);
|
|
168
|
+
greetLines.push(` ${G3(greeting)}`);
|
|
169
|
+
greetLines.push('');
|
|
163
170
|
if (USE_INK)
|
|
164
|
-
ink.initInk();
|
|
171
|
+
ink.initInk(greetLines);
|
|
165
172
|
}
|
|
166
173
|
// ─── Greeting (legacy / non-interactive fallback) ───
|
|
167
174
|
export function renderGreeting(name) {
|
|
@@ -186,21 +193,6 @@ export function renderContextLine() {
|
|
|
186
193
|
out(` ${C.dim(`${days[d.getDay()]} · ${h12}:${min} ${ampm}`)}`);
|
|
187
194
|
out('');
|
|
188
195
|
}
|
|
189
|
-
// ─── Reveal (the moment after discovery) ───
|
|
190
|
-
export async function renderReveal(stats) {
|
|
191
|
-
await _sleep(600);
|
|
192
|
-
process.stdout.write(' ');
|
|
193
|
-
await typewrite(`${stats.conversations} conversations.`, G2, 22);
|
|
194
|
-
await _sleep(400);
|
|
195
|
-
process.stdout.write(' ');
|
|
196
|
-
await typewrite(`${stats.innerCircle} people who matter.`, G1, 25);
|
|
197
|
-
await _sleep(500);
|
|
198
|
-
if (stats.aboutToBreak > 0) {
|
|
199
|
-
process.stdout.write(' ');
|
|
200
|
-
await typewrite(`${stats.aboutToBreak} thread${stats.aboutToBreak === 1 ? '' : 's'} about to break.`, chalk.bold.hex('#c0caf5'), 28);
|
|
201
|
-
}
|
|
202
|
-
process.stdout.write('\n\n');
|
|
203
|
-
}
|
|
204
196
|
// ─── CRM List ───
|
|
205
197
|
export function renderCRMList(contacts) {
|
|
206
198
|
for (const t of contacts.slice(0, 12)) {
|
|
@@ -298,14 +290,12 @@ export async function renderCRMStream(source, spinner) {
|
|
|
298
290
|
out('');
|
|
299
291
|
first = false;
|
|
300
292
|
}
|
|
301
|
-
|
|
302
|
-
const ago = shortAgo(t.lastMsg?.ago || '');
|
|
303
|
-
const gap = Math.max(2, w - 4 - name.length - ago.length);
|
|
304
|
-
out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
|
|
293
|
+
out(` ${C.hd(t.name.slice(0, 40))}`);
|
|
305
294
|
if (t.relationship) {
|
|
306
295
|
out(` ${C.mid(t.relationship.slice(0, w - 8))}`);
|
|
307
296
|
}
|
|
308
297
|
rendered.push(t);
|
|
298
|
+
await _sleep(180);
|
|
309
299
|
}
|
|
310
300
|
if (first)
|
|
311
301
|
spinner?.stop();
|
|
@@ -314,12 +304,10 @@ export async function renderCRMStream(source, spinner) {
|
|
|
314
304
|
}
|
|
315
305
|
export async function renderCRMContact(t) {
|
|
316
306
|
const w = W();
|
|
317
|
-
|
|
318
|
-
const ago = shortAgo(t.lastMsg?.ago || '');
|
|
319
|
-
const gap = Math.max(2, w - 4 - name.length - ago.length);
|
|
320
|
-
out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
|
|
307
|
+
out(` ${C.hd(t.name.slice(0, 40))}`);
|
|
321
308
|
if (t.relationship)
|
|
322
309
|
out(` ${C.mid(t.relationship.slice(0, w - 8))}`);
|
|
310
|
+
await _sleep(180);
|
|
323
311
|
}
|
|
324
312
|
// ─── Destroy (call at exit) ───
|
|
325
313
|
export function destroyUI() {
|
package/dist/ui/app.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface CardData {
|
|
|
17
17
|
who?: string;
|
|
18
18
|
urgency?: string;
|
|
19
19
|
}
|
|
20
|
-
export declare function initInk(): void;
|
|
20
|
+
export declare function initInk(seedLines?: string[]): void;
|
|
21
21
|
export declare function destroy(): void;
|
|
22
22
|
export declare function log(text: string): void;
|
|
23
23
|
export declare function gap(): void;
|
package/dist/ui/app.js
CHANGED
|
@@ -25,13 +25,13 @@ const Bar = ({ done, total }) => {
|
|
|
25
25
|
return (_jsxs(Text, { children: [_jsx(Text, { color: "#565f89", children: '─'.repeat(f) }), _jsx(Text, { color: "#292e42", children: '─'.repeat(BAR_W - f) }), ' ', _jsx(Text, { color: "#565f89", children: done }), _jsx(Text, { color: "#3b4261", children: "/" }), _jsx(Text, { color: "#565f89", children: total })] }));
|
|
26
26
|
};
|
|
27
27
|
const ProgressView = ({ progress, frame }) => {
|
|
28
|
-
const spin = (off = 0) => BAR_CHARS[(frame + off) % BAR_CHARS.length];
|
|
28
|
+
const spin = (off = 0) => BAR_CHARS[(Math.floor((frame + off) / 5)) % BAR_CHARS.length];
|
|
29
29
|
if (progress.thinking && !progress.items.length && !progress.total) {
|
|
30
30
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ' ' }), _jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: "thinking" })] })] }));
|
|
31
31
|
}
|
|
32
32
|
const { phase, done, total, items } = progress;
|
|
33
33
|
const allDone = done === total && total > 0;
|
|
34
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ' ' }), allDone ? (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ' ' }), allDone ? (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: phase === 'scanning' ? 'scanned' : 'putting it together' })] })) : total > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [' ', _jsx(Text, { color: "#565f89", children: phase || 'working' }), ' ', _jsx(Bar, { done: done, total: total })] }), items.slice(0, MAX_VIS).map((item, i) => (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin(i) }), ' ', _jsx(Text, { color: "#565f89", children: item.label }), item.tool && _jsxs(Text, { color: "#3b4261", children: [' — ', item.tool] })] }, i))), items.length > MAX_VIS && _jsxs(Text, { color: "#3b4261", children: [' ', "+", items.length - MAX_VIS, " more"] })] })) : null, progress.thinking && total > 0 && (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: "thinking" })] }))] }));
|
|
35
35
|
};
|
|
36
36
|
const CardView = ({ card, num, sel }) => {
|
|
37
37
|
const opts = card.options || [];
|
|
@@ -89,6 +89,7 @@ const App = () => {
|
|
|
89
89
|
let _inst = null;
|
|
90
90
|
let _started = false;
|
|
91
91
|
const _origLog = console.log.bind(console);
|
|
92
|
+
let _seedLines = [];
|
|
92
93
|
function ensureStarted() {
|
|
93
94
|
if (_started)
|
|
94
95
|
return;
|
|
@@ -96,6 +97,15 @@ function ensureStarted() {
|
|
|
96
97
|
process.stdout.write('\x1Bc'); // clear screen
|
|
97
98
|
process.stdout.write('\x1B[?25l'); // hide cursor
|
|
98
99
|
_inst = inkRender(React.createElement(App));
|
|
100
|
+
// Seed initial lines (e.g. greeting from intro)
|
|
101
|
+
if (_seedLines.length) {
|
|
102
|
+
const lines = _seedLines;
|
|
103
|
+
_seedLines = [];
|
|
104
|
+
// Small delay to let React mount before pushing state
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
_update?.(p => ({ ...p, lines: [...lines, ...p.lines] }));
|
|
107
|
+
}, 50);
|
|
108
|
+
}
|
|
99
109
|
// Redirect console.log through Ink
|
|
100
110
|
console.log = (...args) => {
|
|
101
111
|
const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
|
|
@@ -105,7 +115,11 @@ function ensureStarted() {
|
|
|
105
115
|
};
|
|
106
116
|
}
|
|
107
117
|
// ─── Exports ──────────────────────────────────────
|
|
108
|
-
export function initInk() {
|
|
118
|
+
export function initInk(seedLines) {
|
|
119
|
+
if (seedLines)
|
|
120
|
+
_seedLines = seedLines;
|
|
121
|
+
ensureStarted();
|
|
122
|
+
}
|
|
109
123
|
export function destroy() {
|
|
110
124
|
console.log = _origLog;
|
|
111
125
|
_inst?.unmount();
|
package/dist/ui/progress.js
CHANGED
|
@@ -43,14 +43,14 @@ export class InkProgress {
|
|
|
43
43
|
const workersDone = this.workers.filter(w => w.done).length;
|
|
44
44
|
const activeWorkers = this.workers.filter(w => !w.done);
|
|
45
45
|
if (this.workers.length > 0) {
|
|
46
|
-
showProgress(activeWorkers.length > 0 ? 'working' : '
|
|
46
|
+
showProgress(activeWorkers.length > 0 ? 'working' : 'finishing up', workersDone, this.workers.length, activeWorkers.map(w => ({
|
|
47
47
|
label: w.label,
|
|
48
48
|
tool: w.tool ? this.s(w.tool) : undefined,
|
|
49
49
|
})), this._thinking);
|
|
50
50
|
}
|
|
51
51
|
else if (this.tools.length > 0) {
|
|
52
52
|
const active = this.tools.filter(t => !t.done);
|
|
53
|
-
showProgress(toolsDone === this.tools.length ? '
|
|
53
|
+
showProgress(toolsDone === this.tools.length ? 'finishing up' : 'scanning', toolsDone, this.tools.length, active.map(t => ({ label: this.s(t.name) })), this._thinking);
|
|
54
54
|
}
|
|
55
55
|
else if (this._thinking) {
|
|
56
56
|
showProgress('', 0, 0, [], true);
|
|
@@ -71,7 +71,8 @@ export class InkProgress {
|
|
|
71
71
|
const t = this.tools.find(t => t.name === name && !t.done);
|
|
72
72
|
if (t)
|
|
73
73
|
t.done = true;
|
|
74
|
-
|
|
74
|
+
// Delay repaint so the label lingers on screen
|
|
75
|
+
setTimeout(() => this.paint(), 400);
|
|
75
76
|
}
|
|
76
77
|
workerStart(id, label) {
|
|
77
78
|
this._thinking = false;
|
|
@@ -113,6 +114,9 @@ export class InkProgress {
|
|
|
113
114
|
}
|
|
114
115
|
pause() { this._paused = true; hideProgress(); }
|
|
115
116
|
resume() { this._paused = false; this.paint(); }
|
|
116
|
-
stop() {
|
|
117
|
+
stop() {
|
|
118
|
+
// Let the final state linger so it doesn't vanish instantly
|
|
119
|
+
setTimeout(() => hideProgress(), 800);
|
|
120
|
+
}
|
|
117
121
|
clear() { hideProgress(); }
|
|
118
122
|
}
|
package/dist/ui/theme.js
CHANGED
|
@@ -51,7 +51,7 @@ export const W = () => Math.min(process.stdout.columns || 80, 80);
|
|
|
51
51
|
export const INNER = () => W() - 4;
|
|
52
52
|
// Spinner frames
|
|
53
53
|
export const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
54
|
-
export const BAR_CHARS = ['
|
|
54
|
+
export const BAR_CHARS = ['✶', '*', '✢'];
|
|
55
55
|
// Progress label map — personality, not developer console
|
|
56
56
|
export const TOOL_LABEL = {
|
|
57
57
|
search_all_messages: 'reading the room',
|