dual-brain 0.1.19 → 0.1.20
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 +69 -20
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -1498,10 +1498,19 @@ 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 based on task risk',
|
|
1503
|
+
'cost-saver': 'cheaper models, skips GPT for non-critical',
|
|
1504
|
+
'balanced': 'smart routing, reviews on important changes',
|
|
1505
|
+
'quality-first': 'dual-brain for medium+ risk, stricter reviews',
|
|
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 tip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews on important changes';
|
|
1503
1512
|
|
|
1504
|
-
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
|
|
1513
|
+
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}[2m — ${tip}[0m`;
|
|
1505
1514
|
}
|
|
1506
1515
|
|
|
1507
1516
|
/**
|
|
@@ -1509,7 +1518,11 @@ function buildProviderStatusLine(profile, auth) {
|
|
|
1509
1518
|
* Returns a string like: "│ content padded to W │"
|
|
1510
1519
|
*/
|
|
1511
1520
|
function makeBoxRow(content, W) {
|
|
1512
|
-
|
|
1521
|
+
// Strip ANSI codes, then strip zero-width variation selectors (U+FE0F etc.)
|
|
1522
|
+
// so that emoji like ⚖️ (U+2696+U+FE0F) don't inflate the measured length.
|
|
1523
|
+
const plain = content
|
|
1524
|
+
.replace(/\x1b\[[0-9;]*m/g, '') // ANSI color codes
|
|
1525
|
+
.replace(/[\uFE00-\uFE0F]/g, ''); // variation selectors (zero-width)
|
|
1513
1526
|
const padding = Math.max(0, W - plain.length);
|
|
1514
1527
|
return `│ ${content}${' '.repeat(padding)} │`;
|
|
1515
1528
|
}
|
|
@@ -1793,32 +1806,39 @@ async function mainScreen(rl, ask) {
|
|
|
1793
1806
|
badgeVisible.push('[stale]'.length);
|
|
1794
1807
|
}
|
|
1795
1808
|
const msgCount = sess.messageCount ?? sess.promptCount ?? 0;
|
|
1796
|
-
|
|
1797
|
-
const
|
|
1809
|
+
// Human-readable: "4 tasks" instead of "(4)"
|
|
1810
|
+
const taskLabel = msgCount === 1 ? '1 task' : `${msgCount} tasks`;
|
|
1811
|
+
const taskBadge = `\x1b[2m${taskLabel}\x1b[0m`;
|
|
1812
|
+
const taskBadgeW = taskLabel.length;
|
|
1798
1813
|
|
|
1799
1814
|
const badgeStr = badges.join('');
|
|
1800
1815
|
const badgesW = badgeVisible.reduce((s, n) => s + n, 0);
|
|
1801
1816
|
|
|
1802
|
-
// Layout: "{num} {name...}{badges} {age} {
|
|
1817
|
+
// Layout: "{num} {name...}{badges} {age} {tasks}"
|
|
1818
|
+
// Use basename for name — strip full paths for readability
|
|
1819
|
+
const displayName = rawName.startsWith('/')
|
|
1820
|
+
? rawName.split('/').filter(Boolean).pop() || rawName
|
|
1821
|
+
: rawName;
|
|
1822
|
+
|
|
1803
1823
|
const numStr = String(i + 1);
|
|
1804
1824
|
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} ${
|
|
1825
|
+
// Available for name: W minus fixed chrome, badge widths, and task badge
|
|
1826
|
+
const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - taskBadgeW;
|
|
1827
|
+
const truncName = displayName.length > nameMax
|
|
1828
|
+
? displayName.slice(0, Math.max(0, nameMax - 3)) + '...'
|
|
1829
|
+
: displayName.padEnd(nameMax);
|
|
1830
|
+
const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${taskBadge}`;
|
|
1811
1831
|
sessionRows.push(row(content));
|
|
1812
1832
|
});
|
|
1813
1833
|
}
|
|
1814
1834
|
|
|
1815
|
-
// ── Actions bar
|
|
1816
|
-
const
|
|
1817
|
-
const actionsContent = openPRs.length > 0 ? `${actionsBase} p PRs` : actionsBase;
|
|
1835
|
+
// ── Actions bar — four product verbs first, then navigation ────────────────
|
|
1836
|
+
const actionsContent = 'd Do p Plan r Review s Ship │ n New / Search q Quit';
|
|
1818
1837
|
const actionsRow = row(actionsContent);
|
|
1819
1838
|
|
|
1820
1839
|
// ── Print the full box ────────────────────────────────────────────────────
|
|
1821
1840
|
// Include action cards between status and sessions (with separators only when non-empty)
|
|
1841
|
+
const poweredByRow = row('\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m');
|
|
1822
1842
|
const lines = [
|
|
1823
1843
|
top,
|
|
1824
1844
|
...statusRows,
|
|
@@ -1827,6 +1847,8 @@ async function mainScreen(rl, ask) {
|
|
|
1827
1847
|
...sessionRows,
|
|
1828
1848
|
sep,
|
|
1829
1849
|
actionsRow,
|
|
1850
|
+
sep,
|
|
1851
|
+
poweredByRow,
|
|
1830
1852
|
bot,
|
|
1831
1853
|
];
|
|
1832
1854
|
// ── Stale session hint ──────────────────────────────────────────────────
|
|
@@ -1834,8 +1856,7 @@ async function mainScreen(rl, ask) {
|
|
|
1834
1856
|
process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
|
|
1835
1857
|
}
|
|
1836
1858
|
|
|
1837
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
1838
|
-
process.stdout.write(`\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m\n\n`);
|
|
1859
|
+
process.stdout.write(lines.join('\n') + '\n\n');
|
|
1839
1860
|
|
|
1840
1861
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
1841
1862
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
@@ -1922,8 +1943,7 @@ async function mainScreen(rl, ask) {
|
|
|
1922
1943
|
// Single-key commands only fire when buffer is empty
|
|
1923
1944
|
if (taskBuffer.length === 0) {
|
|
1924
1945
|
const lower = str.toLowerCase();
|
|
1925
|
-
const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
|
|
1926
|
-
if (lower === 'p' && openPRs.length > 0) singleKeySet.add('p');
|
|
1946
|
+
const singleKeySet = new Set(['n', 's', 'q', '/', 'i', 'd', 'p', 'r']);
|
|
1927
1947
|
if (singleKeySet.has(lower)) {
|
|
1928
1948
|
cleanup();
|
|
1929
1949
|
process.stdout.write('\n');
|
|
@@ -1992,6 +2012,37 @@ async function mainScreen(rl, ask) {
|
|
|
1992
2012
|
|
|
1993
2013
|
if (choice === 'n') { return { next: 'new-session' }; }
|
|
1994
2014
|
|
|
2015
|
+
// Four product verbs
|
|
2016
|
+
if (choice === 'd') {
|
|
2017
|
+
// "Do" — prompt user for a task description, then dispatch
|
|
2018
|
+
const prompt = (await ask(' What do you want to do? ')).trim();
|
|
2019
|
+
if (!prompt) return { next: 'main' };
|
|
2020
|
+
return { next: 'go', prompt };
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
if (choice === 'p') {
|
|
2024
|
+
// "Plan" — dry-run routing for a task
|
|
2025
|
+
const prompt = (await ask(' Describe the task to plan: ')).trim();
|
|
2026
|
+
if (!prompt) return { next: 'main' };
|
|
2027
|
+
return { next: 'go', prompt, dryRun: true };
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (choice === 'r') {
|
|
2031
|
+
// "Review" — dual-brain review current diff
|
|
2032
|
+
const { spawnSync } = await import('node:child_process');
|
|
2033
|
+
process.stdout.write('\n Running dual-brain review...\n\n');
|
|
2034
|
+
spawnSync('node', ['.claude/hooks/dual-brain-review.mjs'], { stdio: 'inherit', cwd });
|
|
2035
|
+
return { next: 'main' };
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (choice === 's') {
|
|
2039
|
+
// "Ship" — run quality gate then prompt for commit/PR
|
|
2040
|
+
const { spawnSync } = await import('node:child_process');
|
|
2041
|
+
process.stdout.write('\n Running quality gate + ship flow...\n\n');
|
|
2042
|
+
spawnSync('node', ['.claude/hooks/quality-gate.mjs'], { stdio: 'inherit', cwd });
|
|
2043
|
+
return { next: 'main' };
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1995
2046
|
if (choice === '/') {
|
|
1996
2047
|
const query = (await ask(' Search: ')).trim();
|
|
1997
2048
|
if (!query) return { next: 'main' };
|
|
@@ -2028,9 +2079,7 @@ async function mainScreen(rl, ask) {
|
|
|
2028
2079
|
return { next: 'main' };
|
|
2029
2080
|
}
|
|
2030
2081
|
|
|
2031
|
-
if (choice === 's') { return { next: 'settings' }; }
|
|
2032
2082
|
if (choice === 'i') { return { next: 'import-picker' }; }
|
|
2033
|
-
if (choice === 'p' && openPRs.length > 0) { return { next: 'pr-triage', openPRs }; }
|
|
2034
2083
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
2035
2084
|
|
|
2036
2085
|
return { next: 'main' };
|
package/package.json
CHANGED