@jsonresume/jobs 0.9.0 → 0.11.0

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.
@@ -7,10 +7,12 @@ const SECTIONS = [
7
7
  keys: [
8
8
  ['j / ↓', 'Move down'],
9
9
  ['k / ↑', 'Move up'],
10
- ['Enter', 'Open job details'],
10
+ ['g / G', 'Jump to first / last'],
11
+ ['Ctrl+U / D', 'Page up / page down'],
12
+ ['Enter', 'Open split-pane detail view'],
11
13
  ['Esc / q', 'Back / quit'],
12
- ['Tab', 'Next section tab'],
13
- ['Shift+Tab', 'Previous section tab'],
14
+ ['Tab', 'Next tab'],
15
+ ['Shift+Tab', 'Previous tab'],
14
16
  ],
15
17
  },
16
18
  {
@@ -20,28 +22,32 @@ const SECTIONS = [
20
22
  ['x', 'Mark applied'],
21
23
  ['m', 'Mark maybe'],
22
24
  ['p', 'Mark passed'],
25
+ ['v', 'Toggle batch selection'],
23
26
  ],
24
27
  },
25
28
  {
26
29
  title: 'Search & Filter',
27
30
  keys: [
31
+ ['n', 'Inline keyword search'],
28
32
  ['/', 'Search profiles'],
29
33
  ['f', 'Manage filters'],
30
- ['R', 'Force refresh'],
34
+ ['e', 'Export shortlist to markdown'],
35
+ ['R', 'Force refresh (bypass cache)'],
31
36
  ],
32
37
  },
33
38
  {
34
- title: 'AI Features',
39
+ title: 'Detail View',
35
40
  keys: [
36
- ['Space', 'AI summary of job'],
37
- ['S', 'AI batch review'],
41
+ ['J / K', 'Scroll detail content'],
42
+ ['o', 'Open HN post in browser'],
38
43
  ],
39
44
  },
40
45
  {
41
- title: 'Detail View',
46
+ title: 'AI Features (requires OPENAI_API_KEY)',
42
47
  keys: [
43
- ['o', 'Open HN post in browser'],
44
- ['e', 'Export shortlist to markdown'],
48
+ ['Space', 'AI summary of current job'],
49
+ ['c', 'Research dossier (uses Claude Code CLI)'],
50
+ ['S', 'AI batch review of visible jobs'],
45
51
  ],
46
52
  },
47
53
  ];
@@ -68,7 +74,7 @@ export default function HelpModal({ onClose }) {
68
74
  h(
69
75
  Box,
70
76
  { justifyContent: 'center', marginBottom: 1 },
71
- h(Text, { bold: true, color: 'cyan' }, 'Keyboard Shortcuts')
77
+ h(Text, { bold: true, color: 'cyan' }, 'Keyboard Shortcuts')
72
78
  ),
73
79
  ...SECTIONS.flatMap((section) => [
74
80
  h(
@@ -10,6 +10,7 @@ export default function JobDetail({
10
10
  onBack,
11
11
  onMark,
12
12
  onAISummary,
13
+ onDossier,
13
14
  isActive,
14
15
  isPanel,
15
16
  }) {
@@ -45,6 +46,7 @@ export default function JobDetail({
45
46
  if (input === 'm') onMark(job.id, 'maybe');
46
47
  if (input === 'p') onMark(job.id, 'not_interested');
47
48
  if (input === ' ') onAISummary(job);
49
+ if (input === 'c' && onDossier) onDossier(job);
48
50
  if (input === 'o' && (detail?.url || job.url)) {
49
51
  import('child_process').then(({ exec }) => {
50
52
  const url = detail?.url || job.url;
@@ -16,13 +16,26 @@ function useColumns(hasRerank, compact) {
16
16
  const cols = stdout?.columns || 120;
17
17
  const available = compact ? Math.floor(cols * 0.4) : cols;
18
18
 
19
+ const dossierW = 2;
20
+
19
21
  if (compact) {
20
- // Compact mode: just score, title, status
22
+ // Compact mode: just score, title, dossier, status
21
23
  const scoreW = 5;
22
24
  const statusW = 2;
23
25
  const gaps = GAP * 2;
24
- const titleW = Math.max(10, available - scoreW - statusW - gaps - 2);
25
- return { cols: available, titleW, compW: 0, locW: 0, scoreW, statusW };
26
+ const titleW = Math.max(
27
+ 10,
28
+ available - scoreW - dossierW - statusW - gaps - 2
29
+ );
30
+ return {
31
+ cols: available,
32
+ titleW,
33
+ compW: 0,
34
+ locW: 0,
35
+ scoreW,
36
+ statusW,
37
+ dossierW,
38
+ };
26
39
  }
27
40
 
28
41
  const scoreW = 5;
@@ -30,8 +43,9 @@ function useColumns(hasRerank, compact) {
30
43
  const salaryW = 12;
31
44
  const statusW = 2;
32
45
  const cursorW = 2;
33
- const gaps = GAP * (hasRerank ? 6 : 5);
34
- const fixed = cursorW + scoreW + aiW + salaryW + statusW + gaps + 2; // +2 paddingX
46
+ const gaps = GAP * (hasRerank ? 7 : 6);
47
+ const fixed =
48
+ cursorW + scoreW + aiW + salaryW + dossierW + statusW + gaps + 2;
35
49
  const flex = Math.max(30, available - fixed);
36
50
  const titleW = Math.max(12, Math.floor(flex * 0.35));
37
51
  const compW = Math.max(10, Math.floor(flex * 0.3));
@@ -44,6 +58,7 @@ function useColumns(hasRerank, compact) {
44
58
  scoreW,
45
59
  salaryW,
46
60
  statusW,
61
+ dossierW,
47
62
  aiW,
48
63
  };
49
64
  }
@@ -99,6 +114,11 @@ function HeaderRow({ hasRerank, titleW, compW, locW, compact }) {
99
114
  { width: 12, marginRight: GAP },
100
115
  h(Text, { bold: true, dimColor: true }, 'Salary')
101
116
  ),
117
+ h(
118
+ Box,
119
+ { width: 2, marginRight: GAP },
120
+ h(Text, { bold: true, dimColor: true }, '📋')
121
+ ),
102
122
  h(Box, { width: 2 }, h(Text, { bold: true, dimColor: true }, ' '))
103
123
  );
104
124
  }
@@ -112,12 +132,15 @@ function JobRow({
112
132
  locW,
113
133
  marked,
114
134
  compact,
135
+ dossierStatus,
115
136
  }) {
116
137
  const loc = formatLocation(job.location, job.remote);
117
138
  const sal = formatSalary(job.salary, job.salary_usd);
118
139
  const score =
119
140
  typeof job.similarity === 'number' ? job.similarity.toFixed(2) : '—';
120
141
  const icon = stateIcon(job.state);
142
+ const dossierIcon =
143
+ dossierStatus === 'generating' ? '◌' : dossierStatus === 'done' ? '📋' : '';
121
144
 
122
145
  const stColor =
123
146
  job.state === 'interested'
@@ -145,6 +168,8 @@ function JobRow({
145
168
  backgroundColor: bg,
146
169
  };
147
170
 
171
+ const dossierColor = dossierStatus === 'generating' ? 'yellow' : 'green';
172
+
148
173
  if (compact) {
149
174
  return h(
150
175
  Box,
@@ -160,6 +185,15 @@ function JobRow({
160
185
  { flexGrow: 1 },
161
186
  h(Text, props, truncate(job.title || '—', titleW))
162
187
  ),
188
+ h(
189
+ Box,
190
+ { width: 2 },
191
+ h(
192
+ Text,
193
+ { ...props, color: dossierIcon ? dossierColor : undefined },
194
+ dossierIcon || ' '
195
+ )
196
+ ),
163
197
  h(Box, { width: 2 }, h(Text, props, icon))
164
198
  );
165
199
  }
@@ -196,6 +230,15 @@ function JobRow({
196
230
  h(Text, props, truncate(loc, locW - 1))
197
231
  ),
198
232
  h(Box, { width: 12, marginRight: GAP }, h(Text, props, truncate(sal, 11))),
233
+ h(
234
+ Box,
235
+ { width: 2, marginRight: GAP },
236
+ h(
237
+ Text,
238
+ { ...props, color: dossierIcon ? dossierColor : undefined },
239
+ dossierIcon || ' '
240
+ )
241
+ ),
199
242
  h(Box, { width: 2 }, h(Text, props, icon))
200
243
  );
201
244
  }
@@ -248,8 +291,10 @@ export default function JobList({
248
291
  onSelect,
249
292
  onMark,
250
293
  onAISummary,
294
+ onDossier,
251
295
  onAIBatch,
252
296
  onExport,
297
+ getDossierStatus,
253
298
  isActive,
254
299
  tab,
255
300
  compact,
@@ -321,6 +366,7 @@ export default function JobList({
321
366
  if (input === 'p' && jobs[cursor])
322
367
  onMark(jobs[cursor].id, 'not_interested');
323
368
  if (input === ' ' && jobs[cursor]) onAISummary(jobs[cursor]);
369
+ if (input === 'c' && jobs[cursor] && onDossier) onDossier(jobs[cursor]);
324
370
  if (input === 'S' && onAIBatch) onAIBatch(jobs);
325
371
  if (input === 'e' && onExport) onExport();
326
372
  },
@@ -344,6 +390,7 @@ export default function JobList({
344
390
  compW,
345
391
  locW,
346
392
  compact,
393
+ dossierStatus: getDossierStatus ? getDossierStatus(job.id) : null,
347
394
  })
348
395
  );
349
396
 
@@ -73,7 +73,7 @@ export default function SearchManager({
73
73
  h(
74
74
  Text,
75
75
  { color: 'magenta' },
76
- ' Creating search profile... (AI is blending your resume with the prompt)'
76
+ ' Creating profile generating a custom embedding from your resume + search prompt'
77
77
  )
78
78
  )
79
79
  );
@@ -110,7 +110,7 @@ export default function SearchManager({
110
110
  h(
111
111
  Text,
112
112
  { dimColor: true, marginTop: 1 },
113
- 'Enter to continue, Ctrl+C to cancel'
113
+ 'Enter to continue, Esc to cancel'
114
114
  )
115
115
  );
116
116
  }
@@ -153,7 +153,7 @@ export default function SearchManager({
153
153
  h(
154
154
  Text,
155
155
  { dimColor: true, marginTop: 1 },
156
- 'Enter to create, Ctrl+C to cancel'
156
+ 'Enter to create, Esc to cancel'
157
157
  )
158
158
  );
159
159
  }
@@ -12,6 +12,7 @@ const KEYS = {
12
12
  ['v', 'select'],
13
13
  ['n', 'find'],
14
14
  ['space', 'AI'],
15
+ ['c', 'dossier'],
15
16
  ],
16
17
  detail: [
17
18
  ['j/k', 'nav jobs'],
@@ -19,8 +20,13 @@ const KEYS = {
19
20
  ['i/x/m/p', 'mark'],
20
21
  ['o', 'open URL'],
21
22
  ['space', 'AI'],
23
+ ['c', 'dossier'],
22
24
  ['esc', 'back'],
23
25
  ],
26
+ search: [
27
+ ['esc', 'clear search'],
28
+ ['enter', 'apply'],
29
+ ],
24
30
  filters: [
25
31
  ['j/k', 'nav'],
26
32
  ['enter', 'edit'],
@@ -35,7 +41,13 @@ const KEYS = {
35
41
  ['d', 'delete'],
36
42
  ['esc', 'close'],
37
43
  ],
38
- ai: [['esc', 'dismiss']],
44
+ ai: [
45
+ ['j/k', 'scroll'],
46
+ ['g/G', 'top/bottom'],
47
+ ['i/x/m/p', 'mark'],
48
+ ['e', 'export'],
49
+ ['esc', 'back'],
50
+ ],
39
51
  help: [['?/esc', 'close']],
40
52
  };
41
53
 
@@ -67,14 +79,21 @@ export default function StatusBar({
67
79
  h(Text, { dimColor: true }, '─'.repeat(Math.max(10, cols - 2)))
68
80
  );
69
81
 
70
- const rightInfo = h(
71
- Box,
72
- { gap: 1 },
73
- loading ? h(Text, { color: 'yellow' }, 'loading…') : null,
74
- reranking ? h(Text, { color: 'magenta' }, 'reranking…') : null,
75
- h(Text, { dimColor: true }, `${jobCount}/${totalCount}`),
76
- aiEnabled ? null : h(Text, { dimColor: true }, 'no-AI')
82
+ const rightParts = [];
83
+ if (loading)
84
+ rightParts.push(h(Text, { key: 'l', color: 'yellow' }, 'loading…'));
85
+ if (reranking)
86
+ rightParts.push(h(Text, { key: 'r', color: 'magenta' }, 'reranking…'));
87
+ rightParts.push(
88
+ h(Text, { key: 'c', dimColor: true }, `${jobCount}/${totalCount}`)
77
89
  );
90
+ if (!aiEnabled) {
91
+ rightParts.push(
92
+ h(Text, { key: 'ai', dimColor: true }, 'set OPENAI_API_KEY for AI')
93
+ );
94
+ }
95
+
96
+ const rightInfo = h(Box, { gap: 1 }, ...rightParts);
78
97
 
79
98
  const content = toast
80
99
  ? h(Box, { paddingX: 1, justifyContent: 'space-between' }, toast, rightInfo)