dual-brain 0.1.19 → 0.1.21

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 (2) hide show
  1. package/bin/dual-brain.mjs +80 -26
  2. package/package.json +1 -1
@@ -1483,7 +1483,7 @@ function detectInterruptedWork(sessions, cwd) {
1483
1483
  * Shows: "● Claude ● OpenAI ⚖️ Balanced"
1484
1484
  * Uses ANSI color codes for the dots — no dollar amounts or usage bars.
1485
1485
  */
1486
- function buildProviderStatusLine(profile, auth) {
1486
+ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1487
1487
  const GREEN = '●';
1488
1488
  const RED = '●';
1489
1489
 
@@ -1498,10 +1498,28 @@ function buildProviderStatusLine(profile, auth) {
1498
1498
  'solo-claude': '⚡ Fast',
1499
1499
  'solo-openai': '⚡ Fast',
1500
1500
  };
1501
+ const WORK_STYLE_TIPS = {
1502
+ 'auto': 'adapts routing by task risk',
1503
+ 'cost-saver': 'single model, minimal reviews',
1504
+ 'balanced': 'smart routing, reviews when needed',
1505
+ 'quality-first': 'dual-brain on everything important',
1506
+ 'solo-claude': 'Claude only, no GPT dispatch',
1507
+ 'solo-openai': 'OpenAI only, no Claude dispatch',
1508
+ };
1501
1509
  const bias = profile?.bias || profile?.mode || 'balanced';
1502
1510
  const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
1511
+ const fullTip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews when needed';
1512
+
1513
+ // Trim tip to fit within box width (measure visible chars: strip ANSI + variation selectors)
1514
+ const labelPlain = label.replace(/[︀-️]/g, '').replace(/[[0-9;]*m/g, '');
1515
+ const prefixLen = ('● Claude ● OpenAI ' + labelPlain + ' — ').length;
1516
+ const tipMax = maxWidth - prefixLen;
1517
+ const tip = tipMax >= 6
1518
+ ? (fullTip.length > tipMax ? fullTip.slice(0, tipMax - 1) + '…' : fullTip)
1519
+ : '';
1503
1520
 
1504
- return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
1521
+ const suffix = tip ? ` — ${tip}` : '';
1522
+ return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
1505
1523
  }
1506
1524
 
1507
1525
  /**
@@ -1509,7 +1527,11 @@ function buildProviderStatusLine(profile, auth) {
1509
1527
  * Returns a string like: "│ content padded to W │"
1510
1528
  */
1511
1529
  function makeBoxRow(content, W) {
1512
- const plain = content.replace(/\x1b\[[0-9;]*m/g, '');
1530
+ // Strip ANSI codes, then strip zero-width variation selectors (U+FE0F etc.)
1531
+ // so that emoji like ⚖️ (U+2696+U+FE0F) don't inflate the measured length.
1532
+ const plain = content
1533
+ .replace(/\x1b\[[0-9;]*m/g, '') // ANSI color codes
1534
+ .replace(/[\uFE00-\uFE0F]/g, ''); // variation selectors (zero-width)
1513
1535
  const padding = Math.max(0, W - plain.length);
1514
1536
  return `│ ${content}${' '.repeat(padding)} │`;
1515
1537
  }
@@ -1685,7 +1707,7 @@ async function mainScreen(rl, ask) {
1685
1707
  }
1686
1708
 
1687
1709
  // ── Status section ────────────────────────────────────────────────────────
1688
- const providerLine = buildProviderStatusLine(profile, auth);
1710
+ const providerLine = buildProviderStatusLine(profile, auth, W);
1689
1711
 
1690
1712
  const statusRows = [row(providerLine)];
1691
1713
  if (dtVersion) {
@@ -1783,42 +1805,45 @@ async function mainScreen(rl, ask) {
1783
1805
  badges.push('\x1b[32m[active]\x1b[0m');
1784
1806
  badgeVisible.push('[active]'.length);
1785
1807
  }
1786
- if (sess.source === 'replit-tools' || sess.source === 'data-tools') {
1787
- badges.push('\x1b[36m[dt]\x1b[0m');
1788
- badgeVisible.push('[dt]'.length);
1789
- }
1790
1808
  const ageMs = sess.lastActive ? Date.now() - new Date(sess.lastActive).getTime() : 0;
1791
1809
  if (ageMs > 7 * 24 * 3600 * 1000) {
1792
1810
  badges.push('\x1b[2m[stale]\x1b[0m');
1793
1811
  badgeVisible.push('[stale]'.length);
1794
1812
  }
1795
1813
  const msgCount = sess.messageCount ?? sess.promptCount ?? 0;
1796
- const msgBadge = `\x1b[2m(${msgCount})\x1b[0m`;
1797
- const msgBadgeW = `(${msgCount})`.length;
1814
+ // Human-readable: "4 tasks" instead of "(4)"
1815
+ const taskLabel = msgCount === 1 ? '1 task' : `${msgCount} tasks`;
1816
+ const taskBadge = `\x1b[2m${taskLabel}\x1b[0m`;
1817
+ const taskBadgeW = taskLabel.length;
1798
1818
 
1799
1819
  const badgeStr = badges.join('');
1800
1820
  const badgesW = badgeVisible.reduce((s, n) => s + n, 0);
1801
1821
 
1802
- // Layout: "{num} {name...}{badges} {age} {msg}"
1822
+ // Layout: "{num} {name...}{badges} {age} {tasks}"
1823
+ // Use basename for name — strip full paths for readability
1824
+ const displayName = rawName.startsWith('/')
1825
+ ? rawName.split('/').filter(Boolean).pop() || rawName
1826
+ : rawName;
1827
+
1803
1828
  const numStr = String(i + 1);
1804
1829
  const ageStr = sess.age || '';
1805
- // Available for name: W minus fixed chrome, badge widths, and msg badge
1806
- const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - msgBadgeW;
1807
- const truncName = rawName.length > nameMax
1808
- ? rawName.slice(0, Math.max(0, nameMax - 3)) + '...'
1809
- : rawName.padEnd(nameMax);
1810
- const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${msgBadge}`;
1830
+ // Available for name: W minus fixed chrome, badge widths, and task badge
1831
+ const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - taskBadgeW;
1832
+ const truncName = displayName.length > nameMax
1833
+ ? displayName.slice(0, Math.max(0, nameMax - 3)) + '...'
1834
+ : displayName.padEnd(nameMax);
1835
+ const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${taskBadge}`;
1811
1836
  sessionRows.push(row(content));
1812
1837
  });
1813
1838
  }
1814
1839
 
1815
- // ── Actions bar ───────────────────────────────────────────────────────────
1816
- const actionsBase = ' Resume n New / Search i Import s Settings q Quit';
1817
- const actionsContent = openPRs.length > 0 ? `${actionsBase} p PRs` : actionsBase;
1840
+ // ── Actions bar — four product verbs first, then navigation ────────────────
1841
+ const actionsContent = 'd Do p Plan r Review s Ship │ n New / Search q Quit';
1818
1842
  const actionsRow = row(actionsContent);
1819
1843
 
1820
1844
  // ── Print the full box ────────────────────────────────────────────────────
1821
1845
  // Include action cards between status and sessions (with separators only when non-empty)
1846
+ const poweredByRow = row('\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m');
1822
1847
  const lines = [
1823
1848
  top,
1824
1849
  ...statusRows,
@@ -1827,6 +1852,8 @@ async function mainScreen(rl, ask) {
1827
1852
  ...sessionRows,
1828
1853
  sep,
1829
1854
  actionsRow,
1855
+ sep,
1856
+ poweredByRow,
1830
1857
  bot,
1831
1858
  ];
1832
1859
  // ── Stale session hint ──────────────────────────────────────────────────
@@ -1834,8 +1861,7 @@ async function mainScreen(rl, ask) {
1834
1861
  process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
1835
1862
  }
1836
1863
 
1837
- process.stdout.write(lines.join('\n') + '\n');
1838
- process.stdout.write(`\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m\n\n`);
1864
+ process.stdout.write(lines.join('\n') + '\n\n');
1839
1865
 
1840
1866
  // ── Key handling ──────────────────────────────────────────────────────────
1841
1867
  // Use raw keypress mode so we can show a live type-to-start buffer.
@@ -1922,8 +1948,7 @@ async function mainScreen(rl, ask) {
1922
1948
  // Single-key commands only fire when buffer is empty
1923
1949
  if (taskBuffer.length === 0) {
1924
1950
  const lower = str.toLowerCase();
1925
- const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
1926
- if (lower === 'p' && openPRs.length > 0) singleKeySet.add('p');
1951
+ const singleKeySet = new Set(['n', 's', 'q', '/', 'i', 'd', 'p', 'r']);
1927
1952
  if (singleKeySet.has(lower)) {
1928
1953
  cleanup();
1929
1954
  process.stdout.write('\n');
@@ -1992,6 +2017,37 @@ async function mainScreen(rl, ask) {
1992
2017
 
1993
2018
  if (choice === 'n') { return { next: 'new-session' }; }
1994
2019
 
2020
+ // Four product verbs
2021
+ if (choice === 'd') {
2022
+ // "Do" — prompt user for a task description, then dispatch
2023
+ const prompt = (await ask(' What do you want to do? ')).trim();
2024
+ if (!prompt) return { next: 'main' };
2025
+ return { next: 'go', prompt };
2026
+ }
2027
+
2028
+ if (choice === 'p') {
2029
+ // "Plan" — dry-run routing for a task
2030
+ const prompt = (await ask(' Describe the task to plan: ')).trim();
2031
+ if (!prompt) return { next: 'main' };
2032
+ return { next: 'go', prompt, dryRun: true };
2033
+ }
2034
+
2035
+ if (choice === 'r') {
2036
+ // "Review" — dual-brain review current diff
2037
+ const { spawnSync } = await import('node:child_process');
2038
+ process.stdout.write('\n Running dual-brain review...\n\n');
2039
+ spawnSync('node', ['.claude/hooks/dual-brain-review.mjs'], { stdio: 'inherit', cwd });
2040
+ return { next: 'main' };
2041
+ }
2042
+
2043
+ if (choice === 's') {
2044
+ // "Ship" — run quality gate then prompt for commit/PR
2045
+ const { spawnSync } = await import('node:child_process');
2046
+ process.stdout.write('\n Running quality gate + ship flow...\n\n');
2047
+ spawnSync('node', ['.claude/hooks/quality-gate.mjs'], { stdio: 'inherit', cwd });
2048
+ return { next: 'main' };
2049
+ }
2050
+
1995
2051
  if (choice === '/') {
1996
2052
  const query = (await ask(' Search: ')).trim();
1997
2053
  if (!query) return { next: 'main' };
@@ -2028,9 +2084,7 @@ async function mainScreen(rl, ask) {
2028
2084
  return { next: 'main' };
2029
2085
  }
2030
2086
 
2031
- if (choice === 's') { return { next: 'settings' }; }
2032
2087
  if (choice === 'i') { return { next: 'import-picker' }; }
2033
- if (choice === 'p' && openPRs.length > 0) { return { next: 'pr-triage', openPRs }; }
2034
2088
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
2035
2089
 
2036
2090
  return { next: 'main' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {