@jsonresume/jobs 0.10.0 → 0.12.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 +236 -4
- package/package.json +1 -1
- package/src/api.js +8 -0
- package/src/cache.js +2 -1
- package/src/localApi.js +5 -0
- package/src/tui/AIPanel.js +118 -15
- package/src/tui/App.js +164 -12
- package/src/tui/HelpModal.js +1 -0
- package/src/tui/JobDetail.js +2 -0
- package/src/tui/JobList.js +52 -5
- package/src/tui/StatusBar.js +9 -1
- package/src/tui/useAI.js +420 -2
- package/src/tui/useJobs.js +20 -3
package/src/tui/App.js
CHANGED
|
@@ -24,9 +24,19 @@ import StatusBar from './StatusBar.js';
|
|
|
24
24
|
import AIPanel from './AIPanel.js';
|
|
25
25
|
import HelpModal from './HelpModal.js';
|
|
26
26
|
|
|
27
|
-
const TABS = [
|
|
27
|
+
const TABS = [
|
|
28
|
+
'all',
|
|
29
|
+
'new',
|
|
30
|
+
'reviewed',
|
|
31
|
+
'interested',
|
|
32
|
+
'applied',
|
|
33
|
+
'maybe',
|
|
34
|
+
'passed',
|
|
35
|
+
];
|
|
28
36
|
const TAB_LABELS = {
|
|
29
37
|
all: 'All',
|
|
38
|
+
new: 'New',
|
|
39
|
+
reviewed: 'Reviewed',
|
|
30
40
|
interested: 'Interested',
|
|
31
41
|
applied: 'Applied',
|
|
32
42
|
maybe: 'Maybe',
|
|
@@ -65,7 +75,9 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
65
75
|
// Active search profile
|
|
66
76
|
const [activeSearchId, setActiveSearchId] = useState(null);
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
const searchesHook = useSearches(api);
|
|
79
|
+
|
|
80
|
+
// Persistent filters (local + server sync for search profiles)
|
|
69
81
|
const [filterStore, setFilterStore] = useState(() => loadFilters());
|
|
70
82
|
const activeFilters = useMemo(
|
|
71
83
|
() => getFiltersForSearch(filterStore, activeSearchId),
|
|
@@ -78,8 +90,12 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
78
90
|
saveFilters(next);
|
|
79
91
|
return next;
|
|
80
92
|
});
|
|
93
|
+
// Sync to server for search profiles
|
|
94
|
+
if (activeSearchId) {
|
|
95
|
+
searchesHook.updateFilters(activeSearchId, newActive);
|
|
96
|
+
}
|
|
81
97
|
},
|
|
82
|
-
[activeSearchId]
|
|
98
|
+
[activeSearchId, searchesHook]
|
|
83
99
|
);
|
|
84
100
|
const filterState = useMemo(
|
|
85
101
|
() => ({ active: activeFilters }),
|
|
@@ -90,6 +106,22 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
90
106
|
[updateFilters]
|
|
91
107
|
);
|
|
92
108
|
|
|
109
|
+
// Sync server-side filters into local store when search profiles load
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!searchesHook.searches.length) return;
|
|
112
|
+
setFilterStore((prev) => {
|
|
113
|
+
let updated = prev;
|
|
114
|
+
for (const s of searchesHook.searches) {
|
|
115
|
+
if (s.filters?.length && !prev.searches?.[s.id]) {
|
|
116
|
+
updated = setFiltersForSearch(updated, s.id, s.filters);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (updated !== prev) saveFilters(updated);
|
|
120
|
+
return updated;
|
|
121
|
+
});
|
|
122
|
+
}, [searchesHook.searches]);
|
|
123
|
+
|
|
124
|
+
const ai = useAI(resume);
|
|
93
125
|
const {
|
|
94
126
|
jobs: rawJobs,
|
|
95
127
|
allJobs,
|
|
@@ -98,10 +130,25 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
98
130
|
error,
|
|
99
131
|
markJob,
|
|
100
132
|
forceRefresh,
|
|
101
|
-
} = useJobs(api, activeFilters, tab, activeSearchId);
|
|
102
|
-
const ai = useAI(resume);
|
|
103
|
-
const searchesHook = useSearches(api);
|
|
133
|
+
} = useJobs(api, activeFilters, tab, activeSearchId, ai.getDossierStatus);
|
|
104
134
|
const { toast, show: showToast } = useToast();
|
|
135
|
+
const [confirmExit, setConfirmExit] = useState(false);
|
|
136
|
+
|
|
137
|
+
// Seed dossier icons from server-side flags when jobs load
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (allJobs.length) ai.seedDossierFlags(allJobs);
|
|
140
|
+
}, [allJobs, ai]);
|
|
141
|
+
|
|
142
|
+
// Kill claude processes on exit (Ctrl+C)
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const cleanup = () => ai.cancel();
|
|
145
|
+
process.on('SIGINT', cleanup);
|
|
146
|
+
process.on('SIGTERM', cleanup);
|
|
147
|
+
return () => {
|
|
148
|
+
process.removeListener('SIGINT', cleanup);
|
|
149
|
+
process.removeListener('SIGTERM', cleanup);
|
|
150
|
+
};
|
|
151
|
+
}, [ai]);
|
|
105
152
|
|
|
106
153
|
// Apply inline search filter
|
|
107
154
|
const jobs = useMemo(() => {
|
|
@@ -128,12 +175,12 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
128
175
|
.catch(() => {});
|
|
129
176
|
}, [api]);
|
|
130
177
|
|
|
131
|
-
// Update selectedJob when cursor moves in detail view
|
|
178
|
+
// Update selectedJob when cursor moves or jobs list changes in detail view
|
|
132
179
|
useEffect(() => {
|
|
133
180
|
if (view === 'detail' && jobs[cursor]) {
|
|
134
181
|
setSelectedJob(jobs[cursor]);
|
|
135
182
|
}
|
|
136
|
-
}, [cursor, view]);
|
|
183
|
+
}, [cursor, view, jobs]);
|
|
137
184
|
|
|
138
185
|
// Inline search escape handler
|
|
139
186
|
useInput(
|
|
@@ -153,7 +200,19 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
153
200
|
if (view === 'filters' || view === 'searches' || view === 'help') return;
|
|
154
201
|
if (inlineSearch) return;
|
|
155
202
|
|
|
156
|
-
if (input === 'q' && view === 'list')
|
|
203
|
+
if (input === 'q' && view === 'list') {
|
|
204
|
+
if (ai.hasActiveProcess && !confirmExit) {
|
|
205
|
+
showToast(
|
|
206
|
+
'Claude dossier still running — press q again to quit',
|
|
207
|
+
'warning'
|
|
208
|
+
);
|
|
209
|
+
setConfirmExit(true);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
ai.cancel();
|
|
213
|
+
exit();
|
|
214
|
+
}
|
|
215
|
+
if (input !== 'q') setConfirmExit(false);
|
|
157
216
|
if (input === 'q' && view === 'detail') setView('list');
|
|
158
217
|
if (input === 'R' && (view === 'list' || view === 'detail')) {
|
|
159
218
|
forceRefresh();
|
|
@@ -176,9 +235,12 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
176
235
|
setView('detail');
|
|
177
236
|
}
|
|
178
237
|
if (key.escape && view === 'detail') setView('list');
|
|
238
|
+
if (input === 'c' && view === 'detail' && selectedJob) {
|
|
239
|
+
handleDossier(selectedJob);
|
|
240
|
+
}
|
|
179
241
|
|
|
180
242
|
if (key.escape && view === 'ai') {
|
|
181
|
-
|
|
243
|
+
// Don't kill running dossier — just hide the panel
|
|
182
244
|
setView(selectedJob ? 'detail' : 'list');
|
|
183
245
|
}
|
|
184
246
|
|
|
@@ -219,6 +281,11 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
219
281
|
setView('ai');
|
|
220
282
|
ai.summarizeJob(job);
|
|
221
283
|
};
|
|
284
|
+
const handleDossier = (job) => {
|
|
285
|
+
setSelectedJob(job);
|
|
286
|
+
setView('ai');
|
|
287
|
+
ai.dossier(job, api);
|
|
288
|
+
};
|
|
222
289
|
const handleAIBatch = (visibleJobs) => {
|
|
223
290
|
setView('ai');
|
|
224
291
|
ai.batchReview(visibleJobs);
|
|
@@ -252,6 +319,16 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
252
319
|
|
|
253
320
|
const counts = {
|
|
254
321
|
all: allJobs.length,
|
|
322
|
+
new: allJobs.filter(
|
|
323
|
+
(j) =>
|
|
324
|
+
!j.state &&
|
|
325
|
+
!j.has_dossier &&
|
|
326
|
+
ai.getDossierStatus(j.id) !== 'done' &&
|
|
327
|
+
ai.getDossierStatus(j.id) !== 'generating'
|
|
328
|
+
).length,
|
|
329
|
+
reviewed: allJobs.filter(
|
|
330
|
+
(j) => (j.has_dossier || ai.getDossierStatus(j.id) === 'done') && !j.state
|
|
331
|
+
).length,
|
|
255
332
|
interested: allJobs.filter((j) => j.state === 'interested').length,
|
|
256
333
|
applied: allJobs.filter((j) => j.state === 'applied').length,
|
|
257
334
|
maybe: allJobs.filter((j) => j.state === 'maybe').length,
|
|
@@ -312,6 +389,9 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
312
389
|
onCursorChange: setCursor,
|
|
313
390
|
onSelect: handleSelect,
|
|
314
391
|
onMark: handleMark,
|
|
392
|
+
onAISummary: handleAISummary,
|
|
393
|
+
onDossier: handleDossier,
|
|
394
|
+
getDossierStatus: ai.getDossierStatus,
|
|
315
395
|
isActive: true,
|
|
316
396
|
compact: true,
|
|
317
397
|
reservedRows: 8,
|
|
@@ -327,6 +407,8 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
327
407
|
onBack: handleBack,
|
|
328
408
|
onMark: handleMark,
|
|
329
409
|
onAISummary: handleAISummary,
|
|
410
|
+
onDossier: handleDossier,
|
|
411
|
+
getDossierStatus: ai.getDossierStatus,
|
|
330
412
|
isActive: false,
|
|
331
413
|
isPanel: true,
|
|
332
414
|
})
|
|
@@ -336,6 +418,74 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
336
418
|
);
|
|
337
419
|
}
|
|
338
420
|
|
|
421
|
+
// Split-pane: compact list on left, AI/dossier on right
|
|
422
|
+
if (view === 'ai' && selectedJob) {
|
|
423
|
+
return h(
|
|
424
|
+
Box,
|
|
425
|
+
{ flexDirection: 'column', height: process.stdout.rows || 40 },
|
|
426
|
+
header,
|
|
427
|
+
h(
|
|
428
|
+
Box,
|
|
429
|
+
{ flexGrow: 1, flexDirection: 'row' },
|
|
430
|
+
// Left pane: compact job list
|
|
431
|
+
h(
|
|
432
|
+
Box,
|
|
433
|
+
{
|
|
434
|
+
flexDirection: 'column',
|
|
435
|
+
width: '40%',
|
|
436
|
+
borderStyle: 'single',
|
|
437
|
+
borderColor: 'gray',
|
|
438
|
+
borderRight: true,
|
|
439
|
+
borderLeft: false,
|
|
440
|
+
borderTop: false,
|
|
441
|
+
borderBottom: false,
|
|
442
|
+
},
|
|
443
|
+
h(JobList, {
|
|
444
|
+
jobs,
|
|
445
|
+
cursor,
|
|
446
|
+
tab,
|
|
447
|
+
onCursorChange: setCursor,
|
|
448
|
+
onSelect: handleSelect,
|
|
449
|
+
onMark: handleMark,
|
|
450
|
+
onAISummary: handleAISummary,
|
|
451
|
+
onDossier: handleDossier,
|
|
452
|
+
getDossierStatus: ai.getDossierStatus,
|
|
453
|
+
isActive: false,
|
|
454
|
+
compact: true,
|
|
455
|
+
reservedRows: 8,
|
|
456
|
+
})
|
|
457
|
+
),
|
|
458
|
+
// Right pane: AI/dossier panel
|
|
459
|
+
h(
|
|
460
|
+
Box,
|
|
461
|
+
{ flexDirection: 'column', width: '60%' },
|
|
462
|
+
h(AIPanel, {
|
|
463
|
+
text: ai.text,
|
|
464
|
+
loading: ai.loading,
|
|
465
|
+
error: ai.error,
|
|
466
|
+
mode: ai.mode,
|
|
467
|
+
job: selectedJob,
|
|
468
|
+
onMark: handleMark,
|
|
469
|
+
onDismiss: () => {
|
|
470
|
+
setView(selectedJob ? 'detail' : 'list');
|
|
471
|
+
},
|
|
472
|
+
onExport: () => {
|
|
473
|
+
const f = ai.exportDossier(selectedJob);
|
|
474
|
+
if (f) showToast(`Saved ./${f}`, 'export');
|
|
475
|
+
return f;
|
|
476
|
+
},
|
|
477
|
+
onRegenerate: (job) => {
|
|
478
|
+
ai.regenerateDossier(job, api);
|
|
479
|
+
showToast('Regenerating dossier…', 'info');
|
|
480
|
+
},
|
|
481
|
+
isActive: true,
|
|
482
|
+
})
|
|
483
|
+
)
|
|
484
|
+
),
|
|
485
|
+
statusBar
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
339
489
|
// Full-width list view
|
|
340
490
|
return h(
|
|
341
491
|
Box,
|
|
@@ -350,6 +500,7 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
350
500
|
onSelect: handleSelect,
|
|
351
501
|
onMark: handleMark,
|
|
352
502
|
onAISummary: handleAISummary,
|
|
503
|
+
onDossier: handleDossier,
|
|
353
504
|
onAIBatch: handleAIBatch,
|
|
354
505
|
onExport: handleExport,
|
|
355
506
|
isActive: !inlineSearch,
|
|
@@ -384,14 +535,15 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
384
535
|
onClose: () => setView('list'),
|
|
385
536
|
})
|
|
386
537
|
: null,
|
|
387
|
-
view === 'ai'
|
|
538
|
+
view === 'ai' && !selectedJob
|
|
388
539
|
? h(AIPanel, {
|
|
389
540
|
text: ai.text,
|
|
390
541
|
loading: ai.loading,
|
|
391
542
|
error: ai.error,
|
|
543
|
+
mode: ai.mode,
|
|
392
544
|
onDismiss: () => {
|
|
393
545
|
ai.clear();
|
|
394
|
-
setView(
|
|
546
|
+
setView('list');
|
|
395
547
|
},
|
|
396
548
|
isActive: true,
|
|
397
549
|
})
|
package/src/tui/HelpModal.js
CHANGED
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/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,6 +20,7 @@ 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
|
],
|
|
24
26
|
search: [
|
|
@@ -39,7 +41,13 @@ const KEYS = {
|
|
|
39
41
|
['d', 'delete'],
|
|
40
42
|
['esc', 'close'],
|
|
41
43
|
],
|
|
42
|
-
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
|
+
],
|
|
43
51
|
help: [['?/esc', 'close']],
|
|
44
52
|
};
|
|
45
53
|
|