@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.
- package/README.md +11 -3
- package/bin/cli.js +6 -4
- package/package.json +1 -1
- package/src/api.js +9 -1
- package/src/cache.js +2 -1
- package/src/formatters.js +0 -20
- package/src/localApi.js +14 -3
- package/src/tui/AIPanel.js +113 -15
- package/src/tui/App.js +149 -14
- package/src/tui/FilterManager.js +3 -7
- package/src/tui/HelpModal.js +17 -11
- package/src/tui/JobDetail.js +2 -0
- package/src/tui/JobList.js +52 -5
- package/src/tui/SearchManager.js +3 -3
- package/src/tui/StatusBar.js +27 -8
- package/src/tui/useAI.js +387 -4
- package/src/tui/useJobs.js +2 -1
- package/src/tui/PreviewPane.js +0 -68
package/src/tui/HelpModal.js
CHANGED
|
@@ -7,10 +7,12 @@ const SECTIONS = [
|
|
|
7
7
|
keys: [
|
|
8
8
|
['j / ↓', 'Move down'],
|
|
9
9
|
['k / ↑', 'Move up'],
|
|
10
|
-
['
|
|
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
|
|
13
|
-
['Shift+Tab', 'Previous
|
|
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
|
-
['
|
|
34
|
+
['e', 'Export shortlist to markdown'],
|
|
35
|
+
['R', 'Force refresh (bypass cache)'],
|
|
31
36
|
],
|
|
32
37
|
},
|
|
33
38
|
{
|
|
34
|
-
title: '
|
|
39
|
+
title: 'Detail View',
|
|
35
40
|
keys: [
|
|
36
|
-
['
|
|
37
|
-
['
|
|
41
|
+
['J / K', 'Scroll detail content'],
|
|
42
|
+
['o', 'Open HN post in browser'],
|
|
38
43
|
],
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
|
-
title: '
|
|
46
|
+
title: 'AI Features (requires OPENAI_API_KEY)',
|
|
42
47
|
keys: [
|
|
43
|
-
['
|
|
44
|
-
['
|
|
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' }, '
|
|
77
|
+
h(Text, { bold: true, color: 'cyan' }, 'Keyboard Shortcuts')
|
|
72
78
|
),
|
|
73
79
|
...SECTIONS.flatMap((section) => [
|
|
74
80
|
h(
|
package/src/tui/JobDetail.js
CHANGED
|
@@ -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;
|
package/src/tui/JobList.js
CHANGED
|
@@ -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(
|
|
25
|
-
|
|
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 ?
|
|
34
|
-
const fixed =
|
|
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
|
|
package/src/tui/SearchManager.js
CHANGED
|
@@ -73,7 +73,7 @@ export default function SearchManager({
|
|
|
73
73
|
h(
|
|
74
74
|
Text,
|
|
75
75
|
{ color: 'magenta' },
|
|
76
|
-
' Creating
|
|
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,
|
|
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,
|
|
156
|
+
'Enter to create, Esc to cancel'
|
|
157
157
|
)
|
|
158
158
|
);
|
|
159
159
|
}
|
package/src/tui/StatusBar.js
CHANGED
|
@@ -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: [
|
|
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
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|