Recently Written · git

ideamine

An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.

git clone https://github.com/equwal/ideamine

Log | Files | Refs


src/render.js (6569 bytes)

1 // Plain-text views. Kept compact on purpose: every character here may end up in a model's context.
2 
3 import { samePath } from './projects.js';
4 import { baseName, counts, lane, listIdeas } from './store.js';
5 import { clip } from './text.js';
6 
7 const LANE_TITLES = {
8   doing: 'DOING',
9   do: 'DO (best first)',
10   maybe: 'MAYBE',
11   inbox: 'INBOX (untriaged)',
12   skip: 'SKIP',
13   done: 'DONE',
14   dropped: 'DROPPED',
15 };
16 
17 function projectName(p) {
18   return p ? baseName(p) : '';
19 }
20 
21 export function stamp(iso) {
22   if (!iso) return '';
23   const d = new Date(iso);
24   const pad = (n) => String(n).padStart(2, '0');
25   return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
26 }
27 
28 export function ideaLine(idea, { cwd = null } = {}) {
29   const t = idea.triage;
30   const id = `#${idea.id}`.padEnd(5);
31   const meta = t && lane(idea) !== 'inbox' && t.verdict !== 'skip'
32     ? `${t.model.padEnd(6)} ${t.size.toUpperCase().padEnd(2)} ▲${t.impact}  `
33     : '';
34   const where = idea.project && !samePath(idea.project, cwd) ? `  · ${projectName(idea.project)}` : '';
35   return `  ${id}${meta}${clip(idea.title, 64)}${where}`;
36 }
37 
38 export function summaryLine(db) {
39   const c = counts(db);
40   const parts = ['doing', 'do', 'maybe', 'inbox'].filter((k) => c[k]).map((k) => `${c[k]} ${k}`);
41   return `ideamine: ${c.open} open${parts.length ? ` (${parts.join(' · ')})` : ''}${c.done ? ` · ${c.done} done` : ''}`;
42 }
43 
44 /** The board: ideas grouped by lane. */
45 export function renderBoard(db, { filter = 'open', project = null, query = '', cwd = null, limit = 0, hints = false } = {}) {
46   const ideas = listIdeas(db, { filter, project, query, limit });
47   const out = [summaryLine(db)];
48   if (project || query || filter !== 'open') {
49     const scope = [filter !== 'open' && filter, project && `project ${projectName(project)}`, query && `"${query}"`];
50     out[0] += `  [showing: ${scope.filter(Boolean).join(', ')}]`;
51   }
52   if (!ideas.length) {
53     out.push('', db.ideas.length ? '  (nothing here)' : '  (empty — add one with /idea <text>)');
54   }
55   let current = null;
56   for (const idea of ideas) {
57     const l = lane(idea);
58     if (l !== current) {
59       current = l;
60       out.push('', LANE_TITLES[l] || l.toUpperCase());
61     }
62     out.push(ideaLine(idea, { cwd }));
63   }
64   if (hints) {
65     const tips = ['/idea <text> add'];
66     if (counts(db).open) tips.push('/ideas-go build next', '/ideas-all do all that fit here');
67     if (db.ideas.length) tips.push('/ideas-cat N show', '/ideas-rm N delete');
68     out.push('', tips.join(' · '));
69   }
70   return out.join('\n');
71 }
72 
73 /** Search results, best first, with the similarity (by meaning) or the share of words found. */
74 export function renderFound({ mode, results, note }, query, { cwd = null } = {}) {
75   const how = mode === 'meaning' ? 'by meaning' : `by words, because search by meaning is not available (${note})`;
76   const out = [`ideas like "${clip(query, 60)}", ${how}`];
77   if (!results.length) out.push('  (no match)');
78   for (const { idea, score } of results) {
79     out.push(`${String(Math.round(score * 100)).padStart(4)}% ${lane(idea).padEnd(7)}${ideaLine(idea, { cwd }).slice(1)}`);
80   }
81   return out.join('\n');
82 }
83 
84 /** Groups of ideas that are close in meaning, then the ideas in no group. */
85 export function renderGroups(groups, ideas, { cwd = null, scope = 'open' } = {}) {
86   const out = [`ideamine groups: ${groups.length} group${groups.length === 1 ? '' : 's'} of ${scope} ideas, by meaning`];
87   const byId = new Map(ideas.map((i) => [i.id, i]));
88   const line = (idea) => `  ${lane(idea).padEnd(7)}${ideaLine(idea, { cwd }).slice(1)}`;
89   for (const g of groups) out.push('', g.label.toUpperCase(), ...g.ids.map((id) => line(byId.get(id))));
90   const grouped = new Set(groups.flatMap((g) => g.ids));
91   const rest = ideas.filter((i) => !grouped.has(i.id));
92   if (rest.length) out.push('', 'IN NO GROUP', ...rest.map(line));
93   return out.join('\n');
94 }
95 
96 /** One idea in full. */
97 export function renderIdea(idea) {
98   const t = idea.triage;
99   const out = [`#${idea.id} ${idea.title}`];
100   const status = [idea.status];
101   if (t) status.push(t.verdict, t.model, `size ${t.size.toUpperCase()}`, `impact ${t.impact}/5`);
102   out.push(`status   ${status.join(' · ')}`);
103   if (idea.project) out.push(`project  ${idea.project}`);
104   const added = [`${stamp(idea.created)} via ${idea.source}`];
105   if (idea.tags?.length) added.push(`tags: ${idea.tags.join(', ')}`);
106   out.push(`added    ${added.join(' · ')}`);
107   if (idea.dup_of) out.push(`dup of   #${idea.dup_of}`);
108   if (t?.why) out.push(`why      ${t.why}`);
109   if (t?.brief) out.push(`brief    ${t.brief}`);
110   if (t?.by) out.push(`triaged  ${stamp(t.at)} by ${t.by}`);
111   if (idea.text !== idea.title) out.push('', 'text', ...idea.text.split(/\r?\n/).map((l) => `  ${l}`));
112   if (idea.notes?.length) {
113     out.push('', 'notes');
114     for (const n of idea.notes) out.push(`  ${stamp(n.at)}  ${n.text}`);
115   }
116   return out.join('\n');
117 }
118 
119 /** Confirmation after capture, e.g. "💡 Saved #12 · Add dark mode (3 in inbox)". */
120 export function renderAdded(results, db) {
121   const inbox = counts(db).inbox;
122   const lines = [];
123   if (results.length === 1) {
124     const { idea } = results[0];
125     lines.push(`💡 Saved #${idea.id} · ${clip(idea.title, 70)}  (${inbox} in inbox)`);
126   } else {
127     lines.push(`💡 Saved ${results.length} ideas: ${results.map((r) => `#${r.idea.id}`).join(', ')}  (${inbox} in inbox)`);
128   }
129   const seen = new Set(results.map((r) => r.idea.id));
130   for (const r of results) {
131     for (const s of r.similar) {
132       if (seen.has(s.id)) continue;
133       seen.add(s.id);
134       lines.push(`   similar to #${s.id} (${s.status}): ${clip(s.title, 60)}`);
135     }
136   }
137   return lines.join('\n');
138 }
139 
140 /** Markdown export of the whole archive. */
141 export function renderMarkdown(db) {
142   const out = [`# Ideas`, '', `_${summaryLine(db)} · exported ${stamp(new Date().toISOString())}_`];
143   let current = null;
144   for (const idea of listIdeas(db, { filter: 'all' })) {
145     const l = lane(idea);
146     if (l !== current) {
147       current = l;
148       out.push('', `## ${LANE_TITLES[l] || l}`, '');
149     }
150     const t = idea.triage;
151     const meta = t && l !== 'inbox' ? ` — ${t.verdict}, ${t.model}, size ${t.size.toUpperCase()}, impact ${t.impact}/5` : '';
152     out.push(`- **#${idea.id} ${idea.title}**${meta}${idea.project ? ` · \`${projectName(idea.project)}\`` : ''}`);
153     if (t?.brief) out.push(`  - ${t.brief}`);
154     if (idea.text !== idea.title) out.push(`  - > ${idea.text.replace(/\r?\n/g, ' ')}`);
155   }
156   return out.join('\n') + '\n';
157 }