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.
- package/bin/dual-brain.mjs +80 -26
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -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 = '[32m●[0m';
|
|
1488
1488
|
const RED = '[31m●[0m';
|
|
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
|
-
|
|
1521
|
+
const suffix = tip ? `[2m — ${tip}[0m` : '';
|
|
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
|
-
|
|
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
|
-
|
|
1797
|
-
const
|
|
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} {
|
|
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
|
|
1806
|
-
const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 -
|
|
1807
|
-
const truncName =
|
|
1808
|
-
?
|
|
1809
|
-
:
|
|
1810
|
-
const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${
|
|
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
|
|
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