harlay 0.3.0 → 0.4.0
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/LICENSE +40 -0
- package/README.md +2 -2
- package/app/dist/main.cjs +1268 -205
- package/app/renderer/overlay.js +4 -3
- package/app/renderer/popover.css +13 -5
- package/app/renderer/popover.js +101 -37
- package/dist/harlay.mjs +409 -81
- package/package.json +2 -2
package/app/renderer/overlay.js
CHANGED
|
@@ -7,9 +7,9 @@ const stage = document.getElementById('stage');
|
|
|
7
7
|
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
8
8
|
const MARK = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 20 10 4h4.3L6.8 20z" fill="#fff" opacity=".6"/><path d="M10.2 20 17.7 4H22l-7.5 16z" fill="#fff"/></svg>';
|
|
9
9
|
const CHECK = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3.5 8.5 3 3 6-7"/></svg>';
|
|
10
|
-
const BADGE = { 'claude-code': 'CC', codex: 'CX', cursor: 'CU' };
|
|
10
|
+
const BADGE = { 'claude-code': 'CC', codex: 'CX', cursor: 'CU', 'claude-web': 'C', chatgpt: 'G' };
|
|
11
11
|
/** How long the widget stays when the agent doesn't pick the handoff up meanwhile. */
|
|
12
|
-
const STAY_MS = { app: 10_000, terminal: 8_000 };
|
|
12
|
+
const STAY_MS = { app: 10_000, terminal: 8_000, web: 10_000 };
|
|
13
13
|
/** How long it stays once the agent did. */
|
|
14
14
|
const GOT_MS = 1_900;
|
|
15
15
|
|
|
@@ -90,7 +90,8 @@ function fillPill(p, m) {
|
|
|
90
90
|
agentIcon(m.to, txt);
|
|
91
91
|
el('b', '', txt).textContent = m.to.label;
|
|
92
92
|
const act = el('span', 'act', p);
|
|
93
|
-
|
|
93
|
+
// An app or a web chat waits for Enter; a terminal starts at once.
|
|
94
|
+
if (m.to.mode === 'app' || m.to.mode === 'web') {
|
|
94
95
|
el('kbd', '', act).textContent = '⏎';
|
|
95
96
|
act.append('to send');
|
|
96
97
|
} else {
|
package/app/renderer/popover.css
CHANGED
|
@@ -246,16 +246,24 @@ kbd { margin-right: 6px; padding: 1px 5px; border-radius: 5px; border: 1px solid
|
|
|
246
246
|
.tile-meter i { display: block; height: 100%; background: var(--accent); }
|
|
247
247
|
.tile-meter i.warm { background: var(--long); }
|
|
248
248
|
.tile-meter i.hot { background: var(--bad); }
|
|
249
|
-
.tile-key { position: absolute; top: 5px; right: 6px; font: 500 9px var(--mono); color: var(--fg3); opacity: 0; transition: opacity .15s; }
|
|
250
|
-
.tile.sel .tile-key, .tile:hover .tile-key { opacity: 1; }
|
|
251
249
|
.r-busy { justify-content: center; height: 110px; }
|
|
252
|
-
.r-
|
|
253
|
-
.r-
|
|
250
|
+
.r-more { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 2px; margin: 8px 12px 2px; }
|
|
251
|
+
.r-more-k { font-size: 11.5px; color: var(--fg3); margin-right: 2px; }
|
|
252
|
+
.chip { position: relative; display: inline-flex; align-items: center; gap: 7px; height: 28px; padding: 0 8px 0 5px; border-radius: 9px; border: 1px solid transparent; font-size: 11.5px; font-weight: 500; color: var(--fg2); transition: background-color .15s, border-color .15s; }
|
|
253
|
+
.chip.sel { background: var(--card); border-color: rgba(var(--accent-rgb), .45); color: var(--fg); }
|
|
254
|
+
.chip .agent { width: 18px; height: 18px; border-radius: 5px; }
|
|
255
|
+
.chip .agent.badge { font-size: 8px; }
|
|
256
|
+
.chip .corner { right: -4px; bottom: -3px; min-width: 11px; height: 11px; padding: 0 1px; border-radius: 4px; }
|
|
257
|
+
.chip .corner svg { width: 8px; height: 8px; }
|
|
258
|
+
.chip .corner.mono { font-size: 6px; }
|
|
259
|
+
.r-folder { margin: 6px 16px 0; text-align: center; font-size: 11.5px; color: var(--fg3); }
|
|
260
|
+
.r-folder .link { font-size: 11.5px; font-weight: 600; }
|
|
261
|
+
.agent.badge.globe svg { width: 11px; height: 11px; }
|
|
262
|
+
.r-foot { display: flex; align-items: center; justify-content: flex-end; margin: 6px 12px 10px; }
|
|
254
263
|
.r-acts { display: flex; align-items: center; gap: 14px; }
|
|
255
264
|
.link.r-dismiss { color: var(--fg3); }
|
|
256
265
|
.link.r-dismiss:hover { color: var(--fg); }
|
|
257
266
|
.r-alert { margin: 4px 16px 8px; font-size: 12px; font-weight: 500; color: var(--bad); }
|
|
258
|
-
.r-hint kbd { padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line3); font: 500 10px var(--mono); color: var(--fg3); }
|
|
259
267
|
|
|
260
268
|
/* chat health: a battery with one look per state (fresh, long, struggling), and the state in words in the card */
|
|
261
269
|
.battery { width: 22px; height: 11px; overflow: visible; }
|
package/app/renderer/popover.js
CHANGED
|
@@ -12,11 +12,12 @@ const ICON = {
|
|
|
12
12
|
fresh: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 8a5 5 0 1 1-1.6-3.7"/><path d="M13.2 2.5v3h-3"/></svg>',
|
|
13
13
|
gauge: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 11.5a6 6 0 1 1 11 0"/><path d="M8 9.5 10.8 6"/><circle cx="8" cy="10" r=".9" fill="currentColor"/></svg>',
|
|
14
14
|
list: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5.5 4h8M5.5 8h8M5.5 12h8"/><circle cx="2.6" cy="4" r=".6" fill="currentColor"/><circle cx="2.6" cy="8" r=".6" fill="currentColor"/><circle cx="2.6" cy="12" r=".6" fill="currentColor"/></svg>',
|
|
15
|
+
globe: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="5.6"/><path d="M2.6 8h10.8M8 2.4c1.6 1.6 2.3 3.5 2.3 5.6S9.6 12 8 13.6M8 2.4C6.4 4 5.7 5.9 5.7 8S6.4 12 8 13.6"/></svg>',
|
|
15
16
|
copy: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="5.5" y="5.5" width="8" height="8" rx="1.6"/><path d="M10.5 3.2V3a.9.9 0 0 0-.9-.9H3.4a.9.9 0 0 0-.9.9v6.2a.9.9 0 0 0 .9.9h.3"/></svg>',
|
|
16
17
|
};
|
|
17
18
|
const TITLE = { usage: 'Usage', settings: 'Settings' };
|
|
18
|
-
const BADGE = { 'claude-code': 'CC', codex: 'CX', cursor: 'CU' };
|
|
19
|
-
const AGENT_LABEL = { 'claude-code': 'Claude Code', codex: 'Codex', cursor: 'Cursor' };
|
|
19
|
+
const BADGE = { 'claude-code': 'CC', codex: 'CX', cursor: 'CU', 'claude-web': 'C', chatgpt: 'G' };
|
|
20
|
+
const AGENT_LABEL = { 'claude-code': 'Claude Code', codex: 'Codex', cursor: 'Cursor', 'claude-web': 'claude.ai', chatgpt: 'ChatGPT' };
|
|
20
21
|
const HEALTH_LABEL = { fresh: 'Healthy', long: 'Getting long', struggling: 'Running low' };
|
|
21
22
|
|
|
22
23
|
let model = null;
|
|
@@ -187,11 +188,24 @@ function renderGroup(g) {
|
|
|
187
188
|
g.sessions.length ? g.sessions.map((s) => renderSession({ ...s, agentLabel: g.label })) : h('p', { class: 'none' }, 'No chats yet'));
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/** Web chats Harlay read (⌥H on one, or a limit it spotted), first: each opens where it can go, to switch. */
|
|
192
|
+
function renderWebGroup() {
|
|
193
|
+
if (!model.webChats?.length) return null;
|
|
194
|
+
return h('section', { class: 'grp' },
|
|
195
|
+
h('div', { class: 'grp-h' }, h('span', { class: 'agent badge globe', 'aria-hidden': 'true' }, icon('globe')), h('span', { class: 'grp-name' }, 'Web chats')),
|
|
196
|
+
model.webChats.map((s) =>
|
|
197
|
+
h('div', { class: 'item' },
|
|
198
|
+
h('button', { class: 'row', title: s.title, onclick: () => { routeKey = s.key; routeSel = 0; go('route'); } },
|
|
199
|
+
h('span', { class: `st ${s.status}` }, s.status === 'idle' ? null : h('i')),
|
|
200
|
+
h('span', { class: 'title' }, s.title),
|
|
201
|
+
h('span', { class: 'time' }, s.ago)))));
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
function renderHome() {
|
|
191
205
|
const { groups } = model;
|
|
192
206
|
put('#home',
|
|
193
|
-
groups.length
|
|
194
|
-
? h('div', { class: 'scroll' }, groups.map(renderGroup))
|
|
207
|
+
groups.length || model.webChats?.length
|
|
208
|
+
? h('div', { class: 'scroll' }, renderWebGroup(), groups.map(renderGroup))
|
|
195
209
|
: h('p', { class: 'empty' }, 'Install Claude Code, Codex or Cursor and their chats show up here.'),
|
|
196
210
|
model.usage?.length ? h('button', { class: 'more', onclick: () => go('usage') }, icon('gauge', 'gauge'), 'Check agent usage', icon('arrow', 'arrow')) : null);
|
|
197
211
|
// Keep focus in the add-a-chat field across re-renders.
|
|
@@ -277,24 +291,40 @@ function renderSettings() {
|
|
|
277
291
|
{ value: 'dark', label: 'Dark' },
|
|
278
292
|
], settings.theme, (v) => api.act({ type: 'setTheme', theme: v }))),
|
|
279
293
|
toggleRow('Auto-continue', 'Hand off without asking when an agent stops', settings.autoContinue, { type: 'toggleAuto' }),
|
|
294
|
+
toggleRow(
|
|
295
|
+
'Web chats',
|
|
296
|
+
settings.webChats && !settings.webChatsAllowed
|
|
297
|
+
? 'Allow Harlay in System Settings › Privacy & Security › Accessibility.'
|
|
298
|
+
: 'Spots limits on claude.ai and ChatGPT, and ⌥H moves the one in front.',
|
|
299
|
+
settings.webChats,
|
|
300
|
+
{ type: 'toggleWebChats' }),
|
|
280
301
|
toggleRow('Open at login', null, settings.openAtLogin, { type: 'toggleLogin' })));
|
|
281
302
|
}
|
|
282
303
|
|
|
283
304
|
function findSession(key) {
|
|
284
305
|
for (const g of model.groups) for (const s of g.sessions) if (s.key === key) return s;
|
|
285
|
-
return null;
|
|
306
|
+
return model.webChats?.find((s) => s.key === key) ?? null;
|
|
286
307
|
}
|
|
287
308
|
|
|
309
|
+
/** The agent a chat is in, or a web chat's site. */
|
|
288
310
|
function findTool(key) {
|
|
289
311
|
for (const g of model.groups) if (g.sessions.some((s) => s.key === key)) return g.tool;
|
|
290
|
-
return null;
|
|
312
|
+
return model.webChats?.find((s) => s.key === key)?.web ?? null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** For a web chat: the folder its agents open in, and a way to change it. */
|
|
316
|
+
function folderLine(s) {
|
|
317
|
+
if (!s.web || !s.targets.some((t) => t.mode !== 'web')) return null;
|
|
318
|
+
const name = model.agentFolder ? model.agentFolder.split('/').filter(Boolean).pop() : null;
|
|
319
|
+
const pick = (label) => h('button', { class: 'link', title: model.agentFolder ? `${model.agentFolder} · click to change` : 'Pick a folder', onclick: () => api.act({ type: 'pickFolder', key: s.key }) }, label);
|
|
320
|
+
return h('p', { class: 'r-folder' }, name ? ['Agents open in ', pick(name)] : ['Agents need a ', pick('folder')]);
|
|
291
321
|
}
|
|
292
322
|
|
|
293
323
|
// ---- router (⌥H): the latest chat, branching to the agents it can continue in ----
|
|
294
324
|
|
|
295
|
-
/** Usage of an agent's fullest window, for the thin meter under its icon. */
|
|
325
|
+
/** Usage of an agent's fullest window, for the thin meter under its icon. claude.ai draws on Claude Code's plan. */
|
|
296
326
|
function usageOf(tool) {
|
|
297
|
-
const row = model.usage?.find((u) => u.tool === tool);
|
|
327
|
+
const row = model.usage?.find((u) => u.tool === (tool === 'claude-web' ? 'claude-code' : tool));
|
|
298
328
|
if (!row || !row.windows.length) return null;
|
|
299
329
|
return Math.max(...row.windows.map((w) => w.pct));
|
|
300
330
|
}
|
|
@@ -323,12 +353,31 @@ function routeLines(n, selected) {
|
|
|
323
353
|
return svg;
|
|
324
354
|
}
|
|
325
355
|
|
|
326
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* The router's choices: tiles for what's offered first (other agents and a new chat for a chat about code, web chats for
|
|
358
|
+
* one that only talked), and a row of smaller choices for the rest. `all` is both, in order, for the keys.
|
|
359
|
+
*/
|
|
327
360
|
function routeChoices(s, tool) {
|
|
328
|
-
const
|
|
361
|
+
const pick = (t) => ({ tool: t.tool, label: t.label, mode: t.mode, act: { type: 'handoff', key: s.key, target: t.tool } });
|
|
362
|
+
let tiles = s.targets.filter((t) => t.primary).map(pick);
|
|
363
|
+
let more = s.targets.filter((t) => !t.primary).map(pick);
|
|
329
364
|
// Not when this agent just hit its limit: a new chat with it would stop too.
|
|
330
|
-
if (s.canStartFresh && s.status !== 'limit')
|
|
331
|
-
|
|
365
|
+
if (s.canStartFresh && s.status !== 'limit') (s.kind === 'chat' ? more : tiles).push({ tool, label: 'New chat', mode: null, fresh: true, act: { type: 'freshStart', key: s.key } });
|
|
366
|
+
if (!tiles.length) [tiles, more] = [more, []];
|
|
367
|
+
return { tiles, more, all: [...tiles, ...more] };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The small mark on a choice's icon: a new chat, a terminal, or the browser. */
|
|
371
|
+
function cornerOf(c) {
|
|
372
|
+
if (c.fresh) return h('span', { class: 'corner' }, icon('fresh'));
|
|
373
|
+
if (c.mode === 'terminal') return h('span', { class: 'corner mono' }, '>_');
|
|
374
|
+
if (c.mode === 'web') return h('span', { class: 'corner' }, icon('globe'));
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function choiceTitle(c) {
|
|
379
|
+
if (c.fresh) return 'New chat that picks up where this one left off';
|
|
380
|
+
return `Continue in ${c.label}${c.mode === 'terminal' ? ' (Terminal)' : c.mode === 'web' ? ' in your browser' : ''}`;
|
|
332
381
|
}
|
|
333
382
|
|
|
334
383
|
let routeKey = null;
|
|
@@ -338,22 +387,20 @@ function renderRoute() {
|
|
|
338
387
|
const s = routeKey ? findSession(routeKey) : null;
|
|
339
388
|
if (!s) return go('home');
|
|
340
389
|
const tool = findTool(s.key);
|
|
341
|
-
const
|
|
342
|
-
routeSel = Math.min(routeSel, Math.max(0,
|
|
390
|
+
const { tiles, more, all } = routeChoices(s, tool);
|
|
391
|
+
routeSel = Math.min(routeSel, Math.max(0, all.length - 1));
|
|
392
|
+
const hover = (i) => () => { if (routeSel !== i) { routeSel = i; render(); } };
|
|
343
393
|
const tile = (c, i) => {
|
|
344
394
|
const pct = c.fresh ? null : usageOf(c.tool);
|
|
345
395
|
return h('button', {
|
|
346
396
|
class: `tile${i === routeSel ? ' sel' : ''}`,
|
|
347
|
-
title: c
|
|
348
|
-
onmouseenter: (
|
|
397
|
+
title: choiceTitle(c),
|
|
398
|
+
onmouseenter: hover(i),
|
|
349
399
|
onclick: () => api.act(c.act),
|
|
350
400
|
},
|
|
351
|
-
h('span', { class: 'tile-ic' },
|
|
352
|
-
agentIcon(c.tool),
|
|
353
|
-
c.fresh ? h('span', { class: 'corner' }, icon('fresh')) : c.mode === 'terminal' ? h('span', { class: 'corner mono' }, '>_') : null),
|
|
401
|
+
h('span', { class: 'tile-ic' }, agentIcon(c.tool), cornerOf(c)),
|
|
354
402
|
h('span', { class: 'tile-name' }, c.label),
|
|
355
|
-
pct === null ? h('span', { class: 'tile-meter' }) : h('span', { class: 'tile-meter on', title: `${pct}% of its limit used` }, fill(pct, pct >= 90 ? 'hot' : pct >= 70 ? 'warm' : ''))
|
|
356
|
-
h('kbd', { class: 'tile-key' }, String(i + 1)));
|
|
403
|
+
pct === null ? h('span', { class: 'tile-meter' }) : h('span', { class: 'tile-meter on', title: `${pct}% of its limit used` }, fill(pct, pct >= 90 ? 'hot' : pct >= 70 ? 'warm' : '')));
|
|
357
404
|
};
|
|
358
405
|
put('#route',
|
|
359
406
|
// Opened by itself because this chat hit its limit: say so, and when it resets.
|
|
@@ -364,14 +411,23 @@ function renderRoute() {
|
|
|
364
411
|
h('span', { class: 'r-src-time' }, s.ago)),
|
|
365
412
|
s.busy
|
|
366
413
|
? h('div', { class: 'busy r-busy' }, h('span', { class: 'spin' }))
|
|
367
|
-
:
|
|
368
|
-
? [
|
|
414
|
+
: tiles.length
|
|
415
|
+
? [
|
|
416
|
+
routeLines(tiles.length, routeSel),
|
|
417
|
+
h('div', { class: `r-tiles n${tiles.length}` }, tiles.map(tile)),
|
|
418
|
+
more.length
|
|
419
|
+
? h('div', { class: 'r-more' }, h('span', { class: 'r-more-k' }, 'Or'), more.map((c, j) => {
|
|
420
|
+
const i = tiles.length + j;
|
|
421
|
+
return h('button', { class: `chip${i === routeSel ? ' sel' : ''}`, title: choiceTitle(c), onmouseenter: hover(i), onclick: () => api.act(c.act) },
|
|
422
|
+
h('span', { class: 'tile-ic' }, agentIcon(c.tool), cornerOf(c)), c.label);
|
|
423
|
+
}))
|
|
424
|
+
: null,
|
|
425
|
+
folderLine(s),
|
|
426
|
+
]
|
|
369
427
|
: h('p', { class: 'none' }, 'No other agent installed'),
|
|
370
428
|
h('div', { class: 'r-foot' },
|
|
371
|
-
h('span', { class: 'r-hint' }, h('kbd', {}, '↵'), h('kbd', {}, '1–' + Math.max(1, choices.length))),
|
|
372
429
|
h('span', { class: 'r-acts' },
|
|
373
|
-
h('button', { class: 'link r-dismiss', onclick: () => api.hide() }, 'Not now')
|
|
374
|
-
h('button', { class: 'link r-all', onclick: () => go('home') }, icon('list'), 'All chats'))));
|
|
430
|
+
h('button', { class: 'link r-dismiss', onclick: () => api.hide() }, 'Not now'))));
|
|
375
431
|
}
|
|
376
432
|
|
|
377
433
|
// ---- a handoff as it runs (see presentHandoff in main.ts) ----
|
|
@@ -415,9 +471,7 @@ function renderHandoff() {
|
|
|
415
471
|
h('span', { class: 'hf-end' }, agentIcon(hf.from.tool), h('span', { class: 'hf-name' }, hf.from.label)),
|
|
416
472
|
h('span', { class: 'hf-line', 'aria-hidden': 'true' }, h('i')),
|
|
417
473
|
h('span', { class: 'hf-end to' },
|
|
418
|
-
h('span', { class: 'tile-ic' },
|
|
419
|
-
agentIcon(hf.to.tool),
|
|
420
|
-
fresh ? h('span', { class: 'corner' }, icon('fresh')) : hf.to.mode === 'terminal' ? h('span', { class: 'corner mono' }, '>_') : null),
|
|
474
|
+
h('span', { class: 'tile-ic' }, agentIcon(hf.to.tool), cornerOf({ fresh, mode: hf.to.mode })),
|
|
421
475
|
h('span', { class: 'hf-name' }, hf.to.label))),
|
|
422
476
|
h('p', { class: 'hf-title', title: hf.title }, hf.title),
|
|
423
477
|
h('ol', { class: 'hf-steps' }, hf.steps.map((step) =>
|
|
@@ -503,7 +557,7 @@ $('#quit').addEventListener('click', () => api.act({ type: 'quit' }));
|
|
|
503
557
|
document.addEventListener('keydown', (e) => {
|
|
504
558
|
if (view === 'route' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
505
559
|
const s = routeKey ? findSession(routeKey) : null;
|
|
506
|
-
const choices = s && !s.busy ? routeChoices(s, findTool(s.key)) : [];
|
|
560
|
+
const choices = s && !s.busy ? routeChoices(s, findTool(s.key)).all : [];
|
|
507
561
|
const n = Number(e.key);
|
|
508
562
|
if (Number.isInteger(n) && n >= 1 && n <= choices.length) return api.act(choices[n - 1].act);
|
|
509
563
|
if (e.key === 'Enter' && choices[routeSel]) return api.act(choices[routeSel].act);
|
|
@@ -533,8 +587,9 @@ api.onHandoff?.(onHandoff);
|
|
|
533
587
|
api.ready();
|
|
534
588
|
|
|
535
589
|
function demoApi() {
|
|
536
|
-
const label = { 'claude-code': 'Claude Code', codex: 'Codex', cursor: 'Cursor' };
|
|
537
|
-
const
|
|
590
|
+
const label = { 'claude-code': 'Claude Code', codex: 'Codex', cursor: 'Cursor', 'claude-web': 'claude.ai', chatgpt: 'ChatGPT' };
|
|
591
|
+
const web = ['claude-web', 'chatgpt'];
|
|
592
|
+
const settings = { autoContinue: false, openAtLogin: true, theme: 'system', claudeBridge: 'off', webChats: true, webChatsAllowed: true };
|
|
538
593
|
let asked = false;
|
|
539
594
|
const usage = [
|
|
540
595
|
{ tool: 'claude-code', label: 'Claude Code', windows: [{ label: '5h', pct: 72, exact: false, resets: 'resets 4:10 PM' }], pace: '≈ 38 min to limit at this pace', note: '≈ estimated from your session logs', usagePage: false },
|
|
@@ -549,12 +604,12 @@ function demoApi() {
|
|
|
549
604
|
reasons: ['`npm test -- auth` failed 4 times in a row', 'You pushed back 2 times in your last 6 messages', 'Compacted 2 times, so early details are summaries'],
|
|
550
605
|
context: { tokens: 188_000, window: 258_400, compactions: 2 },
|
|
551
606
|
};
|
|
552
|
-
const chat = (tool, id, title, ago, health, status = 'idle', detail = 'Idle', pinned = false) =>
|
|
553
|
-
({ key: `${tool}:${id}`, tool, title, ago, status, detail, cwd: `/Users/you/${title.split(' ')[0].toLowerCase()}`, busy: false, pinned, health, canStartFresh: true, delivery: null });
|
|
607
|
+
const chat = (tool, id, title, ago, health, status = 'idle', detail = 'Idle', pinned = false, kind = 'coding') =>
|
|
608
|
+
({ key: `${tool}:${id}`, tool, title, ago, status, detail, cwd: `/Users/you/${title.split(' ')[0].toLowerCase()}`, busy: false, pinned, health, canStartFresh: true, delivery: null, kind });
|
|
554
609
|
const chats = [
|
|
555
610
|
chat('claude-code', '1', 'Migrate auth to JWT', 'now', struggling, 'limit', 'Limit hit · resets 3:00 PM'),
|
|
556
611
|
chat('claude-code', '2', 'Harlay landing page', '1h', long),
|
|
557
|
-
chat('claude-code', '3', '
|
|
612
|
+
chat('claude-code', '3', 'Which database for a side project?', '3h', fresh, 'idle', 'Idle', false, 'chat'),
|
|
558
613
|
chat('codex', '4', 'Fix flaky checkout tests', '4m', longer, 'warning', '92% of limit used'),
|
|
559
614
|
chat('codex', '5', 'Rate limiter for /login', '2h', fresh),
|
|
560
615
|
chat('codex', '6', 'Implement the task in Colab', '1d', fresh),
|
|
@@ -568,7 +623,7 @@ function demoApi() {
|
|
|
568
623
|
let v = {
|
|
569
624
|
key: c.key,
|
|
570
625
|
from: { tool: c.tool, label: label[c.tool] },
|
|
571
|
-
to: { tool: to, label: to === c.tool ? 'New chat' : label[to], mode: to === 'cursor' ? 'terminal' : 'app' },
|
|
626
|
+
to: { tool: to, label: to === c.tool ? 'New chat' : label[to], mode: to === 'cursor' ? 'terminal' : web.includes(to) ? 'web' : 'app' },
|
|
572
627
|
title: c.title,
|
|
573
628
|
tag: c.status === 'limit' ? 'stopped' : 'moving',
|
|
574
629
|
steps: [
|
|
@@ -604,11 +659,19 @@ function demoApi() {
|
|
|
604
659
|
label: label[tool],
|
|
605
660
|
sessions: chats.filter((c) => c.tool === tool).map((c) => ({
|
|
606
661
|
...c,
|
|
607
|
-
targets: [
|
|
662
|
+
targets: [
|
|
663
|
+
...['codex', 'claude-code', 'cursor'].filter((t) => t !== tool).map((t) => ({ tool: t, label: label[t], mode: t === 'cursor' ? 'terminal' : 'app', primary: c.kind !== 'chat' })),
|
|
664
|
+
...web.filter((w) => !(w === 'claude-web' && tool === 'claude-code' && c.status === 'limit')).map((w) => ({ tool: w, label: label[w], mode: 'web', primary: c.kind === 'chat' })),
|
|
665
|
+
].sort((a, b) => b.primary - a.primary),
|
|
608
666
|
})),
|
|
609
667
|
})),
|
|
610
668
|
usage,
|
|
611
669
|
icons: {},
|
|
670
|
+
webChats: [
|
|
671
|
+
{ key: 'web:https://claude.ai/chat/1', web: 'claude-web', title: 'Trip plan for Tokyo', ago: 'now', status: 'limit', detail: 'Limit hit · resets 3:00 PM', cwd: null, busy: false, pinned: false, health: null, canStartFresh: false, delivery: null, kind: 'chat',
|
|
672
|
+
targets: [{ tool: 'chatgpt', label: 'ChatGPT', mode: 'web', primary: true }, { tool: 'codex', label: 'Codex', mode: 'app', primary: false }, { tool: 'cursor', label: 'Cursor', mode: 'terminal', primary: false }] },
|
|
673
|
+
],
|
|
674
|
+
agentFolder: '/Users/you/robotic-harness',
|
|
612
675
|
askClaudeBridge: !asked,
|
|
613
676
|
settings: { ...settings },
|
|
614
677
|
hotkey: '⌥H',
|
|
@@ -631,6 +694,7 @@ function demoApi() {
|
|
|
631
694
|
console.log('[demo] action', a);
|
|
632
695
|
if (a.type === 'toggleAuto') settings.autoContinue = !settings.autoContinue;
|
|
633
696
|
if (a.type === 'toggleLogin') settings.openAtLogin = !settings.openAtLogin;
|
|
697
|
+
if (a.type === 'toggleWebChats') settings.webChats = !settings.webChats;
|
|
634
698
|
if (a.type === 'setTheme') settings.theme = a.theme;
|
|
635
699
|
if (a.type === 'answerClaudeBridge') {
|
|
636
700
|
asked = true;
|