harlay 0.1.0 → 0.1.1

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.
@@ -88,6 +88,10 @@ button { font: inherit; color: inherit; background: none; border: 0; text-align:
88
88
  .label::before { content: ""; width: 5px; height: 5px; background: var(--accent); }
89
89
 
90
90
  /* sessions */
91
+ .label.group { padding: 10px 8px 4px; }
92
+ .label.group:first-child { padding-top: 2px; }
93
+ .label.attn::before { background: var(--err); }
94
+ .label .count { margin-left: auto; letter-spacing: 0; color: var(--fg3); opacity: .8; }
91
95
  .scroll { max-height: 372px; overflow-y: auto; padding: 0 8px 8px; }
92
96
  .scroll::-webkit-scrollbar { width: 8px; }
93
97
  .scroll::-webkit-scrollbar-thumb { background: rgba(var(--ink-rgb), .15); border-radius: 8px; border: 2px solid var(--bg); }
@@ -14,7 +14,7 @@
14
14
  <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 20 10 4h4.3L6.8 20z" fill="currentColor" opacity=".45"/><path class="mk-b" d="M10.2 20 17.7 4H22l-7.5 16z"/></svg>
15
15
  harlay
16
16
  </div>
17
- <span class="pill" id="pill"><i></i><span id="pill-text">Watching</span></span>
17
+ <span class="pill" id="pill"><i></i><span id="pill-text">Standing by</span></span>
18
18
  <button class="icon-btn" id="toggle-theme" aria-label="Dark mode" title="Dark mode"></button>
19
19
  <button class="icon-btn" id="toggle-settings" aria-label="Settings" title="Settings"></button>
20
20
  </header>
@@ -34,7 +34,7 @@ function h(tag, props, ...kids) {
34
34
  else if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
35
35
  else el.setAttribute(k, v === true ? '' : String(v));
36
36
  }
37
- for (const kid of kids.flat()) if (kid != null && kid !== false) el.append(kid);
37
+ for (const kid of kids.flat(Infinity)) if (kid != null && kid !== false) el.append(kid);
38
38
  return el;
39
39
  }
40
40
 
@@ -67,9 +67,9 @@ function renderHero() {
67
67
  h('p', {}, `${plural(sessions.length, 'recent session')} · ${plural(installed, 'agent')} ready`));
68
68
  }
69
69
 
70
- function renderSession(s) {
70
+ function renderSession(s, showRepo) {
71
71
  const open = s.key === openKey;
72
- const meta = [s.toolLabel, s.repo, s.ago].filter(Boolean).join(' · ');
72
+ const meta = [s.toolLabel, showRepo ? s.repo : null, s.ago].filter(Boolean).join(' · ');
73
73
  const tool = (t) => model.tools.find((x) => x.tool === t.tool);
74
74
  const actions = s.busy
75
75
  ? h('div', { class: 'busy' }, h('span', { class: 'spin' }), 'Handing off…')
@@ -106,14 +106,37 @@ function previewBrief(s) {
106
106
  api.act({ type: 'previewBrief', key: s.key });
107
107
  }
108
108
 
109
+ /**
110
+ * Stopped and near-limit sessions first, then everything else grouped by project (most recent project first,
111
+ * as the sessions arrive newest first).
112
+ */
113
+ function groupSessions(sessions) {
114
+ const attention = sessions.filter((s) => s.status === 'limit' || s.status === 'warning');
115
+ const projects = new Map();
116
+ for (const s of sessions) {
117
+ if (attention.includes(s)) continue;
118
+ const key = s.cwd ?? '';
119
+ if (!projects.has(key)) projects.set(key, { title: s.repo ?? 'No project folder', path: s.cwd, sessions: [] });
120
+ projects.get(key).sessions.push(s);
121
+ }
122
+ return { attention, projects: [...projects.values()] };
123
+ }
124
+
109
125
  function renderHome() {
110
126
  const { sessions } = model;
127
+ if (!sessions.length) {
128
+ $('#home').replaceChildren(renderHero(), h('p', { class: 'empty' }, 'Harlay reads session logs from ~/.claude, ~/.codex and ~/.cursor on this Mac.'));
129
+ return;
130
+ }
131
+ const { attention, projects } = groupSessions(sessions);
111
132
  $('#home').replaceChildren(
112
133
  renderHero(),
113
- h('div', { class: 'label' }, 'Recent sessions'),
114
- sessions.length
115
- ? h('div', { class: 'scroll' }, sessions.map(renderSession))
116
- : h('p', { class: 'empty' }, 'Harlay reads session logs from ~/.claude, ~/.codex and ~/.cursor on this Mac.'));
134
+ h('div', { class: 'scroll' },
135
+ attention.length ? [h('div', { class: 'label group attn' }, 'Needs attention'), attention.map((s) => renderSession(s, true))] : null,
136
+ projects.map((p) => [
137
+ h('div', { class: 'label group', title: p.path }, p.title, h('span', { class: 'count' }, String(p.sessions.length))),
138
+ p.sessions.map((s) => renderSession(s, false)),
139
+ ])));
117
140
  }
118
141
 
119
142
  function seg(options, current, onPick) {
@@ -260,7 +283,7 @@ function render() {
260
283
  theme.title = theme.getAttribute('aria-label');
261
284
  const pill = $('#pill');
262
285
  pill.classList.toggle('auto', model.settings.autoContinue);
263
- $('#pill-text').textContent = model.settings.autoContinue ? 'Auto-continue' : 'Watching';
286
+ $('#pill-text').textContent = model.settings.autoContinue ? 'Auto-continue' : 'Standing by';
264
287
  $('#hotkey').textContent = model.hotkey;
265
288
  if (view === 'home') renderHome();
266
289
  else if (view === 'settings') renderSettings();
@@ -342,6 +365,7 @@ function demoApi() {
342
365
  { key: 'claude-code:1', from: 'claude-code', title: 'Migrate auth to JWT', toolLabel: 'Claude Code', status: 'limit', detail: 'Limit hit · resets 6:00 PM', cwd: '/Users/you/acme-api', repo: 'acme-api', ago: 'now', busy: false },
343
366
  { key: 'codex:2', from: 'codex', title: 'Fix flaky checkout tests', toolLabel: 'Codex', status: 'warning', detail: '92% of limit used', cwd: '/Users/you/shop', repo: 'shop', ago: '4m ago', busy: false },
344
367
  { key: 'claude-code:3', from: 'claude-code', title: 'Harlay landing page', toolLabel: 'Claude Code', status: 'idle', detail: 'Idle', cwd: '/Users/you/harlay', repo: 'harlay', ago: '1h ago', busy: false },
368
+ { key: 'codex:5', from: 'codex', title: 'Rate limiter for /login', toolLabel: 'Codex', status: 'idle', detail: 'Idle', cwd: '/Users/you/acme-api', repo: 'acme-api', ago: '2h ago', busy: false },
345
369
  { key: 'cursor:4', from: 'cursor', title: 'Onboarding copy pass', toolLabel: 'Cursor', status: 'handed_off', detail: 'Handed off to Codex', cwd: '/Users/you/web', repo: 'web', ago: '3h ago', busy: false },
346
370
  ];
347
371
  let onModel = () => {};
package/dist/harlay.mjs CHANGED
@@ -1704,7 +1704,7 @@ async function installApp(opts) {
1704
1704
  }
1705
1705
 
1706
1706
  // src/main.ts
1707
- var VERSION = "0.1.0";
1707
+ var VERSION = "0.1.1";
1708
1708
  var HELP = `harlay ${VERSION} \u2014 hand off a stalled AI coding session to another agent
1709
1709
 
1710
1710
  Usage:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harlay",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Hand off a stalled AI coding session (Claude Code, Codex, Cursor) to another agent. CLI and macOS menu bar app.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",